nodejs 随手记(持续)

  • start date: 2018-08-27 13:00:45

基本

结构

nodejs 是一个基于 Chrome V8 引擎的 js 运行环境。nodejs 使用了一个事件驱动、非阻塞式 I/O 的模型,使其轻量级又高效。

p-2.jpg

  • Node.js 标准库,这部分是由 Javascript 编写的,即我们使用过程中直接能调用的 API。在源码中的 lib 目录下可以看到。
  • Node bindings,这一层是 Javascript 与底层 C/C++ 能够沟通的关键,前者通过 bindings 调用后者,相互交换数据。实现在 http://node.cc,这一层是支撑 Node.js 运行的关键,由 C/C++ 实现。
  • V8:Google 推出的 Javascript VM,也是 Node.js 为什么使用的是 Javascript 的关键,它为 Javascript 提供了在非浏览器端运行的环境,它的高效是 Node.js 之所以高效的原因之一。
  • Libuv:跨平台,是自行开发的,拓展了 js 的能力,使得 js 既可以在前端进行 DOM 操作又可以在后端调用操作系统资源(线程池,事件池,异步 I/O 等),是 Node.js 如此强大的关键。
  • C-ares:提供了异步处理 DNS 相关的能力。
  • http_parser、OpenSSL、zlib 等:提供包括 http 解析、SSL、数据压缩等其他的能力。

特点

  • 事件驱动
  • 非阻塞 IO 模型(异步)
  • 轻量和高效

npm

镜像及切换

常用镜像:

切换

临时切换镜像安装

1
npm --registry 镜像源 install 安装包

1
npm --registry https://registry.npm.taobao.org install webpack

永久切换

1
npm config set registry 镜像源

1
npm config set registry http://r.cnpmjs.org/

linux 部署环境

  • 1.下载包,如:node-v8.2.1-linux-x64.tar.xz
  • 2.解压包,
1
tar -xvf node-v8.2.1-linux-x64.tar.xz

检查node-v8.2.1-linux-x64目录下是否包含 bin 目录。

  • 3.建立软连接,(如目录地址为/var/www/html/node-v8.2.1-linux-x64)
1
2
   ln -s /var/www/html/node-v8.2.1-linux-x64/bin/npm /usr/loacl/bin/
ln -s /var/www/html/node-v8.2.1-linux-x64/bin/node /usr/loacl/bin/

检查:

1
2
node -v
npm -v
  • 4.添加全局变量,以确保 npm 全局安装,如
1
export PATH=NODE_HOME=/var/www/html/node-v8.2.1-linux-x64/bin:/usr/local/sbin:/usr/local/bin:/sbin:/bin:/usr/sbin:/usr/bin:$PATH

或修改/etc/profile 文件,末尾输入上述命令。

检查:

1
2
npm i -g cnpm
cnpm -v

1 文件系统

fs 模块(内置)

文件读取

普通读取 fs.readFileSync()(同步)/fs.readFile()(异步)

通过文件流读取

适合大文件读取

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const fs = require('fs');
const readStream = fs.createReadStream('./fileForRead.txt', 'utf8');

readStream
.on('data', function (chunk) {
console.log('读取数据: ' + chunk);
})
.on('error', function (err) {
console.log('出错: ' + err.message);
})
.on('end', function () {
// 没有数据了
console.log('没有数据了');
})
.on('close', function () {
// 已经关闭,不会再有事件抛出
console.log('已经关闭');
});

文件写入

普通写入 fs.writeFileSync()(同步)/fs.writeFile()(异步)

通过文件流写入

1
2
3
4
5
6
7
8
9
10
11
const fs = require('fs');
const writeStream = fs.createWriteStream('./fileForWrite1.txt', 'utf8');

writeStream.on('close', function () {
// 已经关闭,不会再有事件抛出
console.log('已经关闭');
});

writeStream.write('hello');
writeStream.write('nodejs');
writeStream.end('');

