Node.js 教程
介绍 Node.js 运行时与常用模块、基本开发方式。适合会一点 JavaScript、想上手服务端的读者。
- fs 模块
- Promise
- Node 的 REPL 环境
- 定时器
- this 变量
- 概述
- 安装
- npm create 命令
- npm exec 命令
- npm init 命令
- npm link 命令
- npm tag
- npm token
- npm update
- npm version
- npm view 命令
- npm whoami
- npx 使用教程
- package.json
- 发布
- 脚本功能
- Yarn 的用法
- cluster 模块
- events 模块
- http 模块
- os 模块
- process 对象
fs 模块
fs.createReadStream()
fs.createReadStream方法读取一个文件,以 stream 的形式返回。
const readStream = fs.createReadStream(
inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 }
);
该方法的第一个参数是文件的路径,第二个参数是一个配置对象。
配置对象的encoding属性,决定了fs.createReadStream方法的返回值。如果该属性为null,返回的是二进制的 buffer;如果为字符串(比如utf8),返回的是这种编码的字符串。
配置对象的highWaterMark属性指定了每次返回的 buffer 或字符串的最大体积(单位字节)。
stream 以事件的形式获取。
readStream.on('data', (chunk) => {
console.log('>>> '+chunk);
});
readStream.on('end', () => {
console.log('### DONE ###');
});
Node v10 开始,Stream 有异步遍历器(asynchronous iteration)接口(即具有Symbol.asyncIterator属性),因此可以使用for-await-of读取。
async function main(inputFilePath) {
const readStream = fs.createReadStream(inputFilePath,
{ encoding: 'utf8', highWaterMark: 1024 });
for await (const chunk of readStream) {
console.log('>>> '+chunk);
}
console.log('### DONE ###');
}
参考链接
- Using async iteration natively in Node.js, by Axel Rauschmayer
Promise
UnhandledPromiseRejectionWarning
如果 Promise 运行过程中抛出错误,或者状态变为rejected,但是没有相应的处理代码,那么 Node 会抛出一个警告UnhandledPromiseRejectionWarning。
new Promise(function (resolve, reject) {
reject('message');
console.log('hello');
})
// hello
// UnhandledPromiseRejectionWarning: message
上面代码中,Promise 变为rejected状态,但是没有处理的代码,导致抛出警告UnhandledPromiseRejectionWarning。由于抛出的是警告,而不是错误,所以不影响hello的输出。请跟下面的代码比较一下。
new Promise(function (resolve, reject) {
throw new Error('message');
console.log('hello');
})
// UnhandledPromiseRejectionWarning: Error: message
上面代码中,Promise 内部抛出错误,没有得到处理,所以会有警告UnhandledPromiseRejectionWarning。Promise 内部的错误阻止 Promise 内部的代码继续向下运行,所以不会输出hello。
消除这个警告的方法很简单,就是为 Promise 加上错误处理代码,即catch代码块。
new Promise(function (resolve, reject) {
reject('message');
console.log('hello');
}).catch(function (err) { console.log (err) })
// hello
// message
上面代码中,由于catch代码块的存在,所以不会抛出警告。
async 函数里面使用 Promise,也要放在try/catch代码块之中。
const p = new Promise(function (resolve, reject) {
reject('message');
});
(async function () {
try {
await p;
} catch (err) {
console.log(err);
}
})();
// message
上面代码中,如果 Promise 没有放在try/catch代码块,就会抛出警告UnhandledPromiseRejectionWarning。
Node 还提供process对象的unhandledRejection事件,在抛出警告UnhandledPromiseRejectionWarning之前,就会触发这个事件。如果设置了这个事件的处理代码,就不会抛出警告UnhandledPromiseRejectionWarning。
new Promise(function (resolve, reject) {
reject('message');
});
process.on('unhandledRejection', function (err) {
console.log(err);
});
// message
Node 开发团队打算废除UnhandledPromiseRejectionWarning,以后如果没有相应的处理代码,Promise 内部抛错或者变为rejected状态,会导致 Node 进程终止执行,并且有一个非零的返回码。所以应该养成习惯,只要有 Promise,就要部署失败情况下的代码。
Node 的 REPL 环境
简介
REPL 是 read-eval-print-loop 的缩写,表示命令行下的 Node 引擎的一个互动式对话环境。用户在其中输入命令,就可以立刻看到结果。read 表示读取用户的输入,eval 表示执行,print 表示输出运行的结果,loop 表示重复执行这个过程。
命令行下输入node,就可以进入 Node 的 REPL 环境。
$ node
>
REPL 环境的提示符是一个大于号(>)。
退出 REPL,可以在行首按下 Ctrl + d,或者连续两次按下 Ctrl + c。
REPL 环境与 Node 脚本的执行环境基本相似,只有一些很小的差异。比如,REPL 环境不是通过脚本触发的,所以没有__dirname和__filename这两个内置变量。
REPL 会自动加载 Node 的核心模块,比如 fs、http、os、path等,不必require就可以直接使用。
$ node
> fs.read
[Function]
上面代码中,REPL 环境可以直接使用fs.read方法,不必先加载fs模块。
node命令的-e参数,实际上就是在 REPL 环境运行代码。
$ node -e "console.log(os.platform())"
darwin
_变量
REPL 环境下,有一个内置变量_,上一个表达式的值就存放在这个变量之中。
> require('./example');
{ some: 'some text' }
> _.some
'some text'
上面代码中,require()方法加载了一个脚本,这行表达式的值就自动存放在_里面。
REPL 允许用户对_变量赋值。
> _ = 'something'
'something'
> _
'something'
上面代码中,我们将something赋值给_,这时_就是一个普通变量了。
.editor 命令
REPL 环境下,按下回车键,就会提交并执行当前的输入。这对输入多行的代码非常不方便,有两个办法可以输入多行代码。一个是按 Shift + 回车键,另一个是使用.editor命令,键入编辑模式。
> .editor
function sum(a, b) {
return a + b;
}
sum(5, 4);
输入.editor命令后,就可以输入多行文本。输入完成后,按下 Ctrl + d 就会执行这些代码;按下 Ctrl + c 就会取消本次输入,回到输入.editor之前的状态。
特殊命令
除了.editor命令以外,REPL 还提供其他一些命令。
- .break:按下 Shift + 回车进入多行文本输入的过程中,输入
.break命令会取消本次输入,相当于按下 Ctrl + c。 - .clear:重置 REPL 上下文为空,并清除当前输入的多行文本。
- .exit:关闭当前的 I/O 读写,退出 REPL 环境。
- .help:显示 REPL 环境的特殊命令列表。
- .save:将当前的 REPL 对换保存成一个文件,比如
.save ./file/to/save.js。 - .load:加载一个文件进入当前的 REPL 对话,比如
.load ./file/to/load.js。
repl 模块
Node 提供repl模块,可以在脚本中唤起 REPL 环境。
// repl.js
const repl = require('repl');
// Our own prompt
repl.start('Code:: ');
上面代码中,repl.start()方法用于唤起 REPL 环境,该方法的参数是自定义的 REPL 环境提示符。执行上面的脚本,就会出现这个提示符。
$ node repl.js
Code::
这个模块允许开发者向 REPL 环境注入变量,因此可以做很多事情。
// repl.js
const repl = require('repl');
const r = repl.start('Code:: ');
r.context.sum = (...args) => args.reduce((a, b) => a + b, 0);
上面代码向 REPL 环境输入了一个sum函数。
$ node repl.js
Code:: sum(1, 2, 3)
6
Code:: .exit
参考链接
- Node.js REPL in Depth, by Seva Zaikov
定时器
process.nextTick()会立即执行回调函数。
微任务队列。
setImmediate(), clearImmediate()
setImmediate(). It can be passed to clearImmediate()
setImmediate()会在下一轮执行回调函数。
定时器在 IO 操作的回调函数之前执行。
- Timer:
setTimeout和setInterval回调函数。 - I/O callbacks: 处理除了
setTimeout、setInterval、setImmediate的回调函数。 - Check: 处理
setImmediate()指定的回调函数。 - nextTickQueue: 处理
process.nextTick()的回调函数,不是 event loop 的一部分。
this 变量
Node 应用的顶层变量是global,对应浏览器的window变量。
顶层的 this
在 REPL 环境,顶层的this就指向global。
> global === this
true
顶层变量是global和this的属性。
> var foo = "bar";
> this.foo
bar
> global.foo
bar
上面代码中,foo是一个顶层变量,自动生成了this.foo和global.foo两个属性。
在模块环境,顶层的this指向当前模块,即module.exports,默认是一个空对象,与global不是同一个对象。
// 模块环境
console.log(this) // {}
console.log(this === global) // false
模块内部的顶层变量,不会自动成为global和this的属性。
// 模块环境
var foo = "bar";
console.log(this.foo); // undefined
console.log(global.foo); // undefined
上面代码中,顶层变量foo并不会生成this.foo和global.foo两个属性。这是因为foo是模块内部的变量,不是全局有效,因此不是global的属性,而this是当前的模块对象,this.foo代表模块实例的属性,这跟变量foo是两回事情。
另外,如果声明变量的时候,不使用var命令,而是直接赋值,那么该变量在 REPL 环境下将成为global和this的属性,在模块环境将只成为 global 的属性。
// REPL 环境
> foo = "bar";
> global.foo
bar
> this.foo
bar
// 模块环境
foo = "bar";
console.log(this.foo); // undefined
console.log(global.foo); // bar
函数内部的 this
直接执行一个函数(不使用new命令),函数内部的this指向global,REPL 环境和模块环境都是如此。
foo = "bar";
function testThis () {
this.foo = "foo";
}
console.log(global.foo); // bar
testThis();
console.log(global.foo); // foo
如果是严格模式,函数内部的this返回undefined。
foo = "bar";
function testThis() {
"use strict";
this.foo = "foo";
}
console.log(this.foo); // "bar"
testThis(); // TypeError: Cannot set property 'foo' of undefined
如果使用new命令调用某个函数,该函数就变成了构造函数,函数内部的this指向新建的实例对象。
概述
npm有两种含义。
首先,npm 是一个网站,用来登记和管理 Node 的模块,网址为npmjs.org。
其次,npm 是一个命令行软件,用来在用户的电脑上安装和管理 Node 模块。
安装
npm不需要单独安装。安装 Node 的时候,会默认一起安装npm。
但是,默认安装的npm可能不是最新版本。在 Node 安装成功后,最好用下面的命令,更新到最新版本。
$ npm install npm@latest -g
然后,运行下面的命令,查看一下 npm 的版本。
$ npm --version
# 等同于
$ npm -v
下面三个命令,也可以用来获取帮助。
# 查看 npm 命令列表
$ npm help
# 查看各个命令的简单用法
$ npm -l
# 查看 npm 的配置
$ npm config list -l
npm init
npm init用来初始化生成一个新的package.json文件。它会向用户提问一系列问题,如果你觉得不用修改默认配置,一路回车就可以了。
如果使用了-f(代表force)、-y(代表yes),则跳过提问阶段,直接生成一个新的package.json文件。
$ npm init -y
npm set
npm set用来设置环境变量。
$ npm set init-author-name 'Your name'
$ npm set init-author-email 'Your email'
$ npm set init-author-url 'http://yourdomain.com'
$ npm set init-license 'MIT'
上面命令等于为npm init设置了默认值,以后执行npm init的时候,package.json的作者姓名、邮件、主页、许可证字段就会自动写入预设的值。这些信息会存放在用户主目录的 ~/.npmrc文件,使得用户不用每个项目都输入。如果某个项目有不同的设置,可以针对该项目运行npm config。
$ npm set save-exact true
上面命令设置加入模块时,package.json将记录模块的确切版本,而不是一个可选的版本范围。
npm config
$ npm config set prefix $dir
上面的命令将指定的$dir目录,设为模块的全局安装目录。如果当前有这个目录的写权限,那么运行npm install的时候,就不再需要sudo命令授权了。
$ npm config set save-prefix '~'
上面的命令使得npm install --save和npm install --save-dev安装新模块时,允许的版本范围从克拉符号(^)改成波浪号(~),即从允许小版本升级,变成只允许补丁包的升级。
$ npm config set init.author.name $name
$ npm config set init.author.email $email
上面命令指定使用npm init时,生成的package.json文件的字段默认值。
npm info
npm info命令可以查看每个模块的具体信息。比如,查看underscore模块的信息。
$ npm info underscore
{ name: 'underscore',
description: 'JavaScript\'s functional programming helper library.',
'dist-tags': { latest: '1.5.2', stable: '1.5.2' },
repository:
{ type: 'git',
url: 'git://github.com/jashkenas/underscore.git' },
homepage: 'http://underscorejs.org',
main: 'underscore.js',
version: '1.5.2',
devDependencies: { phantomjs: '1.9.0-1' },
licenses:
{ type: 'MIT',
url: 'https://raw.github.com/jashkenas/underscore/master/LICENSE' },
files:
[ 'underscore.js',
'underscore-min.js',
'LICENSE' ],
readmeFilename: 'README.md'}
上面命令返回一个JavaScript对象,包含了underscore模块的详细信息。这个对象的每个成员,都可以直接从info命令查询。
$ npm info underscore description
JavaScript's functional programming helper library.
$ npm info underscore homepage
http://underscorejs.org
$ npm info underscore version
1.5.2
npm search
npm search命令用于搜索npm仓库,它后面可以跟字符串,也可以跟正则表达式。
$ npm search <搜索词>
下面是一个例子。
$ npm search node-gyp
// NAME DESCRIPTION
// autogypi Autogypi handles dependencies for node-gyp projects.
// grunt-node-gyp Run node-gyp commands from Grunt.
// gyp-io Temporary solution to let node-gyp run `rebuild` under…
// ...
npm list
npm list命令以树型结构列出当前项目安装的所有模块,以及它们依赖的模块。
$ npm list
加上global参数,会列出全局安装的模块。
$ npm list -global
npm list命令也可以列出单个模块。
$ npm list underscore
npm link
开发NPM模块的时候,有时我们会希望,边开发边试用,比如本地调试的时候,require('myModule')会自动加载本机开发中的模块。Node规定,使用一个模块时,需要将其安装到全局的或项目的node_modules目录之中。对于开发中的模块,解决方法就是在全局的node_modules目录之中,生成一个符号链接,指向模块的本地目录。
npm link就能起到这个作用,会自动建立这个符号链接。
请设想这样一个场景,你开发了一个模块myModule,目录为src/myModule,你自己的项目myProject要用到这个模块,项目目录为src/myProject。首先,在模块目录(src/myModule)下运行npm link命令。
src/myModule$ npm link
上面的命令会在NPM的全局模块目录内,生成一个符号链接文件,该文件的名字就是package.json文件中指定的模块名。
/path/to/global/node_modules/myModule -> src/myModule
这个时候,已经可以全局调用myModule模块了。但是,如果我们要让这个模块安装在项目内,还要进行下面的步骤。
切换到项目目录,再次运行npm link命令,并指定模块名。
src/myProject$ npm link myModule
上面命令等同于生成了本地模块的符号链接。
src/myProject/node_modules/myModule -> /path/to/global/node_modules/myModule
然后,就可以在你的项目中,加载该模块了。
var myModule = require('myModule');
这样一来,myModule的任何变化,都可以直接反映在myProject项目之中。但是,这样也出现了风险,任何在myProject目录中对myModule的修改,都会反映到模块的源码中。
如果你的项目不再需要该模块,可以在项目目录内使用npm unlink命令,删除符号链接。
src/myProject$ npm unlink myModule
npm bin
npm bin命令显示相对于当前目录的,Node模块的可执行脚本所在的目录(即.bin目录)。
# 项目根目录下执行
$ npm bin
./node_modules/.bin
npm adduser
npm adduser用于在npmjs.com注册一个用户。
$ npm adduser
Username: YOUR_USER_NAME
Password: YOUR_PASSWORD
Email: YOUR_EMAIL@domain.com
npm publish
npm publish用于将当前模块发布到npmjs.com。执行之前,需要向npmjs.com申请用户名。
$ npm adduser
如果已经注册过,就使用下面的命令登录。
$ npm login
登录以后,就可以使用npm publish命令发布。
$ npm publish
如果当前模块是一个beta版,比如1.3.1-beta.3,那么发布的时候需要使用tag参数,将其发布到指定标签,默认的发布标签是latest。
$ npm publish --tag beta
如果发布私有模块,模块初始化的时候,需要加上scope参数。只有npm的付费用户才能发布私有模块。
$ npm init --scope=<yourscope>
Scopes相当于npm模块的命名空间,scoped package的包名格式为:
@scope/project-name
包名以@开头,介于@和/之间的为scope。
Scoped Package默认为私有模块,付费用户才能发布,不过发布时可以指定为公开模块,就不需要付费。
$ npm publish --access=public
如果你的模块是用ES6写的,那么发布的时候,最好转成ES5。首先,需要安装Babel。
$ npm install --save-dev babel-cli@6 babel-preset-es2015@6
然后,在package.json里面写入build脚本。
"scripts": {
"build": "babel source --presets babel-preset-es2015 --out-dir distribution",
"prepublish": "npm run build"
}
运行上面的脚本,会将source目录里面的ES6源码文件,转为distribution目录里面的ES5源码文件。然后,在项目根目录下面创建两个文件.npmignore和.gitignore,分别写入以下内容。
// .npmignore
source
// .gitignore
node_modules
distribution
npm deprecate
如果想废弃某个版本的模块,可以使用npm deprecate命令。
$ npm deprecate my-thing@"< 0.2.3" "critical bug fixed in v0.2.3"
运行上面的命令以后,小于0.2.3版本的模块的package.json都会写入一行警告,用户安装这些版本时,这行警告就会在命令行显示。
npm owner
模块的维护者可以发布新版本。npm owner命令用于管理模块的维护者。
# 列出指定模块的维护者
$ npm owner ls <package name>
# 新增维护者
$ npm owner add <user> <package name>
# 删除维护者
$ npm owner rm <user> <package name>
其他命令
npm home,npm repo
npm home命令可以打开一个模块的主页,npm repo命令则是打开一个模块的代码仓库。
$ npm home $package
$ npm repo $package
这两个命令不需要模块先安装。
npm outdated
npm outdated命令检查当前项目所依赖的模块,是否已经有新版本。
$ npm outdated
Package Current Wanted Latest Location
normalize.css 4.0.0 4.2.0 5.0.0 photo-gallery
它会输出当前版本(current version)、应当安装的版本(wanted version)和最新发布的版本(latest version)。
npm prune
npm prune检查当前项目的node_modules目录中,是否有package.json里面没有提到的模块,然后将所有这些模块输出在命令行。
$ npm prune
npm shrinkwrap
npm shrinkwrap的作用是锁定当前项目的以来模块的版本。
$ npm shrinkwrap
运行该命令后,会在当前项目的根目录下生成一个npm-shrinkwrap.json文件,内容是node_modules目录下所有已经安装模块的版本。
下次运行npm install命令时,npm发现当前目录下有npm-shrinkwrap.json文件,就会只安装里面提到的模块,且版本也会保持一致。
命令行参数
--proxy
--proxy参数允许对npm命令设置代理。
$ npm --proxy http://127.0.0.1:8100 install
# or
$ npm config set proxy http://127.0.0.1:8100
$ npm install
上面的命令将通过本机的8100端口代理npm的通信。
如果设置了环境变量HTTP-PROXY或者http-proxy和HTTPS-PROXY或者https-proxy,那么代理设置将以这个环境变量为准。
--registry
--registry参数设置npm与之通信的远程主机,默认是https://registry.npmjs.org/。
$ npm install --registry=https://registry.npm.taobao.org
# or
$ npm config set registry "https://registry.npm.taobao.org"
$ npm install
上面的命令将远程主机设为npm的淘宝镜像。
参考链接
- James Halliday, task automation with npm run: npm run命令(package.json文件的script属性)的用法
- Keith Cirkel, How to Use npm as a Build Tool
- justjs, npm link: developing your own npm modules without tears
- hoodie-css, Development Environment Help
- Stephan Bönnemann, How to make use of npm’s package distribution tags to create release channels
- Alex Booker, How to Build and Publish ES6 npm Modules Today, with Babel
安装
npm install
基本用法
npm install命令用于安装模块。npm i是该命令的别名。
$ npm install lodash
上面命令在当前目录中安装了lodash模块。
默认安装的是最新版本(即latest标签指向的版本),但是你可以 semver 表达式指定安装的版本。
# 等同于 npm install lodash
$ npm install lodash@latest
# 指定确定的版本
$ npm install lodash@4.17.4
# 指定版本范围
$ npm install sax@">=4.15.0 <4.18.0"
# 指定大版本
$ npm install lodash@^4.0.0
上面最后一行命令,指定安装最新的4.x版。
默认情况下,npm install不会修改package.json。--save或-S参数,将模块写入package.json的dependencies字段,--save-dev或-D,将模块加入package.json的devDependencies字段。
# 将模块写入 package.json 的 dependencies 字段
$ npm install lodash --save
$ npm install lodash -S
# 将模块写入 package.json 的 devDependencies 字段
$ npm install lodash --save-dev
$ npm install lodash -D
上面命令将模块写入package.json的时候,如果npm install没有指定版本,npm 会在安装的版本号前面添加^。比如,假定 lodash 的最新版是4.17.4,那么执行npm install -S lodash以后,package.json将写入的版本范围如下。
"dependencies": {
"lodash": "^4.17.4",
...
},
上面的^4.17.4,表示兼容 4.17.4 以后的 4.x 版。
如果不希望出现这种默认行为,可以使用--save-exact指定只将当前确定的版本号,写入package.json。
$ npm install lodash --save --save-exact
# 或者
$ npm install lodash --save-dev --save-exact
执行上面的命令以后,package.json里面的版本号将是固定的。
"dependencies": {
"lodash": "4.17.4",
...
},
注意,--save-exact单独使用是无效的,必须与--save或--save-dev一起使用。
npm install也支持直接输入 Github 代码库地址。
$ npm install git://github.com/package/path.git
$ npm install git://github.com/package/path.git#0.1.0
全局安装
npm 还可以将模块安装在全局,供所有项目使用。注意,一般情况下,全局安装只适用于一些命令行工具。
全局安装,要使用--global或-g参数。此时,加上--save、--save-exact、--save-dev都是无效的。
$ npm install create-react-app --global
# 或者
$ npm install create-react-app -g
上面命令全局安装create-react-app模块。
安装机制与重装
本地安装后,模块将存放在当前项目的node_modules子目录,然后只有在项目目录之中,才能调用这个模块。全局安装后,模块将存放在全局目录之中(通常是/usr/local/lib/),所有项目都可以调用这个模块,但是不应该这样做。
安装之前,npm install会先检查,node_modules目录之中是否已经存在指定模块。如果存在,就不再重新安装了,即使远程仓库已经有了一个新版本,也是如此。
如果你希望,一个模块不管是否安装过,npm 都要强制重新安装,可以使用-f或--force参数。
$ npm install lodash --force
如果所有模块都要强制重新安装,那就删除node_modules目录,重新执行npm install。
$ rm -rf node_modules
$ npm install
安装项目依赖
不使用任何参数时,只使用npm install,会默认安装package.json里面的dependencies字段和devDependencies字段列出的所有模块。
$ npm install
如果使用--production参数,可以只安装dependencies字段的模块。
$ npm install --production
# 等同于
$ NODE_ENV=production npm install
避免安装权限
默认情况下,npm 全局模块都安装在系统目录(比如/usr/local/lib/),普通用户没有写入权限,需要用到sudo命令。这不是很方便,我们可以在没有 root 权限的情况下,安装全局模块。
首先,在主目录下新建配置文件.npmrc,在该文件中将prefix变量定义到一个你的个人目录下面(假定该目录是~/my-npm-modules)。
prefix = /home/yourUsername/my-npm-modules
此后,全局安装的模块都会安装在这个子目录中,npm也会到~/my-npm-modules/bin目录去寻找命令。
最后,将这个路径在.bash_profile文件(或.bashrc文件)中加入PATH变量。
export PATH=~/my-npm-modules/bin:$PATH
npm update
npm update命令可以更新本地安装的模块到最新版本(符合 semver 的设置),如果该模块没有安装,则会安装该模块。npm up和npm upgrade是该命令的缩写。
# 升级当前项目的某个模块
$ npm update lodash
# 升级全局安装的某个模块
$ npm update -g lodash
不使用任何参数时,将更新当前项目的所有dependencies字段里面的模块。如果有模块没有安装,也将一起安装。
$ npm update
--dev参数会连带安装和更新devDependencies字段里面的模块。
$ npm update --dev
更新时,会先到远程仓库查询最新版本,然后查询本地版本。如果本地版本不存在,或者远程版本较新,就会安装最新版本。
使用-S或--save参数,可以在安装的同时,更新package.json里面模块的版本号。
// 更新之前的package.json
dependencies: {
dep1: "^1.1.1"
}
// 更新之后的package.json
dependencies: {
dep1: "^1.2.2"
}
注意,从 npm v2.6.1 开始,npm update只更新顶层模块,而不更新依赖的依赖,以前版本是递归更新的。如果想取到老版本的效果,要使用下面的命令。
$ npm --depth 9999 update
注意,如果已经安装的模块版本比 semver 指定的版本更加新时,npm update有降级效果。
npm uninstall
npm uninstall命令,用来卸载已安装的模块。npm remove、npm rm、npm r、npm un和npm unlink,都是该命令的别名。
# 卸载项目模块
$ npm uninstall lodash
# 卸载全局模块
$ npm uninstall lodash --global
$ npm uninstall lodash -g
使用--save参数(或-S),该模块将会从package.json的dependencies字段中移除。使用--save-dev参数(或-D)时,该模块将会从package.json的devDependencies字段中移除。
$ npm uninstall lodash --save
$ npm uninstall lodash --save-dev
package-lock.json
从 npm 5.0 版本开始,npm 模块默认会锁版本。在npm install命令安装依赖时,会自动生成package-lock.json文件,如果该文件已存在,则会更新该文件。下面是package-lock.json的一个例子。
{
"name": "react-example",
"version": "1.0.0",
"lockfileVersion": 1,
"dependencies": {
"has-flag": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-1.0.0.tgz",
"integrity": "sha1-nZ55MWXOAXoA8AQYxD+UKnsdEfo="
},
"supports-color": {
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-3.2.3.tgz",
"integrity": "sha1-ZawFBLOVQXHYpklGsq48u4pfVPY="
},
"duplexify": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.5.0.tgz",
"integrity": "sha1-GqdzAC4VeEV+nZ1KULDMquvL1gQ=",
"dependencies": {
"end-of-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.0.0.tgz",
"integrity": "sha1-1FlucCc0qT5A6a+GQxnqvZn/Lw4="
}
}
}
}
}
这个文件不仅指定了每个模块的精确版本,而且还指定了node_modules目录的结构(即哪些模块要安装在目录的顶层)。也就是说,哪怕你使用不同版本的 npm,只要有这个文件,最后得到的总是同样的node_modules目录。可以理解成,这个文件是node_modules目录的一个快照。
上面的例子中可以看到,每个目录不仅有精确版本(version字段),还有下载地址(resolved字段)、哈希值(integrity字段)和依赖模块(dependencies字段)。
package-lock.json文件的lockfileVersion字段目前固定为1,以前的npm-shrinkwrap.json文件对应的lockfileVersion为0。这表明,前者实际上是后者的升级版。之所以叫一个新名字,是因为想表明这是一种全新的锁版本设计。
package-lock.json与npm-shrinkwrap.json有一些区别。首先,npm 在任何情况下,都不会将package-lock.json加入发布的代码之中。然后,在一个有package-lock.json的目录之中,执行npm shrinkwrap命令时,npm 会自动将package-lock.json改名为npm-shrinkwrap.json。
如果同一个目录之中,同时存在package-lock.json与npm-shrinkwrap.json两个文件。这时,npm 会忽略package-lock.json,只使用npm-shrinkwrap.json。
npm create 命令
npm create 是 npm init 命令的一个别名。
不过,如果执行npm create xyz,实际上执行的是npx create-xyz。它会暂时性安装create-xyz,然后执行它的package.json里面定义的脚本命令。
下面是一些例子。
npm create svelte@latestnpm create vue@latestnpm init react-app my-app
npm exec 命令
npm exec用来执行某个 npm 模块的内部命令,不管该模块在本地还是在远程。它有一个别名x,即npm exec等同于npm x。
该命令与npx命令的作用类似,但是使用上有所不同。
npx会将所有参数原样传入模块内部。
$ npx foo@latest bar --param=@npmcli/foo
# 等同于
$ foo bar --param=@npmcli/foo
npm exec则需要使用--分隔符,指定所要执行的命令和它的参数。
$ npm exec -- foo@latest bar --param=@npmcli/foo
# 等同于
$ foo bar --param=@npmcli/foo
npm exec也可以用--package参数指定模块。
$ npm exec --package=foo -- bar --bar-argument
# 等同于
$ npx --package=foo bar --bar-argument
--call或-c参数用来指定执行的整个命令。
$ npm exec -c 'eslint && say "hooray, lint passed"'
# 等同于
$ npx -c 'eslint && say "hooray, lint passed"'
npm init 命令
npm init命令的作用,是生成package.json文件。它的别名是create。
新建一个目录,作为模块的开发目录。进入该目录,执行npm init,屏幕上会依次出现一些问题,要求用户回答。用户回答以后,就会生成package.json文件。
$ npm init
如果觉得回答问题太麻烦,想使用package.json的默认值,那就使用--yes或-y参数。
$ npm init --yes
# 或者
$ npm init -y
如果想设置package.json的一些默认值(作者、Email、许可证),需要提前用npm set命令设置。
$ npm set init-author-email "example-user@example.com"
$ npm set init-author-name "example_user"
$ npm set init-license "MIT"
npm init也可以格式化创建项目。
$ npm init foo
# 等同于
$ npm exec create-foo
上面的npm init foo这条命令,会去执行create-foo这个模块(package.json的bin属性)。
举例来说,如果要执行create-react-app创建一个 React 项目,可以执行下面的命令。
$ npm init react-app ./my-react-app
这种用法时,往往使用npm init命令的别名npm create。
$ npm create xyz
// 等同于
$ npm init xyz
// 等同于
$ npx create-xyz
npm link 命令
有时候,我们在本地修改了一些模块,想先测试这些修改是否有效。那么,怎么才能让依赖于该模块的应用,能够加载这些本地模块呢?
一种方法是使用npm install的--save参数。
$ npm install --no-save <模块的本地路径h>
上面的命令会从本地目录安装指定模块,但是不写入package.json。这样就可以让应用加载本地模块。
另一种则是使用npm link命令,在node_modules目录里面建立一个符号链接,链接到本地模块。
它分成两步,第一步先在本地模块的目录里,执行npm link。
$ npm link
第二步是到你的应用目录,执行npm link <本地模块名>,在node_modules目录里面产生本地模块的符号链接。
$ npm link <本地模块名>
也可以将上面两步合二为一,在应用目录里面,直接链接本地模块的路径。
$ npm link <本地模块的路径>
等到测试完毕,再用npm unlink命令,先在应用目录删除符号链接。
$ npm unlink --no-save <本地模块名>
再到本地模块的目录里面,执行npm unlink。
$ npm unlink
npm tag
npm 允许为版本添加标签,方便用户安装特定版本。只要指定标签,就可以安装该标签下的最新版本。
发布模块时,如果不指定 tag,默认使用latest。安装时也是如此,不指定版本时,npm 默认安装latest标签对应的最新版本。
如果指定 tag,发布时会将该 tag 指向最新发布的版本。用户如果想安装该版本,需要在安装时指定 tag。
$ npm publish --tag beta
上面示例中,新发布版本的标签是beta。
下面的命令为已发布的版本指定标签。注意,这一步之前不需要先移除已经存在的标签。
# 语法
$ npm dist-tag add [PackageName]@[Version] [Tag]
# 例子
$ npm dist-tag add foo-package@0.0.0 latest
安装时指定标签。
$ npm install <package>@<tag>
# 或者
$ npm install --tag <tag>
下面的命令查看所有 tag 和对应的版本。
$ npm dist-tag ls
# 或者
$ npm view [模块名] dist-tags
删除已经发布的标签。
$ npm dist-tag rm [PackageName] [Tag]
npm token
npm token命令用来管理认证令牌。
npm token list命令列出所有激活的认证令牌。
$ npm token list
上面命令的返回结果,以表格形式显示。如果加上--json参数,则返回 JSON 格式;加上--parseable参数,返回 Tab 键分隔的数据。
npm token create命令生成一个新的认证令牌。
$ npm token create
--read-only参数用来生成只读令牌,默认是读写令牌。--cidr=<cidr-ranges>参数用来指定令牌生效的 IP 地址范围。
npm token revoke命令收回令牌。
$ npm token revoke <token|id>
npm update
更新所有依赖项。
$ npm update
更新单个依赖。
$ npm update xml2js
同时更新 package.json 里面每个依赖的版本号。
$ npm update --save
npm version
npm version用来指定模块的版本,然后会将新的版本号写入package.json和package-lock.json。
它的命令行用法如下。
npm version [
<newversion> |
major | minor | patch | premajor | preminor | prepatch | prerelease |
from-git
]
上面可以归纳为三种用法。
(1)<newversion>:自己指定版本号。
(2)七个版本关键字:patch,minor,major,prepatch,preminor,premajor,prerelease。这时原有版本号,会在相应的位置增加1。
major:规则如下。
(1)如果没有预发布号,则增加主版本号,并将次版本号和预发布号设为0。
# 版本号从 3.1.0 变为 4.0.0
$ npm version major
(2)如果有预发布号,且次版本号和补丁号都为0,则不升级主版本号,只去掉预发布号。
# 版本号从 4.0.0 变为 5.0.0-0
$ npm version premajor
# 版本号从 5.0.0-0 变为 5.0.0
$ npm version major
(3)如果有预发布号,且次版本号和补丁号都不为0,则增加主版本号,将次版本号和补丁号都置为0,并去掉预发布号。
# 版本号从 5.0.0-0 变为 5.1.0-0
$ npm version preminor : 5.0.0-0–> 5.1.0-0
# 版本号从 5.1.0-0 变为 6.0.0
$ npm version major
minor:规则如下。
(1)如果没有预发布号,则增加次版本号,并将补丁号设为0。
# 版本号从 2.0.1 变为 2.1.0
$ npm version minor
(2)如果有预发布号,且补丁号为0,则去掉预发布号,其他不变。
# 版本号从 2.1.0 变为 3.0.0-0
$ npm version premajor
# 版本号从 3.0.0-0 变为 3.0.0
$ npm version minor
(3)如果有预发布号,且补丁号不为0,则去掉预发布号,增加次版本号,补丁号设为0。
# 版本号从 3.0.0 变为 3.0.1-0
$ npm version prepatch
# 版本号从 3.0.1-0 变为 3.1.0
$ npm version minor
patch:如果预发布号,则去掉预发布后,其他保持不变;如果没有预发布号,则升级补丁号。
# 版本号从 2.0.0-0 变为 2.0.0
$ npm version patch
# 版本号从 2.0.0 变为 2.0.1
$ npm version patch
premajor:增加主版本号,将次版本号和补丁号都设为0,增加预发布号为0。
# 版本号从 1.1.0-0 变为 2.0.0-0
$ npm version premajor
preminor:增加次版本号,补丁号设为0,预发布号设为0。
# 版本号从 1.0.2-0 变为 1.1.0-0
$ npm version preminor
prepatch:增加补丁号,同时预发布号设为0。
# 版本号从 1.0.1-1 变为 1.0.2-0
$ npm version prepatch
prerelease:如果没有预发布号,则增加补丁号,同时预发布号设为0;如果有预发布号,则预发布号增加1。
# 版本号从 1.0.0 变为 1.0.1-0
$ npm version prerelease
# 版本号从 1.0.1-0 变为 1.0.1-1
$ npm version prerelease
(3)from-git:使用最新的 Git 标签,将其作为 npm 版本。
这个命令如果是在一个 Git 仓库里面运行,它会创造一个新的提交和标签。如果不希望生成标签,可以使用命令行参数--no-git-tag-version。
命令行参数-m或者--message可以指定提交信息。提交信息里面的%s参数会被替换成新的版本号。
$ npm version patch -m "Upgrade to %s for reasons"
--pre-id
npm version命令的--pre-id参数,可以指定预发布号的前缀。
$ npm version prerelease --pre-id rc
上面的命令会产生诸如1.0.0-rc.0的版本号。
参考链接
npm view 命令
查看某个模块发布过的所有版本。
$ npm view [PackageName] versions
npm whoami
npm whoami命令返回当前登录的 npm 用户名。
$ npm whoami
npx 使用教程
npm 从5.2版开始,增加了 npx 命令,它有很多用处。

