Node.js 教程

介绍 Node.js 运行时与常用模块、基本开发方式。适合会一点 JavaScript、想上手服务端的读者。

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 ###');
}

参考链接

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 还提供其他一些命令。

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

参考链接

定时器

process.nextTick()会立即执行回调函数。 微任务队列。

setImmediate(), clearImmediate()

setImmediate(). It can be passed to clearImmediate()

setImmediate()会在下一轮执行回调函数。

定时器在 IO 操作的回调函数之前执行。

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仓库,它后面可以跟字符串,也可以跟正则表达式。

$ 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模块的时候,有时我们会希望,边开发边试用,比如本地调试的时候,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模块了。但是,如果我们要让这个模块安装在项目内,还要进行下面的步骤。

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的淘宝镜像。

参考链接

安装

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 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 <本地模块名>,在node_modules目录里面产生本地模块的符号链接。

也可以将上面两步合二为一,在应用目录里面,直接链接本地模块的路径。

等到测试完毕,再用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 格式处理模块。

以下情况会以 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排除。

以下文件,发布的时候总是会包含。

README、CHANGES、LICENSE和NOTICE这四个文件名,可以采取任意的大小写组合。

以下文件,发布的时候总是会被排除。

基本上,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支持以下的条件关键词。

下面是一个例子。

{
  "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

参考链接

脚本功能

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-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,会按下面的顺序执行相应的命令。

  1. pretest
  2. test
  3. posttest

如果执行过程出错,就不会执行排在后面的脚本,即如果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-脚本。

对于最后一个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 的通配符的规则如下。

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目录。

参考链接

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 {
  // 应用的内容
}

参考链接

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 的实例方法如下。

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模块默认支持两个事件。

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事件。

参考链接

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 index.js --watch

上面这个命令执行后,在index.js脚本里面,process.argv数组共有三项。

如果只需要命令行参数,可以用解构赋值获取。

const [ , , ...args ] = process.argv;
console.log(args[0]);   // "--watch"

上面代码,args数组就是通过解构赋值,拿到的所有命令行参数。

参考链接