文件是否存在/文件或目录的用户权限

fs.access()(异步)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
const file = 'package.json';

// 检查文件是否存在于当前目录。
fs.access(file, fs.constants.F_OK, err => {
console.log(`${file} ${err ? '不存在' : '存在'}`);
});

// 检查文件是否可读。
fs.access(file, fs.constants.R_OK, err => {
console.log(`${file} ${err ? '不可读' : '可读'}`);
});

// 检查文件是否可写。
fs.access(file, fs.constants.W_OK, err => {
console.log(`${file} ${err ? '不可写' : '可写'}`);
});

// 检查文件是否存在于当前目录,且是否可写。
fs.access(file, fs.constants.F_OK | fs.constants.W_OK, err => {
if (err) {
console.error(`${file} ${err.code === 'ENOENT' ? '不存在' : '只可读'}`);
} else {
console.log(`${file} 存在,且可写`);
}
});

fs.accessSync()(同步)

1
2
3
4
5
6
try {
fs.accessSync('etc/passwd', fs.constants.R_OK | fs.constants.W_OK);
console.log('可读可写');
} catch (err) {
console.error('不可访问!');
}

创建目录 fs.mkdirSync()(同步)/fs.msdir()(异步)

删除文件

fs.unlinkSync()(同步)

1
2
const fs = require('fs');
fs.unlinkSync('./fileForUnlink.txt');
1
2
3
4
5
const fs = require('fs');
fs.unlink('./fileForUnlink.txt', function (err) {
if (err) throw err;
console.log('删除成功');
});

遍历目录

fs.readdirSync()很坑。。。要读取所有子文件必须遍历

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const fs = require('fs');
const path = require('path');

const getFilesInDir = function (dir) {
let results = [path.resolve(dir)];
let files = fs.readdirSync(dir, 'utf8');

files.forEach(function (file) {
file = path.resolve(dir, file);
let stats = fs.statSync(file);

if (stats.isFile()) {
results.push(file);
} else if (stats.isDirectory()) {
results = results.concat(getFilesInDir(file));
}
});

return results;
};

let files = getFilesInDir('testFolder');
console.log(files);

文件重命名

fs.rename()(异步)

1
2
3
4
5
6
const fs = require('fs');

fs.rename('./hello', './world', function (err) {
if (err) throw err;
console.log('重命名成功');
});

fs.renameSync()(异步)

1
2
3
const fs = require('fs');

fs.renameSync('./hello', './world');

监听文件修改

*fs.watch()比 fs.watchFile()高效。

fs.watchFile()

实现原理:轮询。每隔一段时间检查文件是否发生变化。所以在不同平台上表现基本是一致的。

1
2
3
4
5
6
7
8
9
10
11
12
const fs = require('fs');

const options = {
persistent: true, // 默认就是true
interval: 2000, // 多久检查一次
};

// curr, prev 是被监听文件的状态, fs.Stat实例
// 可以通过 fs.unwatch() 移除监听
fs.watchFile('./fileForWatch.txt', options, function (curr, prev) {
console.log('修改时间为: ' + curr.mtime);
});

fs.watch()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
const fs = require('fs');

const options = {
persistent: true,
recursive: true,
encoding: 'utf8',
};

fs.watch('../', options, function (event, filename) {
console.log('触发事件:' + event);
if (filename) {
console.log('文件名是: ' + filename);
} else {
console.log('文件名是没有提供');
}
});

*修改所有者 fs.chmodSync()(同步)/fs.chmod()(异步)

获取文件状态

fs.stat()、fs.fstat()、fs.lstat()。

fs.stat()和 fs.fstat():传文件路径 vs 文件句柄。
fs.stat()和 fs.lstat():如果文件是软链接,那么 fs.stat()返回目标文件的状态,fs.lstat()返回软链接本身的状态。

追加文件内容 fs.appendFileSync()(同步)/fs.appendFile()(异步)

*文件内容截取 fs.truncate()、fs.ftruncate()