Node 自带 npx 模块,所以可以直接使用 npx 命令。万一不能用,就要手动安装一下。
$ npm install -g npx
调用项目安装的模块
npx 想要解决的主要问题,就是调用项目内部安装的模块。比如,项目内部安装了测试工具 Mocha。
$ npm install -D mocha
一般来说,调用 Mocha ,只能在项目脚本和 package.json 的scripts字段里面, 如果想在命令行下调用,必须像下面这样。
# 项目的根目录下执行
$ node-modules/.bin/mocha --version
npx 就是想解决这个问题,让项目内部安装的模块用起来更方便,只要像下面这样调用就行了。
$ npx mocha --version
npx 的原理很简单,就是运行的时候,会到node_modules/.bin路径和环境变量$PATH里面,检查命令是否存在。
由于 npx 会检查环境变量$PATH,所以系统命令也可以调用。
# 等同于 ls
$ npx ls
注意,Bash 内置的命令不在$PATH里面,所以不能用。比如,cd是 Bash 命令,因此就不能用npx cd。
避免全局安装模块
除了调用项目内部模块,npx 还能避免全局安装的模块。比如,create-react-app这个模块是全局安装,npx 可以运行它,而且不进行全局安装。
$ npx create-react-app my-react-app
上面代码运行时,npx 将create-react-app下载到一个临时目录,然后自动执行package.json文件的bin字段指定的命令,执行以后再删除整个模块。所以,以后再次执行上面的命令,会重新下载create-react-app。
下载全局模块时,npx 允许指定版本。
$ npx uglify-js@3.1.0 main.js -o ./dist/main.js
上面代码指定使用 3.1.0 版本的uglify-js压缩脚本。
注意,只要 npx 后面的模块无法在本地发现,就会下载同名模块。比如,本地没有安装http-server模块,下面的命令会自动下载该模块,在当前目录启动一个 Web 服务。
$ npx http-server
npx 也可以执行package.json文件的scripts字段的命令,比如npx foo dev会执行foo模块的scripts字段的dev命令。
--no-install 参数和--ignore-existing 参数
如果想让 npx 强制使用本地模块,不下载远程模块,可以使用--no-install参数。如果本地不存在该模块,就会报错。
$ npx --no-install http-server
反过来,如果忽略本地的同名模块,强制安装使用远程模块,可以使用--ignore-existing参数。比如,本地已经全局安装了create-react-app,但还是想使用远程模块,就用这个参数。
$ npx --ignore-existing create-react-app my-react-app
使用不同版本的 node
利用 npx 可以下载模块这个特点,可以指定某个版本的 Node 运行脚本。它的窍门就是使用 npm 的 node 模块。
$ npx node@0.12.8 -v
v0.12.8
上面命令会使用 0.12.8 版本的 Node 执行脚本。原理是从 npm 下载这个版本的 node,使用后再删掉。
某些场景下,这个方法用来切换 Node 版本,要比 nvm 那样的版本管理器方便一些。
-p 参数
-p参数用于指定 npx 所要安装的模块,所以上一节的命令可以写成下面这样。
$ npx -p node@0.12.8 node -v
v0.12.8
上面命令先指定安装node@0.12.8,然后再执行node -v命令。
-p参数对于需要安装多个模块的场景很有用。
$ npx -p lolcatjs -p cowsay [command]
-c 参数
如果 npx 安装多个模块,默认情况下,所执行的命令之中,只有第一个可执行项会使用 npx 安装的模块,后面的可执行项还是会交给 Shell 解释。
$ npx -p lolcatjs -p cowsay 'cowsay hello | lolcatjs'
# 报错
上面代码中,cowsay hello | lolcatjs执行时会报错,原因是第一项cowsay由 npx 解释,而第二项命令localcatjs由 Shell 解释,但是lolcatjs并没有全局安装,所以报错。
-c参数可以将所有命令都用 npx 解释。有了它,下面代码就可以正常执行了。
$ npx -p lolcatjs -p cowsay -c 'cowsay hello | lolcatjs'
-c参数的另一个作用,是将环境变量带入所要执行的命令。举例来说,npm 提供当前项目的一些环境变量,可以用下面的命令查看。
$ npm run env | grep npm_
-c参数可以把这些 npm 的环境变量带入 npx 命令。
$ npx -c 'echo "$npm_package_name"'
上面代码会输出当前项目的项目名。
执行 GitHub 源码
npx 还可以执行 GitHub 上面的模块源码。
# 执行 Gist 代码
$ npx https://gist.github.com/zkat/4bc19503fe9e9309e2bfaa2c58074d32
# 执行仓库代码
$ npx github:piuccio/cowsay hello
注意,远程代码必须是一个模块,即必须包含package.json和入口脚本。
参考链接
package.json
简介
npm 模块就是一个包含 package.json 文件的目录。
npm 模块分成 CommonJS 和 ES 两种格式。以下情况会以 ES 格式处理模块。
- 脚本后缀名为
.mjs。 - 脚本后缀名为
.js,并且 package.json 文件的type字段的值为module。 node运行时带有--input-type=module,这时通过--eval或--print参数传入的代码字符串会被当作 ES 格式处理。- 脚本包含一些 ES 才有的语法特征,比如
import语句、export语句、import.meta等。
以下情况会以 CommonJS 格式处理模块。
- 脚本后缀名为
.cjs。 - 脚本后缀名为
.js,并且 package.json 文件的type字段的值为commonjs。 node运行时带有--input-type=commonjs,这时通过--eval或--print参数传入的代码字符串会被当作 CommonJS 格式处理。
其他情况下,Node.js 目前会以 CommonJS 格式处理模块,不过以后可能会改成 ES 格式。
files 字段
files字段是一个数组,里面指定了一组文件。当模块发布到 NPM 网站时,这组文件会被包括。这个字段是可选的,如果没有指定内容,那么发布时所有文件都会被包括在内。如果files字段包含目录名,该目录里面的所有文件都会被计入。
{
"name": "@adam_baldwin/wombats",
"version": "1.0.0",
"description": "",
"main": "index.js",
"files": [
"index.js"
],
...
}
npm不会发布.gitignore里面列出的文件和目录。项目的根目录或子目录里面,还可以放置一个.npmignore文件,该文件会覆盖.gitignore,里面指定的文件和目录不会被发布。
项目的根目录下,files字段优先级最高;子目录下,.npmignore优先。files字段指定的文件,不会被.npmignore或.gitignore排除。
以下文件,发布的时候总是会包含。
- package.json
- README
- CHANGES / CHANGELOG / HISTORY
- LICENSE / LICENCE
- NOTICE
main字段里面的文件
README、CHANGES、LICENSE和NOTICE这四个文件名,可以采取任意的大小写组合。
以下文件,发布的时候总是会被排除。
- .git
- CVS
- .svn
- .hg
- .lock-wscript
- .wafpickle-N
.*.swp- .DS_Store
._*- npm-debug.log
- .npmrc
- node_modules
- config.gypi
*.orig- package-lock.json
基本上,npm 会发布files字段指定的文件和目录,以及那些总是会包含在内的文件(比如package.json),然后再除去那些被其他规则排除的文件和目录。
npm-packlist 模块会列出所有将要打包发布的文件和模块。npm pack命令则会将那些将要发布的内容打成一个tgz压缩包,放在项目的根目录下。
字段
main
main字段指定模块的入口文件。比如,当前模块叫做foo,那么require('foo')时,加载哪个文件,由该字段指定。
main字段的值应该是一个相对路径,相对于模块的根目录进行计算。
如果没有指定main字段,那么默认的入口文件是模块根目录的index.js。
exports
exports字段也是指定模块的入口文件,它比main字段更强大。一般来说,优先使用exports字段,只有对 Node.js 10 和更低的版本,才推荐使用main字段。如果同时定义了exports和main两个字段,前者的优先级更高。
另外,exports字段只针对模块的消费者,并不在模块内部使用。
exports字段可以指定模块的唯一入口。
{
"exports": "./index.js"
}
上面字段中,exports指定了唯一入口,这意味着只能从模块名加载(require('pkg')),而不能从子路径加载,比如require('pkg/subpath.js')会报错。
为了保证模块的兼容性,常常同时定义main和exports。
{
"main": "./index.js",
"exports": "./index.js"
}
如果当前模块有多个入口,可以在exports字段里面一一指定。
{
"exports": {
".": "./index.js",
"./submodule.js": "./src/submodule.js"
}
}
上面示例中,模块有两个入口,.表示通过模块名加载,./submodule.js表示通过模块的子路径加载,写成import submodule from 'es-module-package/submodule.js';。
这时,exports字段的值是一个对象。前面,exports的值为字符串,其实是简写形式。
{
"exports": "./index.js"
}
// 等同于
{
"exports": {
".": "./index.js"
}
}
一般来说,对于同一个入口,模块作者最好同时指定有.js后缀名和没有.js后缀名两种情况。
{
"name": "my-package",
"exports": {
".": "./lib/index.js",
"./lib": "./lib/index.js",
"./lib/index": "./lib/index.js",
"./lib/index.js": "./lib/index.js",
"./feature": "./feature/index.js",
"./feature/index": "./feature/index.js",
"./feature/index.js": "./feature/index.js",
"./package.json": "./package.json"
}
}
上面示例中,就同时指定了./lib/index和./lib/index.js两种入口形式。
exports字段也可以使用通配符。
{
"name": "my-package",
"exports": {
".": "./lib/index.js",
"./lib": "./lib/index.js",
"./lib/*": "./lib/*.js",
"./lib/*.js": "./lib/*.js",
"./feature": "./feature/index.js",
"./feature/*": "./feature/*.js",
"./feature/*.js": "./feature/*.js",
"./package.json": "./package.json"
}
}
上面示例中,exports字段里面使用了通配符,指定了lib目录和feature目录下的所有入口对应的脚本文件。注意,入口包括.js后缀名和没有后缀名两种情况。另外,通配符*包括了多层目录的情况。
由于exports允许逐一指定不同入口对应的脚本文件,如果以后修改了脚本名称,可以只修改exports字段,不改变入口。另外,exports也排除某些内部脚本被加载。
{
"name": "my-package",
"exports": {
".": "./lib/index.js",
"./feature/*.js": "./feature/*.js",
"./feature/internal/*": null
}
}
上面示例中,exports字段共有三个映射,其中第二个映射是指定加载featrue子目录下的脚本,而第三个映射则是排除feature目录的子目录internal/*下的脚本。
exports还支持条件加载,即不同的情况加载不同的脚本。
{
"exports": {
"import": "./index-module.js",
"require": "./index-require.cjs"
},
"type": "module"
}
上面示例中,import指定 ES 格式加载时,入口文件为./index-module.js,CommonJS 格式加载时(require命令),入口文件为./index-require.cjs。
exports支持以下的条件关键词。
- node-addons:Node.js 的扩展
- node:Node.js 环境
- import:ES 格式加载时,特指使用
import或import()加载时。 - require:CommonJS 格式加载时。
- default:其他条件都不匹配时的默认入口文件,总是放在最后。
下面是一个例子。
{
"exports": {
".": "./index.js",
"./feature.js": {
"node": "./feature-node.js",
"default": "./feature.js"
}
}
}
exports还支持嵌套条件。
{
"exports": {
"node": {
"import": "./feature-node.mjs",
"require": "./feature-node.cjs"
},
"default": "./feature.mjs"
}
}
注意,exports的映射不允许指向外部模块。
imports
imports字段用于模块的内部,为入口文件指定别名。它的路径总是从#开始,避免与实际路径相混淆。
{
"imports": {
"#dep": {
"node": "dep-node-native",
"default": "./dep-polyfill.js"
}
},
"dependencies": {
"dep-node-native": "^1.0.0"
}
}
上面示例中,imports字段的意思是,加载#dep这个别名时(import dep from "#def";),对于 Node.js 加载外部模块dep-node-native,对于构建工具则会加载本地文件./dep-polyfill.js。
发布
发布标签
npm 支持为版本打上标签,这叫做发布标签(dist-tag)。如果不指定发布标签,默认就是latest。用户下载模块时,默认安装的就是latest标签指向的版本。
新发布的版本,如果不希望用户默认安装,就需要自己指定标签。举例来说,某个模块的最新版本是4.6.12,但是有些用户还在使用老版本3.2.13。现在,你修正了一些老版本的 bug,发了一个新版本3.2.14。如果不指定发布标签,3.2.14的发布标签就是latest,因为它是最新发布的。
这导致的后果就是,用户执行下面的命令,进行默认安装时,会出现非预期的结果。
$ npm install <package>
执行上面命令时,用户会默认安装3.2.14,而不是4.6.12。因为latest标签指向3.2.14。
解决方法就是,发布3.2.14的时候,为它打上一个发布标签。这样,3.2.14就不会占用latest标签。
$ npm publish --tag=previous
执行上面的命令后,3.2.14的发布标签就是previous。
安装时,必须指定这个标签,才能安装到3.2.14。
$ npm install <package>@previous
# 或者
$ npm install <package> --tag previous
上面的命令的两种语法都可以指定标签名。由于latest是默认标签,所以可以省略。
$ npm install <package>
# 等同于
$ npm install <package>@latest
一种常见的做法是,发布下一个大版本时,指定它的发布标签为next。
# 发布
$ npm publish --tag=next
# 安装
$ npm publish <package>@next
这样的话,用户默认安装的还是主流版本,但是愿意尝鲜的用户,可以使用新版本。
等到新版本足够可靠以后,再把latest标签指定到新版本。
$ npm dist-tag add <package>@5.0.1 latest
如果希望把默认的发布标签改掉,不再是latest,可以写在package.json里面。
{
…
"publishConfig": {
"tag": "next"
}
}
上面的设置,可以使得发布新版本时,发布标签默认为next。
常用的发布标签有stable、beta、dev等等。
npm dist-tag 命令
npm dist-tag命令用来管理发布标签。
npm dist-tag ls用来列出所有的发布标签。如果不指定模块名,那么默认为当前模块。
$ npm dist-tag ls [<pkg>]
npm dist-tag add用来为一个版本指定发布标签。
$ npm dist-tag add <pkg>@<version> [<tag>]
npm dist-tag rm用来移除一个发布标签。
$ npm dist-tag rm <pkg> <tag>
.gitignore,.npmignore
如果当前项目的根目录下有.gitignore文件,该文件里面的路径不会打包进入 npm 模块。
如果有.npmignore文件,那么 npm 将忽略.gitignore文件,不将.npmignore文件里面的路径打包进入 npm 模块。如果有些文件不希望进入 npm 模块(比如测试用例),但是希望进入 Git 仓库,那么可以使用.npmignore。
# .gitignore
node_modules
# .npmignore
node_modules
tests
此外,package.json的files字段,也可以用来排除进入 npm 模块的文件。
{
"files": [
"index.js"
]
}
npm 会最优先排除files字段里面的文件。另外,无论如何设置,package.json文件都会进入 npm 模块。
npm pack 命令
npm pack命令用来打包当前项目,打包后的文件会在当前目录下生成,文件名为<name>-<version>.tgz。
$ npm pack
如果多次运行该命令,每次生成的包将覆盖前一次的包。
npm pack可以接受路径作为参数,打包该路径下的模块。如果没有提供任何参数,将打包当前目录。
$ npm pack foo/bar
--dry-run参数会输出打包的内容,而不生成打包文件。
$ npm pack --dry-run
参考链接
- One simple trick for JavaScript package maintainers, by Stephan Bönnemann
脚本功能
npm run
npm不仅可以用于模块管理,还可以用于执行脚本。package.json文件有一个scripts字段,可以用于指定脚本命令,供npm直接调用。
{
"name": "myproject",
"devDependencies": {
"jshint": "latest",
"browserify": "latest",
"mocha": "latest"
},
"scripts": {
"lint": "jshint **.js",
"test": "mocha test/"
}
}
上面代码中,scripts字段指定了两项命令lint和test。命令行输入npm run-script lint或者npm run lint,就会执行jshint **.js,输入npm run-script test或者npm run test,就会执行mocha test/。npm run是npm run-script的缩写,一般都使用前者,但是后者可以更好地反应这个命令的本质。
npm run命令会自动在环境变量$PATH添加node_modules/.bin目录,所以scripts字段里面调用命令时不用加上路径,这就避免了全局安装NPM模块。
npm run如果不加任何参数,直接运行,会列出package.json里面所有可以执行的脚本命令。
npm内置了两个命令简写,npm test等同于执行npm run test,npm start等同于执行npm run start。
npm run会创建一个Shell,执行指定的命令,并临时将node_modules/.bin加入PATH变量,这意味着本地模块可以直接运行。
举例来说,你执行ESLint的安装命令。
$ npm i eslint --save-dev
运行上面的命令以后,会产生两个结果。首先,ESLint被安装到当前目录的node_modules子目录;其次,node_modules/.bin目录会生成一个符号链接node_modules/.bin/eslint,指向ESLint模块的可执行脚本。
然后,你就可以在package.json的script属性里面,不带路径的引用eslint这个脚本。
{
"name": "Test Project",
"devDependencies": {
"eslint": "^1.10.3"
},
"scripts": {
"lint": "eslint ."
}
}
等到运行npm run lint的时候,它会自动执行./node_modules/.bin/eslint .。
如果直接运行npm run不给出任何参数,就会列出scripts属性下所有命令。
$ npm run
Available scripts in the user-service package:
lint
jshint **.js
test
mocha test/
下面是另一个package.json文件的例子。
"scripts": {
"watch": "watchify client/main.js -o public/app.js -v",
"build": "browserify client/main.js -o public/app.js",
"start": "npm run watch & nodemon server.js",
"test": "node test/all.js"
},
上面代码在scripts项,定义了四个别名,每个别名都有对应的脚本命令。
$ npm run watch
$ npm run build
$ npm run start
$ npm run test
其中,start和test属于特殊命令,可以省略run。
$ npm start
$ npm test
如果希望一个操作的输出,是另一个操作的输入,可以借用Linux系统的管道命令,将两个操作连在一起。
"build-js": "browserify browser/main.js | uglifyjs -mc > static/bundle.js"
但是,更方便的写法是引用其他npm run命令。
"build": "npm run build-js && npm run build-css"
上面的写法是先运行npm run build-js,然后再运行npm run build-css,两个命令中间用&&连接。如果希望两个命令同时平行执行,它们中间可以用&连接。
下面是一个流操作的例子。
"devDependencies": {
"autoprefixer": "latest",
"cssmin": "latest"
},
"scripts": {
"build:css": "autoprefixer -b 'last 2 versions' < assets/styles/main.css | cssmin > dist/main.css"
}
写在scripts属性中的命令,也可以在node_modules/.bin目录中直接写成bash脚本。下面是一个bash脚本。
#!/bin/bash
cd site/main
browserify browser/main.js | uglifyjs -mc > static/bundle.js
假定上面的脚本文件名为build.sh,并且权限为可执行,就可以在scripts属性中引用该文件。
"build-js": "bin/build.sh"
参数
npm run命令还可以添加参数。
"scripts": {
"test": "mocha test/"
}
上面代码指定npm test,实际运行mocha test/。如果要通过npm test命令,将参数传到mocha,则参数之前要加上两个连词线。
$ npm run test -- anothertest.js
# 等同于
$ mocha test/ anothertest.js
上面命令表示,mocha要运行所有test子目录的测试脚本,以及另外一个测试脚本anothertest.js。
npm run本身有一个参数-s,表示关闭npm本身的输出,只输出脚本产生的结果。
// 输出npm命令头
$ npm run test
// 不输出npm命令头
$ npm run -s test
scripts脚本命令最佳实践
scripts字段的脚本命令,有一些最佳实践,可以方便开发。首先,安装npm-run-all模块。
$ npm install npm-run-all --save-dev
这个模块用于运行多个scripts脚本命令。
# 继发执行
$ npm-run-all build:html build:js
# 等同于
$ npm run build:html && npm run build:js
# 并行执行
$ npm-run-all --parallel watch:html watch:js
# 等同于
$ npm run watch:html & npm run watch:js
# 混合执行
$ npm-run-all clean lint --parallel watch:html watch:js
# 等同于
$ npm-run-all clean lint
$ npm-run-all --parallel watch:html watch:js
# 通配符
$ npm-run-all --parallel watch:*
(1)start脚本命令
start脚本命令,用于启动应用程序。
"start": "npm-run-all --parallel dev serve"
上面命令并行执行dev脚本命令和serve脚本命令,等同于下面的形式。
$ npm run dev & npm run serve
如果start脚本没有配置,npm start命令默认执行下面的脚本,前提是模块的根目录存在一个server.js文件。
$ node server.js
(2)dev脚本命令
dev脚本命令,规定开发阶段所要做的处理,比如构建网页资源。
"dev": "npm-run-all dev:*"
上面命令用于继发执行所有dev的子命令。
"predev:sass": "node-sass --source-map src/css/hoodie.css.map --output-style nested src/sass/base.scss src/css/hoodie.css"
上面命令将sass文件编译为css文件,并生成source map文件。
"dev:sass": "node-sass --source-map src/css/hoodie.css.map --watch --output-style nested src/sass/base.scss src/css/hoodie.css"
上面命令会监视sass文件的变动,只要有变动,就自动将其编译为css文件。
"dev:autoprefix": "postcss --use autoprefixer --autoprefixer.browsers \"> 5%\" --output src/css/hoodie.css src/css/hoodie.css"
上面命令为css文件加上浏览器前缀,限制条件是只考虑市场份额大于5%的浏览器。
(3)serve脚本命令
serve脚本命令用于启动服务。
"serve": "live-server dist/ --port=9090"
上面命令启动服务,用的是live-server模块,将服务启动在9090端口,展示dist子目录。
live-server模块有三个功能。
- 启动一个HTTP服务器,展示指定目录的
index.html文件,通过该文件加载各种网络资源,这是file://协议做不到的。 - 添加自动刷新功能。只要指定目录之中,文件有任何变化,它就会刷新页面。
npm run serve命令执行以后,自动打开浏览器。、
以前,上面三个功能需要三个模块来完成:http-server、live-reload和opener,现在只要live-server一个模块就够了。
(4)test脚本命令
test脚本命令用于执行测试。
"test": "npm-run-all test:*",
"test:lint": "sass-lint --verbose --config .sass-lint.yml src/sass/*"
上面命令规定,执行测试时,运行lint脚本,检查脚本之中的语法错误。
(5)prod脚本命令
prod脚本命令,规定进入生产环境时需要做的处理。
"prod": "npm-run-all prod:*",
"prod:sass": "node-sass --output-style compressed src/sass/base.scss src/css/prod/hoodie.min.css",
"prod:autoprefix": "postcss --use autoprefixer --autoprefixer.browsers "> 5%" --output src/css/prod/hoodie.min.css src/css/prod/hoodie.min.css"
上面命令将sass文件转为css文件,并加上浏览器前缀。
(6)help脚本命令
help脚本命令用于展示帮助信息。
"help": "markdown-chalk --input DEVELOPMENT.md"
上面命令之中,markdown-chalk模块用于将指定的markdown文件,转为彩色文本显示在终端之中。
(7)docs脚本命令
docs脚本命令用于生成文档。
"docs": "kss-node --source src/sass --homepage ../../styleguide.md"
上面命令使用kss-node模块,提供源码的注释生成markdown格式的文档。
pre- 和 post- 脚本
npm run为每条命令提供了pre-和post-两个钩子(hook)。以npm run lint为例,执行这条命令之前,npm会先查看有没有定义prelint和postlint两个钩子,如果有的话,就会先执行npm run prelint,然后执行npm run lint,最后执行npm run postlint。
{
"name": "myproject",
"devDependencies": {
"eslint": "latest"
"karma": "latest"
},
"scripts": {
"lint": "eslint --cache --ext .js --ext .jsx src",
"test": "karma start --log-leve=error karma.config.js --single-run=true",
"pretest": "npm run lint",
"posttest": "echo 'Finished running tests'"
}
}
上面代码是一个package.json文件的例子。如果执行npm test,会按下面的顺序执行相应的命令。
pretesttestposttest
如果执行过程出错,就不会执行排在后面的脚本,即如果prelint脚本执行出错,就不会接着执行lint和postlint脚本。
下面是一个例子。
{
"test": "karma start",
"test:lint": "eslint . --ext .js --ext .jsx",
"pretest": "npm run test:lint"
}
上面代码中,在运行npm run test之前,会自动检查代码,即运行npm run test:lint命令。
下面是一些常见的pre-和post-脚本。
prepublish:发布一个模块前执行。postpublish:发布一个模块后执行。preinstall:用户执行npm install命令时,先执行该脚本。postinstall:用户执行npm install命令时,安装结束后执行该脚本,通常用于将下载的源码编译成用户需要的格式,比如有些模块需要在用户机器上跟本地的C++模块一起编译。preuninstall:卸载一个模块前执行。postuninstall:卸载一个模块后执行。preversion:更改模块版本前执行。postversion:更改模块版本后执行。pretest:运行npm test命令前执行。posttest:运行npm test命令后执行。prestop:运行npm stop命令前执行。poststop:运行npm stop命令后执行。prestart:运行npm start命令前执行。poststart:运行npm start命令后执行。prerestart:运行npm restart命令前执行。postrestart:运行npm restart命令后执行。
对于最后一个npm restart命令,如果没有设置restart脚本,prerestart和postrestart会依次执行stop和start脚本。
另外,不能在pre脚本之前再加pre,即prepretest脚本不起作用。
注意,即使Npm可以自动运行pre和post脚本,也可以手动执行它们。
$ npm run prepublish
下面是post install的例子。
{
"postinstall": "node lib/post_install.js"
}
上面的这个命令,主要用于处理从Git仓库拉下来的源码。比如,有些源码是用TypeScript写的,可能需要转换一下。
下面是publish钩子的一个例子。
{
"dist:modules": "babel ./src --out-dir ./dist-modules",
"gh-pages": "webpack",
"gh-pages:deploy": "gh-pages -d gh-pages",
"prepublish": "npm run dist:modules",
"postpublish": "npm run gh-pages && npm run gh-pages:deploy"
}
上面命令在运行npm run publish时,会先执行Babel编译,然后调用Webpack构建,最后发到Github Pages上面。
以上都是npm相关操作的钩子,如果安装某些模块,还能支持Git相关的钩子。下面以husky模块为例。
$ npm install husky --save-dev
安装以后,就能在package.json添加precommit、prepush等钩子。
{
"scripts": {
"lint": "eslint yourJsFiles.js",
"precommit": "npm run test && npm run lint",
"prepush": "npm run test && npm run lint",
"...": "..."
}
}
类似作用的模块还有pre-commit、precommit-hook等。
内部变量
scripts字段可以使用一些内部变量,主要是package.json的各种字段。
比如,package.json的内容是{"name":"foo", "version":"1.2.5"},那么变量npm_package_name的值是foo,变量npm_package_version的值是1.2.5。
{
"scripts":{
"bundle": "mkdir -p build/$npm_package_version/"
}
}
运行npm run bundle以后,将会生成build/1.2.5/子目录。
config字段也可以用于设置内部字段。
"name": "fooproject",
"config": {
"reporter": "xunit"
},
"scripts": {
"test": "mocha test/ --reporter $npm_package_config_reporter"
}
上面代码中,变量npm_package_config_reporter对应的就是reporter。
通配符
npm 的通配符的规则如下。
*匹配0个或多个字符?匹配1个字符[...]匹配某个范围的字符。如果该范围的第一个字符是!或^,则匹配不在该范围的字符。!(pattern|pattern|pattern)匹配任何不符合给定的模式?(pattern|pattern|pattern)匹配0个或1个给定的模式+(pattern|pattern|pattern)匹配1个或多个给定的模式*(a|b|c)匹配0个或多个给定的模式@(pattern|pat*|pat?erN)只匹配给定模式之一**如果出现在路径部分,表示0个或多个子目录。
Yarn 的用法
简介
Yarn 是 Facebook 联合其他大公司推出的模块管理器。相比 npm,它有两个显著特点。
(1)安装速度较快。
Yarn 采用平行安装模式,而 npm 采用的是线性模式,只有前一个模块安装完,才会安装下一个。
(2)默认开启“版本锁定”功能
Yarn 希望安装依赖时,所有依赖的版本在不同机器都保持相同。为了达到这个目的,第一次安装依赖时,它默认生成一个锁定文件yarn.lock,将这个文件放到代码库之中,下次安装时就能保证,总是安装相同版本的依赖。这与npm shrinkwrap命令生成的npm-shrinkwrap.json的作用相似,只不过 Yarn 默认就可以生成这个文件。
全局参数 global
如果要全局执行一个命令,必须加上global参数。目前,add、bin、ls和remove四个命令,支持global参数。
$ yarn global add create-react-app --prefix /usr/local
yarn install
yarn install命令用于安装一个模块。如果yarn.lock文件存在,会优先读取该文件,按照该文件指定的版本安装。
使用--production参数或环境变量NODE_ENV等于production,将不会安装devDependencies字段指定的模块。
$ yarn install --production
如果使用--no-lockfile参数,yarn install将不会读取或生成yarn.lock。
$ yarn install --no-lockfile
yarn add
yarn add命令允许新增安装一个模块。它默认会将该模块加入package.json文件的dependencies字段。如果想加入devDependencies字段,要使用--dev参数。
$ yarn add package-name
$ yarn add package-name@1.2.3
$ yarn add package-name@tag
yarn licenses
yarn licenses命令有两个子命令。
yarn licenses ls命令列出所有模块的许可证。
yarn licenses generate-disclaimer命令将所有模块的许可证的条款,全部显示出来。
yarn why
yarn why命令列出之所以安装某个模块的原因,即为什么安装了它。
$ yarn why jest
你也可以用它分析某个目录或者某个文件。
$ yarn why node_modules/once
$ yarn why node_modules/once/once.js
yarn upgrade
yarn upgrade命令会按照package.json里面指定的版本范围,更新依赖版本,重新生成yarn.lock。
$ yarn upgrade
如果单独升级某个模块,yarn upgrade会将它升级到latest标签指定的版本,然后改写package.json。这意味着该命令可能会将一个1.x版本的模块,升级到2.x。
$ yarn upgrade d3-scale
更新时指定版本范围或标签,也是允许的。
$ yarn upgrade d3-scale@1.0.2
$ yarn upgrade react@next
yarn generate-lock-entry
yarn generate-lock-entry命令依照package.json文件,生成yarn.lock文件。
$ yarn generate-lock-entry
yarn.lock 文件
yarn.lock是一个锁文件,用来记录当前项目的依赖模块的精确版本。只要项目的根目录有这个文件,下次安装依赖的时候,总是会安装一模一样的node_modules目录,这个特点称为决定性(determinism)。
如果当前项目没有这个文件,那么第一次运行yarn install或者yarn add [模块名]命令的时候,就会生成这个文件。以后,再运行yarn add命令,会更新这个文件。
举例来说,yarn add supports-color命令会产生下面的yarn.lock文件。
has-flag@^1.0.0:
version "1.0.0"
resolved "https://registry.yarnpkg.com/has-flag/-/has-flag-1.0.0.tgz#9d9e793165ce017a00f00418c43f942a7b1d11fa"
supports-color@^3.2.3:
version "3.2.3"
resolved "https://registry.yarnpkg.com/supports-color/-/supports-color-3.2.3.tgz#65ac0504b3954171d8a64946b2ae3cbb8a5f54f6"
dependencies:
has-flag "^1.0.0"
上面代码中,模块之间使用空行分隔。每个模块会指明当前安装的精确版本(version字段)和下载地址(resovled字段),以及依赖的模块(dependencies字段)。
注意,从yarn.lock文件看不出来,哪个模块会安装在node_modules目录的顶层,必须结合package.json才能看出来,具体的算法由 Yarn 决定。这也意味着,不同版本的 Yarn 处理同样的yarn.lock文件,可能会得到不一样的node_modules目录,但是每个模块的版本肯定都是相同的。只有相同版本的 Yarn,才能保证一定会得到相同的node_modules目录。
参考链接
- Yarn determinism, by Sebastian McKenzie
- Running Yarn offline, by Konstantin Raev
- Yarn CLI
cluster 模块
cluster模块用于组建 Node.js 应用的集群。
cluster.isMaster属性表示当前进程是否为主进程。
const cluster = require('cluster');
const isMaster = cluster.isMaster;
cluster.fork()方法复制当前进程。
const cluster = require('cluster');
const { cpus } = require('os');
const numWorkers = cpus().length;
const isMaster = cluster.isMaster;
if (isMaster) {
process.stdout.write('master process');
const workers = [];
for(let i = 0; i < numWorkers; i++) {
workers.push(cluster.fork());
}
} else {
process.stdout.write('worker process');
}
上面代码按照 CPU 内核的数目新建 Worker 进程。
主线程可以负责监听整个集群的运行情况。我们使用cluster.on()方法监听集群的事件。online事件在 Worker 进程上线时触发,exit事件在 Worker 进程下线时触发。
if (isMaster) {
log(`Forking ${numWorkers} workers`);
const workers = [];
for(let i = 0; i < numWorkers; i++) {
workers.push(cluster.fork());
}
cluster.on('online', (worker) => log(`Worker ${worker.process.pid} is online`));
cluster.on('exit', (worker, exitCode) => {
log(`Worker ${worker.process.id} exited with code ${exitCode}`);
log(`Starting a new worker`);
cluster.fork();
})
}
上面代码中,主进程如果发现一个 Worker 进程下线,就再新建一个 Worker 进程。
至于每个 Worker 进程可以指定运行的任务。
if (isMaster) {
// ...
} else {
// 应用的内容
}
参考链接
- How to scale your Node.js server using clustering, Michele Riva
events 模块
Node 通过 events 模块提供事件,形成模块之间的通信机制,消除模块与模块的强耦合。
概述
events模块提供一个构造函数,可以用来生成事件发生器的实例。
const EventEmitter = require('events');
const emitter = new EventEmitter();
上面代码中,emitter就是事件发生器的实例。
emit()方法用于触发事件。
emitter.emit('user-registered', user);
上面代码触发了user-registered事件,第二个参数user是事件传递的数据。
on()方法用于监听事件。
emitter.on('user-registered', (user) => {
console.log('event has occured');
});
注意,emit()方法和on()方法都是同步的,请看下面的例子。
emitter.on('someEvent', function () {
console.log('event has occured');
});
function f() {
console.log('start');
emitter.emit('someEvent');
console.log('end');
}
f()
// start
// event has occured
// end
上面代码中,emit()方法执行以后,on()方法会立刻执行。只有等到on()方法执行结束以后,emit()后面的方法才会继续执行。
事件接口的部署
事件接口可以部署在任意对象上,使得这些对象也能订阅和发布消息。
var EventEmitter = require('events').EventEmitter;
function Dog(name) {
this.name = name;
}
Dog.prototype.__proto__ = EventEmitter.prototype;
// 另一种写法
// Dog.prototype = Object.create(EventEmitter.prototype);
var simon = new Dog('simon');
simon.on('bark', function () {
console.log(this.name + ' barked');
});
setInterval(function () {
simon.emit('bark');
}, 500);
上面代码新建了一个构造函数Dog,然后让其继承EventEmitter,因此Dog就拥有了EventEmitter的接口。最后,为Dog的实例指定bark事件的监听函数,再使用EventEmitter的emit方法,触发bark事件。
Node 内置模块util的inherits方法,提供了另一种继承 Event Emitter 接口的方法。
var util = require('util');
var EventEmitter = require('events').EventEmitter;
var Radio = function (station) {
var self = this;
setTimeout(function() {
self.emit('open', station);
}, 0);
setTimeout(function() {
self.emit('close', station);
}, 5000);
this.on('newListener', function(listener) {
console.log('Event Listener: ' + listener);
});
};
util.inherits(Radio, EventEmitter);
module.exports = Radio;
上面代码中,Radio是一个构造函数,它的实例继承了EventEmitter接口。下面是使用这个模块的例子。
var Radio = require('./radio.js');
var station = {
freq: '80.16',
name: 'Rock N Roll Radio',
};
var radio = new Radio(station);
radio.on('open', function(station) {
console.log('"%s" FM %s 打开', station.name, station.freq);
console.log('♬ ♫♬');
});
radio.on('close', function(station) {
console.log('"%s" FM %s 关闭', station.name, station.freq);
});
Event Emitter 的实例方法
Event Emitter 的实例方法如下。
emitter.on(name, f)对事件name指定监听函数femitter.addListener(name, f)addListener是on方法的别名emitter.once(name, f)与on方法类似,但是监听函数f是一次性的,使用后自动移除emitter.listeners(name)返回一个数组,成员是事件name所有监听函数emitter.removeListener(name, f)移除事件name的监听函数femitter.removeAllListeners(name)移除事件name的所有监听函数
emit()
EventEmitter实例对象的emit方法,用来触发事件。它的第一个参数是事件名称,其余参数都会依次传入回调函数。
var EventEmitter = require('events').EventEmitter;
var myEmitter = new EventEmitter();
var connection = function (id) {
console.log('client id: ' + id);
};
myEmitter.on('connection', connection);
myEmitter.emit('connection', 6);
// client id: 6
once()
该方法类似于on方法,但是回调函数只触发一次。
var EventEmitter = require('events').EventEmitter;
var myEmitter = new EventEmitter;
myEmitter.once('message', function(msg){
console.log('message: ' + msg);
});
myEmitter.emit('message', 'this is the first message');
myEmitter.emit('message', 'this is the second message');
myEmitter.emit('message', 'welcome to nodejs');
上面代码触发了三次message事件,但是回调函数只会在第一次调用时运行。
下面代码指定,一旦服务器连通,只调用一次的回调函数。
{% highlight javascript %}
server.once('connection', function (stream) { console.log('Ah, we have our first user!'); });
{% endhighlight %}
该方法返回一个EventEmitter对象,因此可以链式加载监听函数。
removeListener()
该方法用于移除回调函数。它接受两个参数,第一个是事件名称,第二个是回调函数名称。这就是说,不能用于移除匿名函数。
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
emitter.on('message', console.log);
setInterval(function(){
emitter.emit('message', 'foo bar');
}, 300);
setTimeout(function(){
emitter.removeListener('message', console.log);
}, 1000);
上面代码每300毫秒触发一次message事件,直到1000毫秒后取消监听。
另一个例子是使用removeListener方法模拟once方法。
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
function onlyOnce () {
console.log("You'll never see this again");
emitter.removeListener("firstConnection", onlyOnce);
}
emitter.on("firstConnection", onlyOnce);
setMaxListeners()
Node默认允许同一个事件最多可以指定10个回调函数。
emitter.on('someEvent', function () { console.log('event 1'); });
emitter.on('someEvent', function () { console.log('event 2'); });
emitter.on('someEvent', function () { console.log('event 3'); });
超过10个回调函数,会发出一个警告。这个门槛值可以通过setMaxListeners方法改变。
emitter.setMaxListeners(20);
removeAllListeners()
该方法用于移除某个事件的所有回调函数。
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
// some code here
emitter.removeAllListeners("firstConnection");
如果不带参数,则表示移除所有事件的所有回调函数。
{% highlight javascript %}
emitter.removeAllListeners();
{% endhighlight %}
listeners()
listeners方法接受一个事件名称作为参数,返回该事件所有回调函数组成的数组。
var EventEmitter = require('events').EventEmitter;
var ee = new EventEmitter;
function onlyOnce () {
console.log(ee.listeners("firstConnection"));
ee.removeListener("firstConnection", onlyOnce);
console.log(ee.listeners("firstConnection"));
}
ee.on("firstConnection", onlyOnce)
ee.emit("firstConnection");
ee.emit("firstConnection");
// [ [Function: onlyOnce] ]
// []
上面代码显示两次回调函数组成的数组,第一次只有一个回调函数onlyOnce,第二次是一个空数组,因为removeListener方法取消了回调函数。
错误捕获
事件处理过程中抛出的错误,可以使用try...catch捕获。
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();
emitter.on('beep', function () {
console.log('beep');
});
emitter.on('beep', function () {
throw Error('oops!');
});
emitter.on('beep', function () {
console.log('beep again');
});
console.log('before emit');
try {
emitter.emit('beep');
} catch(err) {
console.error('caught while emitting:', err.message);
}
console.log('after emit');
上面的代码,beep一共绑定了三个监听函数。其中,第二个监听函数会抛出错误。执行上面的代码,会得到下面的结果。
before emit
beep
caught while emitting: oops!
after emit
可以看到,第二个监听函数抛出的错误被try...catch代码块捕获了。一旦被捕获,该事件后面的监听函数都不会再执行了。
如果不使用try...catch,可以让进程监听uncaughtException事件。
process.on('uncaughtException', function (err) {
console.error('uncaught exception:', err.stack || err);
// 关闭资源
closeEverything(function(err) {
if (err)
console.error('Error while closing everything:', err.stack || err);
// 退出进程
process.exit(1);
});
});
事件类型
Events模块默认支持两个事件。
newListener事件:添加新的回调函数时触发。removeListener事件:移除回调时触发。
ee.on("newListener", function (evtName) {
console.log("New Listener: " + evtName);
});
ee.on("removeListener", function (evtName) {
console.log("Removed Listener: " + evtName);
});
function foo() {}
ee.on("save-user", foo);
ee.removeListener("save-user", foo);
// New Listener: removeListener
// New Listener: save-user
// Removed Listener: save-user
上面代码会触发两次newListener事件,以及一次removeListener事件。
参考链接
- Hage Yaapa, Node.js EventEmitter Tutorial
http 模块
http模块用于 HTTP 通信。
http.Server
http.Server属性指向一个类,表示 Web 服务器实例。
这个类继承了net.Server,而net.Server继承了 EventEmitter 接口,因此可以使用server.on()方法监听事情。最重要的一个事件是request,表示收到 HTTP 请求。
server.on('request', (request, response) => {
response.end('Hello, world!');
});
server.listen()方法用于启动 Web 服务。这个方法需要指定监听的端口,以及一个回调函数(启动后要做什么)。
server.listen(PORT, () => {
console.log(`starting server at port ${PORT}`);
});
http.createServer()
http.createServer()方法用于创建一个 Web 服务器,它的返回值就是一个http.Server实例。
const { createServer } = require('http');
// 指定端口
const PORT = process.env.PORT || 8080;
const server = createServer();
server.on('request', (request, response) => {
response.end('Hello, world!');
});
server.listen(PORT, () => {
console.log(`starting server at port ${PORT}`);
});
request事件的回调函数,可以作为createServer()方法的参数。因此,上面的代码也可以写成下面的样子。
const { createServer } = require('http');
const PORT = process.env.PORT || 8080;
const server = createServer((request, response) => {
response.end('Hello, world!');
}).listen(PORT, () => {
console.log(`starting server at port ${PORT}`);
});
response 对象
HTTP 请求和回应都是数据流(stream),request是只读数据流,response是可写数据流。
response.write()方法表示依次向 HTTP 回应写入内容。
response.end()方法表示写入数据以后,关闭response数据流。
const { createServer } = require('http');
createServer((request, response) => {
// 等同于 response.end('Hello, world!')
response.write('Hello');
response.write(', ');
response.write('World!');
response.end();
}).listen(8080);
注意,response.end()是必需的,而且必须写在最后,否则 Node 不会关闭请求。
如果要向客户端发送文件,也可以写成数据流。
const { createReadStream } = require('fs');
const { createServer } = require('http');
createServer((request, response) => {
createReadStream(__filename).pipe(response);
}).listen(8080);
上面代码会将这个脚本代码,发送给客户端。这时不用写response.end()方法,因为pipe()方法会在发送结束后,自动关闭数据流。
response.setHeader()方法用于设置返回的头信息。
const { createServer } = require('http');
createServer((req, res) => {
res.setHeader('content-type', 'application/json');
res.end(JSON.stringify({ foo: 'bar' }));
}).listen(8080);
const { createServer } = require('http');
createServer((req, res) => {
res.setHeader(
'Set-Cookie',
['myCookie=myValue'],
['mySecondCookie=mySecondValue']
);
res.end(`Your cookies are: ${req.headers.cookie}`);
}).listen(8080);
response.writeHead()方法与response.setHeader()类似,但是优先级更高。它可以设置返回的 HTTP 状态码,第一个参数是状态码,第二个参数是状态说明。
const { createServer } = require('http');
createServer((req, res) => {
res.writeHead(204, 'My Custom Message');
res.end();
}).listen(8080);
request 对象
request.headers属性返回一个对象,包含了 HTTP 请求的头信息。该对象的键名是头信息的字段名,键值是头信息的字段值。
const { createServer } = require("http");
createServer((request, response) => {
const languages = request.headers['accept-language'];
response.end(languages);
}).listen(8080);
上面代码返回 HTTP 请求的头信息accept-language。
request.url属性返回浏览器请求的路径,可以基于这个属性做一个简单的路由。
const { createServer } = require('http');
createServer((req, res) => {
switch (req.url) {
case '/':
res.end('You are on the main page!');
break;
case '/about':
res.end('You are on about page!');
break;
default:
res.statusCode = 404;
res.end('Page not found!');
}
}).listen(8080);
获取查询字符串,可以用 Node 内置的url模块解析request.url属性。
const { createServer } = require('http');
createServer((req, res) => {
const { query } = require('url').parse(req.url, true);
if (query.name) {
res.end(`You requested parameter name with value ${query.name}`);
} else {
res.end('Hello!');
}
}).listen(8080);
request.method属性返回浏览器请求的方法。
const { createServer } = require('http');
createServer((req, res) => {
if (req.method === 'GET') {
return res.end('List of data');
} else if (req.method === 'POST') {
return res.end('success');
} else {
res.statusCode(400);
return res.end('Unsupported method');
}
}).listen(8080);
POST 请求发送的数据体是一个数据流(stream),可以用request.on()方法监听data事件,累加数据流的每一部分;监听end事件,得到数据流接收结束的通知。
const { createServer } = require('http');
createServer((req, res) => {
if (req.method === 'POST') {
let data = '';
req.on('data', chunk => {
data += chunk;
});
req.on('end', () => {
try {
const requestData = JSON.parse(data);
requestData.ourMessage = 'success';
res.setHeader('Content-Type', 'application/json');
res.end(JSON.stringify(requestData));
} catch (e) {
res.statusCode = 400;
res.end('Invalid JSON');
}
});
} else {
res.statusCode = 400;
res.end('Unsupported method, please POST a JSON object');
}
}).listen(8080);
参考链接
os 模块
cpus属性返回一个数组,每个成员对应一个 CPU 内核。下面代码可以获取本机的 CPU 内核数目。
const { cpus } = require('os');
const numWorkers = cpus().length;
process 对象
process对象是 Node 原生提供的对象,表示当前运行的 Node 进程。它不用引入模块,可以直接使用。
process.argv
process.argv是一个数组,表示启动脚本时的命令行参数。
它的前两项是固定的。
- 第一项是 Node 可执行文件的路径
- 第二项是 JavaScript 脚本的路径
后面的数组成员都是命令行参数。
$ node index.js --watch
上面这个命令执行后,在index.js脚本里面,process.argv数组共有三项。
process.argv[0]:/path/to/nodeprocess.argv[1]:/path/to/index.jsprocess.argv[2]:--watch
如果只需要命令行参数,可以用解构赋值获取。
const [ , , ...args ] = process.argv;
console.log(args[0]); // "--watch"
上面代码,args数组就是通过解构赋值,拿到的所有命令行参数。
参考链接
- Extracting command line arguments from Node.js using destructuring, Nicholas C. Zakas