*修改文件属性(时间)fs.utimes()、fs.futimes()

软链接和硬链接:

  • 硬链接:inode 相同,多个别名。删除一个硬链接文件,不会影响其他有相同 inode 的文件。
  • 软链接:有自己的 inode,用户数据块存放指向文件的 inode。

创建临时目录

1
2
3
4
5
6
const fs = require('fs');

fs.mkdtemp('/tmp/', function (err, folder) {
if (err) throw err;
console.log('创建临时目录: ' + folder);
});

删除目录 fs.rmdirSync()(同步)/fs.rmdir()(异步)

2 开启 gzip

zlib 模块(内置)
zlib 模块提供通过 Gzip 和 Deflate/Inflate 实现的压缩功能,可以通过这样使用它:

1
const zlib = require('zlib');

压缩范例:

1
2
3
4
5
6
7
8
const fs = require('fs');
const zlib = require('zlib');
const gzip = zlib.createGzip();

let inFile = fs.createReadStream('./fileForCompress.txt');
let out = fs.createWriteStream('./fileForCompress.txt.gz');

inFile.pipe(gzip).pipe(out);

解压范例:

1
2
3
4
5
6
7
8
const fs = require('fs');
const zlib = require('zlib');
const gunzip = zlib.createGunzip();

let inFile = fs.createReadStream('./fileForCompress.txt.gz');
let outFile = fs.createWriteStream('./fileForCompress1.txt');

inFile.pipe(gunzip).pipe(outFile);

服务端 gzip 压缩

判断是否包含 accept-encoding 首部,且值为 gzip。是则返回 gzip 压缩后的文件,否将返回未压缩的文件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
const fs = require('fs');
const http = require('http');
const zlib = require('zlib');
const filepath = './fileForGzip.html';

let server = http.createServer(function (req, res) {
let acceptEncoding = req.headers['accept-encoding'];
let gzip;

if (~acceptEncoding.indexOf('gzip')) {
// 判断是否需要gzip压缩
gzip = zlib.createGzip();

// 记得响应 Content-Encoding,告诉浏览器:文件被 gzip 压缩过
res.writeHead(200, {
'Content-Encoding': 'gzip',
});
fs.createReadStream(filepath).pipe(gzip).pipe(res);
} else {
fs.createReadStream(filepath).pipe(res);
}
});

server.listen('8080');

服务端字符串 gzip 压缩

方案:使用 zlib.gzipSync()方法对字符串进行 gzip 压缩。
如:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
const http = require('http');
const zlib = require('zlib');

const responseText = 'hello nodejs.';

let server = http.createServer(function (req, res) {
let acceptEncoding = req.headers['accept-encoding'];
if (~acceptEncoding.indexOf('gzip')) {
res.writeHead(200, {
'content-encoding': 'gzip',
});
res.end(zlib.gzipSync(responseText));
} else {
res.end(responseText);
}
});

server.listen('3000');

3 域名解析

dns.lookup()

1
2
3
4
5
6
const dns = require('dns');

dns.lookup('www.baidu.com', function (err, address, family) {
if (err) throw err;
console.log(address);
});

获取一个域名对应的多个 ip

1
2
3
4
5
6
7
const dns = require('dns');
const options = { all: true };

dns.lookup('www.baidu.com', options, function (err, address, family) {
if (err) throw err;
console.log(address);
});

dns.resolve4()

1
2
3
4
5
6
const dns = require('dns');

dns.resolve4('id.baidu.com', function (err, address) {
if (err) throw err;
console.log(JSON.stringify(address));
});

dns.lookup()和 dns.resolve4()

当配置了本地 Host 时,dns.lookup()方法会返回 host 的 ip 地址,dns.resolve4()无影响。

4 事件循环机制

Node.js 是基于 V8 引擎的 javascript 运行环境. Node.js 具有事件驱动, 非阻塞 I/O 等特点. 结合 Node API, Node.js 具有网络编程, 文件系统等服务端的功能, Node.js 用 libuv 库进行异步事件处理。

4.1 线程

Node.js 的单线程含义, 实际上说的是执行同步代码的主线程. 一个 Node 程序的启动, 不止是分配了一个线程,而是我们只能在一个线程执行代码. 当出现 I/O 资源调用, TCP 连接等外部资源申请的时候, 不会阻塞主线程, 而是委托给 I/O 线程进行处理,并且进入等待队列。 一旦主线程执行完成,将会消费事件队列(Event Queue)。 因为只有一个主线程, 只占用 CPU 内核处理逻辑计算, 因此不适合在 CPU 密集型进行使用。

p-1.png

什么是事件循环(EventLoop) ?

EventLoop 是一种常用的机制,通过对内部或外部的事件提供者发出请求, 如文件读写, 网络连接 等异步操作, 完成后调用事件处理程序. 整个过程都是异步阶段

Node.js 的事件循环机制指当 Node.js 启动, 就会初始化一个 event loop, 处理脚本时, 可能会发生异步 API 行为调用, 使用定时器任务或者 nexTick, 处理完成后进入事件循环处理过程。

事件循环阶段

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
   ┌───────────────────────┐
┌─>│ timers │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ I/O callbacks │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
│ │ idle, prepare │
│ └──────────┬────────────┘ ┌───────────────┐
│ ┌──────────┴────────────┐ │ incoming: │
│ │ poll │<─────┤ connections, │
│ └──────────┬────────────┘ │ data, etc. │
│ ┌──────────┴────────────┐ └───────────────┘
│ │ check │
│ └──────────┬────────────┘
│ ┌──────────┴────────────┐
└──┤ close callbacks │
└───────────────────────┘

每一个阶段都有一个 FIFO 的 callbacks 队列, 每个阶段都有自己的事件处理方式. 当事件循环进入某个阶段时, 将会在该阶段内执行回调,直到队列耗尽或者回调的最大数量已执行, 那么将进入下一个处理阶段.

  • timers 阶段: 这个阶段执行 setTimeout(callback) and setInterval(callback)预定的 callback;
  • I/O callbacks 阶段: 执行除了 close 事件的 callbacks、被 timers(定时器,setTimeout、setInterval 等)设定的 callbacks、setImmediate()设定的 callbacks 之外的 callbacks; (目前这个阶段)
  • idle, prepare 阶段: 仅 node 内部使用;
  • poll 阶段: 获取新的 I/O 事件, 适当的条件下 node 将阻塞在这里;
  • check 阶段: 执行 setImmediate() 设定的 callbacks;
  • close callbacks 阶段: 比如 socket.on(‘close’, callback)的 callback 会在这个阶段执行.

5 CommonJS 和 ES6 模块的区别

  • require 支持 动态导入,import 不支持,正在提案 (babel 下可支持)
  • require 是 同步 导入,import 属于 异步 导入
  • require 是 值拷贝,导出值变化不会影响导入值;import 指向内存地址,导入值会随导出值而变化

5.1 CommonJS

  • 通过 require 引入基础数据类型时,属于复制该变量。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值。
  • 通过 require 引入复杂数据类型时,数据浅拷贝该对象。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
  • 当使用 require 命令加载某个模块时,就会运行整个模块的代码。
  • 当使用 require 命令加载同一个模块时,不会再执行该模块,而是取到缓存之中的值。也就是说,CommonJS 模块无论加载多少次,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
  • 循环加载时,属于加载时执行。即脚本代码在 require 的时候,就会全部执行。一旦出现某个模块被”循环加载”,就只输出已经执行的部分,还未执行的部分不会输出。

5.2 ES6 模块

  • ES6 模块中的值属于【动态只读引用】。
  • 对于只读来说,即不允许修改引入变量的值,import 的变量是只读的,不论是基本数据类型还是复杂数据类型。当模块遇到 import 命令时,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
  • 对于动态来说,原始值发生变化,import 加载的值也会发生变化。不论是基本数据类型还是复杂数据类型。
  • 循环加载时,ES6 模块是动态引用。只要两个模块之间存在某个引用,代码就能够执行。
    上面说了一些重要区别。

6 Fork 与子进程

nodejs 自带的 child_process 模块有一个 fork 方法,可以用于创建子进程。

语法:

1
child_process.fork(modulePath[, args][, options])

注意:

  • 1、该接口专门用于衍生新的 Node.js 进程
  • 2、modulePath 是要在 node 子进程中运行的模块,由于是 node.js 的进程,所以可以是 start.js 这种 js 文件
  • 3、无回调,参数要以第二个参数传入
  • 4、返回的子进程将内置一个额外的 ipc 通信通道,允许消息在父进程和子进程之间来回传递。

如:

1
2
const fork = require('child_process').fork;
fork('./child.js');

父子通信

通过注册 message 事件。

1
2
3
4
5
6
7
8
9
10
11
12
/**
* @file father.js
*/

const fork = require('child_process').fork;

const child = fork('./child.js');
child.on('message', function (msg) {
console.log('get msg: ' + msg);
});

child.send('msg from father');
1
2
3
4
5
6
7
8
/**
* @file child.js
*/

process.end('msg from child');
process.on('message', function (msg) {
console.log('get msg: ' + msg);
});

7 nodejs 升级和版本切换

  • 安装n模块
1
sudo npm install -g n
  • 升级 node.js 到最新稳定版
1
sudo n stable
  • 升级 node.js 到最新版
1
sudo n latest
  • 切换到指定版本
1
sudo n 8.9.4
  • 用指定版本执行脚本
1
sudo n use 8.9.4 someScript.js
  • 查看已安装的所有版本
1
sudo n

8 os 模块

用于获取操作系统(OS)的相关信息。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const os = require('os');

// 系统信息
os.arch(); // 操作系统 CPU 架构,如"x64"
os.platform(); // 编译时的操作系统名,如"win32"
os.type(); // 操作系统类型,如"Windows_NT"
os.uptime(); // 操作系统运行的时间,以秒为单位。如53355.5412312
os.hostname(); // 操作系统的主机名。如"wayne"
os.release(); // 操作系统的发行版本。如"6.1.1234"

// 路径信息
os.homedir(); // 主目录路径,如"C:\\Users\Wayne"
os.tmpdir(); // 默认临时文件目录,如"C:\\Users\Wayne\AppData\Local\Temp"

// 内存信息
os.freemem(); // 可用的RAM,如9123456786
os.totalmem(); // 整个内存容量,如12123456786

os-utils

npm:https://www.npmjs.com/package/os-utils

可用于获取 cpu 信息,如

1
2
3
4
5
6
7
8
9
10
11
const os = require('os-utils');

// cpu 使用率
os.cpuUsage(function (v) {
console.log('CPU Usage (%): ' + v);
});

// cpu空闲时执行
os.cpuFree(function () {
// ...
});

*9.REPL

REPL 的全称:Read、Eval、Print、Loop。进入 REPL 环境:

1
node

退出 REPL 环境:

1
.exit

1
process.exit();

10.package.json 和 node_modules

package.json 字段介绍

packageJson.jpg

node 模块

Node.js 的模块分为两类,一类为原生(核心)模块,一类为文件模块。原生模块在 Node.js 源代码编译的时候编译进了二进制执行文件,加载的速度最快。另一类文件模块是动态加载的,加载速度比原生模块慢。但是 Node.js 对原生模块和文件模块都进行了缓存,于是在第二次 require 时,是不会有重复开销的。其中原生模块都被定义在 lib 这个目录下面,文件模块则不定性。

实际上在文件模块中,又分为 3 类模块。这三类文件模块以后缀来区分,Node.js 会根据后缀名来决定加载方法。

  • .js。通过 fs 模块同步读取 js 文件并编译执行。
  • .node。通过 C/C++ 进行编写的 Addon。通过 dlopen 方法进行加载。
  • .json。读取文件,调用 JSON.parse 解析加载。

Node.js 在编译 js 文件的过程中实际完成的步骤有对 js 文件内容进行头尾包装。以 app.js 为例,包装之后的 app.js 将会变成以下形式:

1
2
3
4
(function (exports, require, module, __filename, __dirname) {
var circle = require('./circle.js');
console.log('The area of a circle of radius 4 is ' + circle.area(4));
});

这段代码会通过 vm 原生模块的 runInThisContext 方法执行(类似 eval,只是具有明确上下文,不污染全局),返回为一个具体的 function 对象。最后传入 module 对象的 exports,require 方法,module,文件名,目录名作为实参并执行。

这就是为什么 require 并没有定义在 app.js 文件中,但是这个方法却存在的原因。从 Node.js 的 API 文档中可以看到还有 filename、dirname、module、exports 几个没有定义但是却存在的变量。其中 filename 和 dirname 在查找文件路径的过程中分析得到后传入的。module 变量是这个模块对象自身,exports 是在 module 的构造函数中初始化的一个空对象({},而不是 null)。

在这个主文件中,可以通过 require 方法去引入其余的模块。而其实这个 require 方法实际调用的就是 load 方法。

load 方法在载入、编译、缓存了 module 后,返回 module 的 exports 对象。这就是 circle.js 文件中只有定义在 exports 对象上的方法才能被外部调用的原因。

以上所描述的模块载入机制均定义在 lib/module.js 中。

一个符合 CommonJS 规范的包应该是如下这种结构:

  • 一个 package.json 文件应该存在于包顶级目录下
  • 二进制文件应该包含在 bin 目录下。
  • JavaScript 代码应该包含在 lib 目录下。
  • 文档应该在 doc 目录下。
  • 单元测试应该在 test 目录下。

Node.js 在没有找到目标文件时,会将当前目录当作一个包来尝试加载,所以在 package.json 文件中最重要的一个字段就是 main。而实际上,这一处是 Node.js 的扩展,标准定义中并不包含此字段,对于 require,只需要 main 属性即可。但是在除此之外包需要接受安装、卸载、依赖管理,版本管理等流程,所以 CommonJS 为 package.json 文件定义了如下一些必须的字段:

  • name。包名,需要在 NPM 上是唯一的。不能带有空格。
  • description。包简介。通常会显示在一些列表中。
  • version。版本号。一个语义化的版本号( http://semver.org/ ),通常为 x.y.z。该版本号十分重要,常常用于一些版本控制的场合。
  • keywords。关键字数组。用于 NPM 中的分类搜索。
  • maintainers。包维护者的数组。数组元素是一个包含 name、email、web 三个属性的 JSON 对象。
  • contributors。包贡献者的数组。第一个就是包的作者本人。在开源社区,如果提交的 patch 被 merge 进 master 分支的话,就应当加上这个贡献 patch 的人。格式包含 name 和 email。如:
1
2
3
4
5
6
7
"contributors": [{
"name": "Jackson Tian",
"email": "mail @gmail.com"
}, {
"name": "fengmk2",
"email": "mail2@gmail.com"
}]
  • bugs。一个可以提交 bug 的 URL 地址。可以是邮件地址(mailto:mailxx@domain),也可以是网页地址(http://url)。
  • licenses。包所使用的许可证。例如:
1
2
3
4
"licenses": [{
"type": "GPLv2",
"url": "http://www.example.com/licenses/gpl.html",
}]
  • repositories。托管源代码的地址数组。
  • dependencies。当前包需要的依赖。这个属性十分重要,NPM 会通过这个属性,帮你自动加载依赖的包。
    以下是 Express 框架的 package.json 文件,值得参考。
1
2
3
4
5
6
7

{
"name": "express",
"description": "Sinatra inspired web development framework",
"version": "3.0.0alpha1-pre",
"author": "TJ Holowaychuk
}

除了前面提到的几个必选字段外,我们还发现了一些额外的字段,如 bin、scripts、engines、devDependencies、author。这里可以重点提及一下 scripts 字段。包管理器(NPM)在对包进行安装或者卸载的时候需要进行一些编译或者清除的工作,scripts 字段的对象指明了在进行操作时运行哪个文件,或者执行拿条命令。如下为一个较全面的 scripts 案例:

1
2
3
4
5
6
7
8

"scripts": {
"install": "install.js",
"uninstall": "uninstall.js",
"build": "build.js",
"doc": "make-doc.js",
"test": "test.js",
}

11 package.json 与 package-lock.json

package.json 中各依赖版本遵守semver 的语义版本控制,简单来说版本由主要版本.次要版本.补丁版本。补丁中的修改不会破坏任何内容的错误修复,次要版本的更改不会破坏任何内容的新功能,主要版本的更改代表一个破坏兼容性的大变化。

默认情况下,npm 安装最新版本,并预先插入版本号,如^1.2.12,这表示至少应该使用1.2.12版本,但任何高于此版本的版本都可以,只要它具有相同的主要版本。

比如我们需要安装 express,npm install express --save,在编写代码时最新版本是4.17.1,所以"express": "^4.17.1"作为我的 package.json 中的依赖项添加。但过一段时候,express 升级到了4.17.2,别人在 clone 项目时执行 npm install 后便安装的是4.17.2的版本。如果4.17.2出现了一些非兼容4.17.1的功能时便会出现问题,这也就是 package-lock 出现的原因。

package-lock.json就是为了避免在安装模块时会导致两种不同安装结果的问题。npm 在 v5 的版本时增加了 package-lock.json,因此如果你使用的 npm 版本大于等于 5,除非禁用此功能否则它会自动生成。

package-lock 的内容结构

package-lock 是 package.json 中列出的每个依赖项的大型列表,应安装的特定版本,模块的位置(URI),验证模块完整性的哈希,它需要的包列表 ,以及依赖项列表。 如 package-lock.json 中的 express:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
"express": {
"version": "4.17.1",
"resolved": "https://r.cnpmjs.org/express/download/express-4.17.1.tgz",
"integrity": "sha1-RJH8OGBc9R+GKdOcK10Cb5ikwTQ=",
"requires": {
"accepts": "~1.3.7",
"array-flatten": "1.1.1",
"body-parser": "1.19.0",
"content-disposition": "0.5.3",
"content-type": "~1.0.4",
"cookie": "0.4.0",
"cookie-signature": "1.0.6",
"debug": "2.6.9",
"depd": "~1.1.2",
"encodeurl": "~1.0.2",
"escape-html": "~1.0.3",
"etag": "~1.8.1",
"finalhandler": "~1.1.2",
"fresh": "0.5.2",
"merge-descriptors": "1.0.1",
"methods": "~1.1.2",
"on-finished": "~2.3.0",
"parseurl": "~1.3.3",
"path-to-regexp": "0.1.7",
"proxy-addr": "~2.0.5",
"qs": "6.7.0",
"range-parser": "~1.2.1",
"safe-buffer": "5.1.2",
"send": "0.17.1",
"serve-static": "1.14.1",
"setprototypeof": "1.1.1",
"statuses": "~1.5.0",
"type-is": "~1.6.18",
"utils-merge": "1.0.1",
"vary": "~1.1.2"
}
}

requires 包含 express 每个依赖包的版本,并且在执行 npm install 命令时 npm 会使用 package-lock.json 而不是 package.json 来解析和安装模块,因为 package-lock 能保证每个依赖项的指定版本、位置和完整性哈希,以保证每次安装都是相同的。

package-lock 的争议

在 npm 5.x.x 之前,package.json 是项目的真实来源,npm 用户喜欢这个模型,并且非常习惯于维护他们的包文件。 但是,当首次引入 package-lock 时,它的行为与有多少人预期的相反。 给定一个预先存在的包和 package-lock,对 package.json 的更改(许多用户认为是真实的来源)没有同步到 package-lock 中。

例如:包 A,版本 1.0.0 在 package.json 和 package.lock.json 中。 在 package.json 中,A 被手动编辑为 1.1.0 版。 如果认为 package.json 是真实来源的用户运行。
package-lock.json 中不存在模块,但它存在于 package.json 中,作为一个将 package.json 视为真实来源的用户,我希望能够安装我的模块。 但是,由于 package-lock.json 不存在该模块,因此未安装该模块,并且我的代码因无法找到模块而失败。

大部分时间,因为我们无法弄清楚为什么我们的依赖关系没有被正确安装,要么删除了 package-lock.json 并重新安装,要么完全禁用 package-lock.json 来解决问题。

期望与真实行为之间的这种冲突在 npm repo 中引发了一个非常有趣的问题线索。 有些人认为 package.json 应该是事实的来源,有些人认为,因为 package-lock 是 npm 用来创建安装的东西,所以应该被认为是事实的来源。 这场争议的解决方案在于 PR#17508。 如果 package.json 已更新,Npm 维护者添加了一个更改,导致 package.json 覆盖 package-lock。 现在,在上述两种情况下,都会正确安装用户期望安装的软件包。 此更改是作为 npm v5.1.0 的一部分发布的,该版本于 2017 年 7 月 5 日上线。

使用 cnpm install 时候,并不会生成 package-lock.json 文件。并且,在 cnpm 安装的时候,就算你项目中有 package-lock.json 文件,cnpm 也不会识别,仍会根据 package.json 来安装。

在考虑 package-lock 的依赖安装时,推荐使用 npm ci 命令,而不是 npm install。

12 大文件逐行读取:readline

fs.readFile()fs.readFileSync()打开文件的时候需要将整个文件读取到内存中,如果遇到大文件的情况就会遇到错误,因此需要逐行读取。

readeline 模块

内置模块,每次从任何可读的流中读取一行。使用如:

1
2
3
4
5
6
7
8
9
const readline = require('readline');

const rl = readline.createInterface({
input: fs.createReadStream('file.txt'),
output: process.stdout,
terminal: false,
});

rl.on('line', line => console.log(line));

line-reader

安装:

1
npm i --save line-reader

line-reader 提供了读取给定文件的每一行的eachLine()方法。它接受一个带有两个参数的回调函数:行内容和一个布尔值,该值指定行 read 是否是文件的最后一行。如:

1
2
3
4
5
6
const lineReader = require('line-reader');

lineReader.eachLine('file.txt', (line, isLast) => {
console.log(line);
console.log('is last line: ', isLast);
});

使用 line-reader 的另一个好处是,当某些条件变为真时,可以停止读取。可以通过回调函数返回值来实现:

1
2
3
4
5
lineReader.eachLine('file.txt', (line, isLast) => {
console.log(line);
console.log('is last line: ', isLast);
if (line.includes('end')) return false; // 通知停止读取
});

LineByLine

安装:

1
npm i --save linebyline

linebyline 以 readline 模块为基础,为每行触发一个 line 事件,如:

1
2
3
4
5
6
const linebyline = require('linebyline');
const rl = linebyline('file.txt');

rl.on('line', (line, lineCount, byteCount) => {
console.log(line, lineCount, byteCount);
}).on('error', err => console.error(err));

常见问题解决汇总

1.不同操作系统下的 NODE_ENV

比如一个 package.json 设置的某个 script 如下:

1
2
3
"scripts": {
"run": "export NODE_ENV=development && npm i"
}

这在 mac 下没有问题,但 windows 上报错了,因为 windows 上没有 export。这时候可以用set代替export

1
2
3
"scripts": {
"run": "set NODE_ENV=development && npm i"
}