<h2 id="12.1">Node.js 概述</h2>
## 简介
Node是JavaScript语言的服务器运行环境。
所谓“运行环境”有两层意思:首先,JavaScript语言通过Node在服务器运行,在这个意义上,Node有点像JavaScript虚拟机;其次,Node提供大量工具库,使得JavaScript语言与操作系统互动(比如读写文件、新建子进程),在这个意义上,Node又是JavaScript的工具库。
Node内部采用Google公司的V8引擎,作为JavaScript语言解释器;通过自行开发的libuv库,调用操作系统资源。
### 安装与更新
访问官方网站[nodejs.org](http://nodejs.org)或者[github.com/nodesource/distributions](https://github.com/nodesource/distributions),查看Node的最新版本和安装方法。
官方网站提供编译好的二进制包,可以把它们解压到`/usr/local`目录下面。
```bash
$ tar -xf node-someversion.tgz
```
然后,建立符号链接,把它们加到$PATH变量里面的路径。
```bash
$ ln -s /usr/local/node/bin/node /usr/local/bin/node
$ ln -s /usr/local/node/bin/npm /usr/local/bin/npm
```
下面是Ubuntu和Debian下面安装Deb软件包的安装方法。
```bash
$ curl -sL https://deb.nodesource.com/setup_4.x | sudo -E bash -
$ sudo apt-get install -y nodejs
$ apt-get install nodejs
```
安装完成以后,运行下面的命令,查看是否能正常运行。
```bash
$ node --version
# 或者
$ node -v
```
更新node.js版本,可以通过node.js的`n`模块完成。
```bash
$ sudo npm install n -g
$ sudo n stable
```
上面代码通过`n`模块,将node.js更新为最新发布的稳定版。
`n`模块也可以指定安装特定版本的node。
```bash
$ sudo n 0.10.21
```
### 版本管理工具nvm
如果想在同一台机器,同时安装多个版本的node.js,就需要用到版本管理工具nvm。
```bash
$ git clone https://github.com/creationix/nvm.git ~/.nvm
$ source ~/.nvm/nvm.sh
```
安装以后,nvm的执行脚本,每次使用前都要激活,建议将其加入~/.bashrc文件(假定使用Bash)。激活后,就可以安装指定版本的Node。
```bash
# 安装最新版本
$ nvm install node
# 安装指定版本
$ nvm install 0.12.1
# 使用已安装的最新版本
$ nvm use node
# 使用指定版本的node
$ nvm use 0.12
```
nvm也允许进入指定版本的REPL环境。
```bash
$ nvm run 0.12
```
如果在项目根目录下新建一个.nvmrc文件,将版本号写入其中,就只输入`nvm use`命令即可,不再需要附加版本号。
下面是其他经常用到的命令。
```bash
# 查看本地安装的所有版本
$ nvm ls
# 查看服务器上所有可供安装的版本。
$ nvm ls-remote
# 退出已经激活的nvm,使用deactivate命令。
$ nvm deactivate
```
### 基本用法
安装完成后,运行node.js程序,就是使用node命令读取JavaScript脚本。
当前目录的`demo.js`脚本文件,可以这样执行。
```bash
$ node demo
# 或者
$ node demo.js
```
使用`-e`参数,可以执行代码字符串。
```bash
$ node -e 'console.log("Hello World")'
Hello World
```
### REPL环境
在命令行键入node命令,后面没有文件名,就进入一个Node.js的REPL环境(Read–eval–print loop,"读取-求值-输出"循环),可以直接运行各种JavaScript命令。
```bash
$ node
> 1+1
2
>
```
如果使用参数 --use_strict,则REPL将在严格模式下运行。
```bash
$ node --use_strict
```
REPL是Node.js与用户互动的shell,各种基本的shell功能都可以在里面使用,比如使用上下方向键遍历曾经使用过的命令。
特殊变量下划线(_)表示上一个命令的返回结果。
```bash
> 1 + 1
2
> _ + 1
3
```
在REPL中,如果运行一个表达式,会直接在命令行返回结果。如果运行一条语句,就不会有任何输出,因为语句没有返回值。
```bash
> x = 1
1
> var x = 1
```
上面代码的第二条命令,没有显示任何结果。因为这是一条语句,不是表达式,所以没有返回值。
### 异步操作
Node采用V8引擎处理JavaScript脚本,最大特点就是单线程运行,一次只能运行一个任务。这导致Node大量采用异步操作(asynchronous opertion),即任务不是马上执行,而是插在任务队列的尾部,等到前面的任务运行完后再执行。
由于这种特性,某一个任务的后续操作,往往采用回调函数(callback)的形式进行定义。
```javascript
var isTrue = function(value, callback) {
if (value === true) {
callback(null, "Value was true.");
}
else {
callback(new Error("Value is not true!"));
}
}
```
上面代码就把进一步的处理,交给回调函数callback。
Node约定,如果某个函数需要回调函数作为参数,则回调函数是最后一个参数。另外,回调函数本身的第一个参数,约定为上一步传入的错误对象。
```javascript
var callback = function (error, value) {
if (error) {
return console.log(error);
}
console.log(value);
}
```
上面代码中,callback的第一个参数是Error对象,第二个参数才是真正的数据参数。这是因为回调函数主要用于异步操作,当回调函数运行时,前期的操作早结束了,错误的执行栈早就不存在了,传统的错误捕捉机制try...catch对于异步操作行不通,所以只能把错误交给回调函数处理。
```javascript
try {
db.User.get(userId, function(err, user) {
if(err) {
throw err
}
// ...
})
} catch(e) {
console.log(‘Oh no!’);
}
```
上面代码中,db.User.get方法是一个异步操作,等到抛出错误时,可能它所在的try...catch代码块早就运行结束了,这会导致错误无法被捕捉。所以,Node统一规定,一旦异步操作发生错误,就把错误对象传递到回调函数。
如果没有发生错误,回调函数的第一个参数就传入null。这种写法有一个很大的好处,就是说只要判断回调函数的第一个参数,就知道有没有出错,如果不是null,就肯定出错了。另外,这样还可以层层传递错误。
```javascript
if(err) {
// 除了放过No Permission错误意外,其他错误传给下一个回调函数
if(!err.noPermission) {
return next(err);
}
}
```
### 全局对象和全局变量
Node提供以下几个全局对象,它们是所有模块都可以调用的。
- **global**:表示Node所在的全局环境,类似于浏览器的window对象。需要注意的是,如果在浏览器中声明一个全局变量,实际上是声明了一个全局对象的属性,比如`var x = 1`等同于设置`window.x = 1`,但是Node不是这样,至少在模块中不是这样(REPL环境的行为与浏览器一致)。在模块文件中,声明`var x = 1`,该变量不是`global`对象的属性,`global.x`等于undefined。这是因为模块的全局变量都是该模块私有的,其他模块无法取到。
- **process**:该对象表示Node所处的当前进程,允许开发者与该进程互动。
- **console**:指向Node内置的console模块,提供命令行环境中的标准输入、标准输出功能。
Node还提供一些全局函数。
- **setTimeout()**:用于在指定毫秒之后,运行回调函数。实际的调用间隔,还取决于系统因素。间隔的毫秒数在1毫秒到2,147,483,647毫秒(约24.8天)之间。如果超过这个范围,会被自动改为1毫秒。该方法返回一个整数,代表这个新建定时器的编号。
- **clearTimeout()**:用于终止一个setTimeout方法新建的定时器。
- **setInterval()**:用于每隔一定毫秒调用回调函数。由于系统因素,可能无法保证每次调用之间正好间隔指定的毫秒数,但只会多于这个间隔,而不会少于它。指定的毫秒数必须是1到2,147,483,647(大约24.8天)之间的整数,如果超过这个范围,会被自动改为1毫秒。该方法返回一个整数,代表这个新建定时器的编号。
- **clearInterval()**:终止一个用setInterval方法新建的定时器。
- **require()**:用于加载模块。
- **Buffer()**:用于操作二进制数据。
Node提供两个全局变量,都以两个下划线开头。
- `__filename`:指向当前运行的脚本文件名。
- `__dirname`:指向当前运行的脚本所在的目录。
除此之外,还有一些对象实际上是模块内部的局部变量,指向的对象根据模块不同而不同,但是所有模块都适用,可以看作是伪全局变量,主要为module, module.exports, exports等。
## 模块化结构
### 概述
Node.js采用模块化结构,按照[CommonJS规范](http://wiki.commonjs.org/wiki/CommonJS)定义和使用模块。模块与文件是一一对应关系,即加载一个模块,实际上就是加载对应的一个模块文件。
require命令用于指定加载模块,加载时可以省略脚本文件的后缀名。
```javascript
var circle = require('./circle.js');
// 或者
var circle = require('./circle');
```
require方法的参数是模块文件的名字。它分成两种情况,第一种情况是参数中含有文件路径(比如上例),这时路径是相对于当前脚本所在的目录,第二种情况是参数中不含有文件路径,这时Node到模块的安装目录,去寻找已安装的模块(比如下例)。
```javascript
var bar = require('bar');
```
有时候,一个模块本身就是一个目录,目录中包含多个文件。这时候,Node在package.json文件中,寻找main属性所指明的模块入口文件。
```javascript
{
"name" : "bar",
"main" : "./lib/bar.js"
}
```
上面代码中,模块的启动文件为lib子目录下的bar.js。当使用`require('bar')`命令加载该模块时,实际上加载的是`./node_modules/bar/lib/bar.js`文件。下面写法会起到同样效果。
```javascript
var bar = require('bar/lib/bar.js')
```
如果模块目录中没有package.json文件,node.js会尝试在模块目录中寻找index.js或index.node文件进行加载。
模块一旦被加载以后,就会被系统缓存。如果第二次还加载该模块,则会返回缓存中的版本,这意味着模块实际上只会执行一次。如果希望模块执行多次,则可以让模块返回一个函数,然后多次调用该函数。
### 核心模块
如果只是在服务器运行JavaScript代码,用处并不大,因为服务器脚本语言已经有很多种了。Node.js的用处在于,它本身还提供了一系列功能模块,与操作系统互动。这些核心的功能模块,不用安装就可以使用,下面是它们的清单。
- **http**:提供HTTP服务器功能。
- **url**:解析URL。
- **fs**:与文件系统交互。
- **querystring**:解析URL的查询字符串。
- **child_process**:新建子进程。
- **util**:提供一系列实用小工具。
- **path**:处理文件路径。
- **crypto**:提供加密和解密功能,基本上是对OpenSSL的包装。
上面这些核心模块,源码都在Node的lib子目录中。为了提高运行速度,它们安装时都会被编译成二进制文件。
核心模块总是最优先加载的。如果你自己写了一个HTTP模块,`require('http')`加载的还是核心模块。
### 自定义模块
Node模块采用CommonJS规范。只要符合这个规范,就可以自定义模块。
下面是一个最简单的模块,假定新建一个foo.js文件,写入以下内容。
```javascript
// foo.js
module.exports = function(x) {
console.log(x);
};
```
上面代码就是一个模块,它通过module.exports变量,对外输出一个方法。
这个模块的使用方法如下。
```javascript
// index.js
var m = require('./foo');
m("这是自定义模块");
```
上面代码通过require命令加载模块文件foo.js(后缀名省略),将模块的对外接口输出到变量m,然后调用m。这时,在命令行下运行index.js,屏幕上就会输出“这是自定义模块”。
```bash
$ node index
这是自定义模块
```
module变量是整个模块文件的顶层变量,它的exports属性就是模块向外输出的接口。如果直接输出一个函数(就像上面的foo.js),那么调用模块就是调用一个函数。但是,模块也可以输出一个对象。下面对foo.js进行改写。
```javascript
// foo.js
var out = new Object();
function p(string) {
console.log(string);
}
out.print = p;
module.exports = out;
```
上面的代码表示模块输出out对象,该对象有一个print属性,指向一个函数。下面是这个模块的使用方法。
```javascript
// index.js
var m = require('./foo');
m.print("这是自定义模块");
```
上面代码表示,由于具体的方法定义在模块的print属性上,所以必须显式调用print属性。
## 异常处理
Node是单线程运行环境,一旦抛出的异常没有被捕获,就会引起整个进程的崩溃。所以,Node的异常处理对于保证系统的稳定运行非常重要。
一般来说,Node有三种方法,传播一个错误。
- 使用throw语句抛出一个错误对象,即抛出异常。
- 将错误对象传递给回调函数,由回调函数负责发出错误。
- 通过EventEmitter接口,发出一个error事件。
### try...catch结构
最常用的捕获异常的方式,就是使用try...catch结构。但是,这个结构无法捕获异步运行的代码抛出的异常。
```javascript
try {
process.nextTick(function () {
throw new Error("error");
});
} catch (err) {
//can not catch it
console.log(err);
}
try {
setTimeout(function(){
throw new Error("error");
},1)
} catch (err) {
//can not catch it
console.log(err);
}
```
上面代码分别用process.nextTick和setTimeout方法,在下一轮事件循环抛出两个异常,代表异步操作抛出的错误。它们都无法被catch代码块捕获,因此catch代码块所在的那部分已经运行结束了。
一种解决方法是将错误捕获代码,也放到异步执行。
```javascript
function async(cb, err) {
setTimeout(function() {
try {
if (true)
throw new Error("woops!");
else
cb("done");
} catch(e) {
err(e);
}
}, 2000)
}
async(function(res) {
console.log("received:", res);
}, function(err) {
console.log("Error: async threw an exception:", err);
});
// Error: async threw an exception: Error: woops!
```
上面代码中,async函数异步抛出的错误,可以同样部署在异步的catch代码块捕获。
这两种处理方法都不太理想。一般来说,Node只在很少场合才用try/catch语句,比如使用`JSON.parse`解析JSON文本。
### 回调函数
Node采用的方法,是将错误对象作为第一个参数,传入回调函数。这样就避免了捕获代码与发生错误的代码不在同一个时间段的问题。
```javascript
fs.readFile('/foo.txt', function(err, data) {
if (err !== null) throw err;
console.log(data);
});
```
上面代码表示,读取文件`foo.txt`是一个异步操作,它的回调函数有两个参数,第一个是错误对象,第二个是读取到的文件数据。如果第一个参数不是null,就意味着发生错误,后面代码也就不再执行了。
下面是一个完整的例子。
```javascript
function async2(continuation) {
setTimeout(function() {
try {
var res = 42;
if (true)
throw new Error("woops!");
else
continuation(null, res); // pass 'null' for error
} catch(e) {
continuation(e, null);
}
}, 2000);
}
async2(function(err, res) {
if (err)
console.log("Error: (cps) failed:", err);
else
console.log("(cps) received:", res);
});
// Error: (cps) failed: woops!
```
上面代码中,async2函数的回调函数的第一个参数就是一个错误对象,这是为了处理异步操作抛出的错误。
### EventEmitter接口的error事件
发生错误的时候,也可以用EventEmitter接口抛出error事件。
```javascript
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter();
emitter.emit('error', new Error('something bad happened'));
```
使用上面的代码必须小心,因为如果没有对error事件部署监听函数,会导致整个应用程序崩溃。所以,一般总是必须同时部署下面的代码。
```javascript
emitter.on('error', function(err) {
console.error('出错:' + err.message);
});
```
### uncaughtException事件
当一个异常未被捕获,就会触发uncaughtException事件,可以对这个事件注册回调函数,从而捕获异常。
```javascript
process.on('uncaughtException', function(err) {
console.error('Error caught in uncaughtException event:', err);
});
try {
setTimeout(function(){
throw new Error("error");
},1)
} catch (err) {
//can not catch it
console.log(err);
}
```
只要给uncaughtException配置了回调,Node进程不会异常退出,但异常发生的上下文已经丢失,无法给出异常发生的详细信息。而且,异常可能导致Node不能正常进行内存回收,出现内存泄露。所以,当uncaughtException触发后,最好记录错误日志,然后结束Node进程。
```javascript
process.on('uncaughtException', function(err) {
logger(err);
process.exit(1);
});
```
### unhandledRejection事件
iojs有一个unhandledRejection事件,用来监听没有捕获的Promise对象的rejected状态。
```javascript
var promise = new Promise(function(resolve, reject) {
reject(new Error("Broken."));
});
promise.then(function(result) {
console.log(result);
})
```
上面代码中,promise的状态变为rejected,并且抛出一个错误。但是,不会有任何反应,因为没有设置任何处理函数。
只要监听unhandledRejection事件,就能解决这个问题。
```javascript
process.on('unhandledRejection', function (err, p) {
console.error(err.stack);
})
```
需要注意的是,unhandledRejection事件的监听函数有两个参数,第一个是错误对象,第二个是产生错误的promise对象。这可以提供很多有用的信息。
```javascript
var http = require('http');
http.createServer(function (req, res) {
var promise = new Promise(function(resolve, reject) {
reject(new Error("Broken."))
})
p.info = {url: req.url}
}).listen(8080)
process.on('unhandledRejection', function (err, p) {
if (p.info && p.info.url) {
console.log('Error in URL', p.info.url)
}
console.error(err.stack)
})
```
上面代码会在出错时,输出用户请求的网址。
```javascript
Error in URL /testurl
Error: Broken.
at /Users/mikeal/tmp/test.js:9:14
at Server.<anonymous> (/Users/mikeal/tmp/test.js:4:17)
at emitTwo (events.js:87:13)
at Server.emit (events.js:169:7)
at HTTPParser.parserOnIncoming [as onIncoming] (_http_server.js:471:12)
at HTTPParser.parserOnHeadersComplete (_http_common.js:88:23)
at Socket.socketOnData (_http_server.js:322:22)
at emitOne (events.js:77:13)
at Socket.emit (events.js:166:7)
at readableAddChunk (_stream_readable.js:145:16)
```
## 命令行脚本
node脚本可以作为命令行脚本使用。
```bash
$ node foo.js
```
上面代码执行了foo.js脚本文件。
foo.js文件的第一行,如果加入了解释器的位置,就可以将其作为命令行工具直接调用。
```bash
#!/usr/bin/env node
```
调用前,需更改文件的执行权限。
```bash
$ chmod u+x foo.js
$ ./foo.js arg1 arg2 ...
```
作为命令行脚本时,`console.log`用于输出内容到标准输出,`process.stdin`用于读取标准输入,`child_process.exec()`用于执行一个shell命令。
<h2 id="12.2">module</h2>
## 概述
Node程序由许多个模块组成,每个模块就是一个文件。Node模块采用了CommonJS规范。
根据CommonJS规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在一个文件定义的变量(还包括函数和类),都是私有的,对其他文件是不可见的。
```javascript
// example.js
var x = 5;
var addX = function(value) {
return value + x;
};
```
上面代码中,变量`x`和函数`addX`,是当前文件`example.js`私有的,其他文件不可见。
如果想在多个文件分享变量,必须定义为`global`对象的属性。
```javascript
global.warning = true;
```
上面代码的`warning`变量,可以被所有文件读取。当然,这样写法是不推荐的。
CommonJS规定,每个文件的对外接口是`module.exports`对象。这个对象的所有属性和方法,都可以被其他文件导入。
```javascript
var x = 5;
var addX = function(value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
```
上面代码通过`module.exports`对象,定义对外接口,输出变量`x`和函数`addX`。`module.exports`对象是可以被其他文件导入的,它其实就是文件内部与外部通信的桥梁。
`require`方法用于在其他文件加载这个接口,具体用法参见《Require命令》的部分。
```javascript
var example = require('./example.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
```
CommonJS模块的特点如下。
- 所有代码都运行在模块作用域,不会污染全局作用域。
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
- 模块加载的顺序,按照其在代码中出现的顺序。
## module对象
Node内部提供一个`Module`构建函数。所有模块都是`Module`的实例。
```javascript
function Module(id, parent) {
this.id = id;
this.exports = {};
this.parent = parent;
// ...
```
每个模块内部,都有一个`module`对象,代表当前模块。它有以下属性。
- `module.id` 模块的识别符,通常是带有绝对路径的模块文件名。
- `module.filename` 模块的文件名,带有绝对路径。
- `module.loaded` 返回一个布尔值,表示模块是否已经完成加载。
- `module.parent` 返回一个对象,表示调用该模块的模块。
- `module.children` 返回一个数组,表示该模块要用到的其他模块。
- `module.exports` 表示模块对外输出的值。
下面是一个示例文件,最后一行输出module变量。
```javascript
// example.js
var jquery = require('jquery');
exports.$ = jquery;
console.log(module);
```
执行这个文件,命令行会输出如下信息。
```javascript
{ id: '.',
exports: { '$': [Function] },
parent: null,
filename: '/path/to/example.js',
loaded: false,
children:
[ { id: '/path/to/node_modules/jquery/dist/jquery.js',
exports: [Function],
parent: [Circular],
filename: '/path/to/node_modules/jquery/dist/jquery.js',
loaded: true,
children: [],
paths: [Object] } ],
paths:
[ '/home/user/deleted/node_modules',
'/home/user/node_modules',
'/home/node_modules',
'/node_modules' ]
}
```
如果在命令行下调用某个模块,比如`node something.js`,那么`module.parent`就是`undefined`。如果是在脚本之中调用,比如`require('./something.js')`,那么`module.parent`就是调用它的模块。利用这一点,可以判断当前模块是否为入口脚本。
```javascript
if (!module.parent) {
// ran with `node something.js`
app.listen(8088, function() {
console.log('app listening on port 8088');
})
} else {
// used with `require('/.something.js')`
module.exports = app;
}
```
### module.exports属性
`module.exports`属性表示当前模块对外输出的接口,其他文件加载该模块,实际上就是读取`module.exports`变量。
```javascript
var EventEmitter = require('events').EventEmitter;
module.exports = new EventEmitter();
setTimeout(function() {
module.exports.emit('ready');
}, 1000);
```
上面模块会在加载后1秒后,发出ready事件。其他文件监听该事件,可以写成下面这样。
```javascript
var a = require('./a');
a.on('ready', function() {
console.log('module a is ready');
});
```
### exports变量
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
```javascript
var exports = module.exports;
```
造成的结果是,在对外输出模块接口时,可以向exports对象添加方法。
```javascript
exports.area = function (r) {
return Math.PI * r * r;
};
exports.circumference = function (r) {
return 2 * Math.PI * r;
};
```
注意,不能直接将exports变量指向一个值,因为这样等于切断了`exports`与`module.exports`的联系。
```javascript
exports = function(x) {console.log(x)};
```
上面这样的写法是无效的,因为`exports`不再指向`module.exports`了。
下面的写法也是无效的。
```javascript
exports.hello = function() {
return 'hello';
};
module.exports = 'Hello world';
```
上面代码中,`hello`函数是无法对外输出的,因为`module.exports`被重新赋值了。
这意味着,如果一个模块的对外接口,就是一个单一的值,不能使用`exports`输出,只能使用`module.exports`输出。
```javascript
module.exports = function (x){ console.log(x);};
```
如果你觉得,`exports`与`module.exports`之间的区别很难分清,一个简单的处理方法,就是放弃使用`exports`,只使用`module.exports`。
## AMD规范与CommonJS规范的兼容性
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
AMD规范使用define方法定义模块,下面就是一个例子:
```javascript
define(['package/lib'], function(lib){
function foo(){
lib.log('hello world!');
}
return {
foo: foo
};
});
```
AMD规范允许输出的模块兼容CommonJS规范,这时`define`方法需要写成下面这样:
```javascript
define(function (require, exports, module){
var someModule = require("someModule");
var anotherModule = require("anotherModule");
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
exports.asplode = function (){
someModule.doTehAwesome();
anotherModule.doMoarAwesome();
};
});
```
## require命令
### 基本用法
Node使用CommonJS模块规范,内置的`require`命令用于加载模块文件。
`require`命令的基本功能是,读入并执行一个JavaScript文件,然后返回该模块的exports对象。如果没有发现指定模块,会报错。
```javascript
// example.js
var invisible = function () {
console.log("invisible");
}
exports.message = "hi";
exports.say = function () {
console.log(message);
}
```
运行下面的命令,可以输出exports对象。
```javascript
var example = require('./example.js');
example
// {
// message: "hi",
// say: [Function]
// }
```
如果模块输出的是一个函数,那就不能定义在exports对象上面,而要定义在`module.exports`变量上面。
```javascript
module.exports = function () {
console.log("hello world")
}
require('./example2.js')()
```
上面代码中,require命令调用自身,等于是执行`module.exports`,因此会输出 hello world。
### 加载规则
`require`命令用于加载文件,后缀名默认为`.js`。
```javascript
var foo = require('foo');
// 等同于
var foo = require('foo.js');
```
根据参数的不同格式,`require`命令去不同路径寻找模块文件。
(1)如果参数字符串以“/”开头,则表示加载的是一个位于绝对路径的模块文件。比如,`require('/home/marco/foo.js')`将加载`/home/marco/foo.js`。
(2)如果参数字符串以“./”开头,则表示加载的是一个位于相对路径(跟当前执行脚本的位置相比)的模块文件。比如,`require('./circle')`将加载当前脚本同一目录的`circle.js`。
(3)如果参数字符串不以“./“或”/“开头,则表示加载的是一个默认提供的核心模块(位于Node的系统安装目录中),或者一个位于各级node_modules目录的已安装模块(全局安装或局部安装)。
举例来说,脚本`/home/user/projects/foo.js`执行了`require('bar.js')`命令,Node会依次搜索以下文件。
- /usr/local/lib/node/bar.js
- /home/user/projects/node_modules/bar.js
- /home/user/node_modules/bar.js
- /home/node_modules/bar.js
- /node_modules/bar.js
这样设计的目的是,使得不同的模块可以将所依赖的模块本地化。
(4)如果参数字符串不以“./“或”/“开头,而且是一个路径,比如`require('example-module/path/to/file')`,则将先找到`example-module`的位置,然后再以它为参数,找到后续路径。
(5)如果指定的模块文件没有发现,Node会尝试为文件名添加`.js`、`.json`、`.node`后,再去搜索。`.js`件会以文本格式的JavaScript脚本文件解析,`.json`文件会以JSON格式的文本文件解析,`.node`文件会以编译后的二进制文件解析。
(6)如果想得到`require`命令加载的确切文件名,使用`require.resolve()`方法。
### 目录的加载规则
通常,我们会把相关的文件会放在一个目录里面,便于组织。这时,最好为该目录设置一个入口文件,让`require`方法可以通过这个入口文件,加载整个目录。
在目录中放置一个`package.json`文件,并且将入口文件写入`main`字段。下面是一个例子。
```javascript
// package.json
{ "name" : "some-library",
"main" : "./lib/some-library.js" }
```
`require`发现参数字符串指向一个目录以后,会自动查看该目录的`package.json`文件,然后加载`main`字段指定的入口文件。如果`package.json`文件没有`main`字段,或者根本就没有`package.json`文件,则会加载该目录下的`index.js`文件或`index.node`文件。
### 模块的缓存
第一次加载某个模块时,Node会缓存该模块。以后再加载该模块,就直接从缓存取出该模块的`module.exports`属性。
```javascript
require('./example.js');
require('./example.js').message = "hello";
require('./example.js').message
// "hello"
```
上面代码中,连续三次使用`require`命令,加载同一个模块。第二次加载的时候,为输出的对象添加了一个`message`属性。但是第三次加载的时候,这个message属性依然存在,这就证明`require`命令并没有重新加载模块文件,而是输出了缓存。
如果想要多次执行某个模块,可以让该模块输出一个函数,然后每次`require`这个模块的时候,重新执行一下输出的函数。
所有缓存的模块保存在`require.cache`之中,如果想删除模块的缓存,可以像下面这样写。
```javascript
// 删除指定模块的缓存
delete require.cache[moduleName];
// 删除所有模块的缓存
Object.keys(require.cache).forEach(function(key) {
delete require.cache[key];
})
```
注意,缓存是根据绝对路径识别模块的,如果同样的模块名,但是保存在不同的路径,`require`命令还是会重新加载该模块。
### 环境变量NODE_PATH
Node执行一个脚本时,会先查看环境变量`NODE_PATH`。它是一组以冒号分隔的绝对路径。在其他位置找不到指定模块时,Node会去这些路径查找。
可以将NODE_PATH添加到`.bashrc`。
```javascript
export NODE_PATH="/usr/local/lib/node"
```
所以,如果遇到复杂的相对路径,比如下面这样。
```javascript
var myModule = require('../../../../lib/myModule');
```
有两种解决方法,一是将该文件加入`node_modules`目录,二是修改`NODE_PATH`环境变量,`package.json`文件可以采用下面的写法。
```javascript
{
"name": "node_path",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "NODE_PATH=lib node index.js"
},
"author": "",
"license": "ISC"
}
```
`NODE_PATH`是历史遗留下来的一个路径解决方案,通常不应该使用,而应该使用`node_modules`目录机制。
### 模块的循环加载
如果发生模块的循环加载,即A加载B,B又加载A,则B将加载A的不完整版本。
```javascript
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
// b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
```
上面代码是三个JavaScript文件。其中,a.js加载了b.js,而b.js又加载a.js。这时,Node返回a.js的不完整版本,所以执行结果如下。
```bash
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
```
修改main.js,再次加载a.js和b.js。
```javascript
// main.js
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
console.log('main.js ', require('./a.js').x);
console.log('main.js ', require('./b.js').x);
```
执行上面代码,结果如下。
```bash
$ node main.js
b.js a1
a.js b2
main.js a2
main.js b2
main.js a2
main.js b2
```
上面代码中,第二次加载a.js和b.js时,会直接从缓存读取exports属性,所以a.js和b.js内部的console.log语句都不会执行了。
### require.main
`require`方法有一个`main`属性,可以用来判断模块是直接执行,还是被调用执行。
直接执行的时候(`node module.js`),`require.main`属性指向模块本身。
```javascript
require.main === module
// true
```
调用执行的时候(通过`require`加载该脚本执行),上面的表达式返回false。
## 模块的加载机制
CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。请看下面这个例子。
下面是一个模块文件`lib.js`。
```javascript
// lib.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
```
上面代码输出内部变量`counter`和改写这个变量的内部方法`incCounter`。
然后,加载上面的模块。
```javascript
// main.js
var counter = require('./lib').counter;
var incCounter = require('./lib').incCounter;
console.log(counter); // 3
incCounter();
console.log(counter); // 3
```
上面代码说明,`counter`输出以后,`lib.js`模块内部的变化就影响不到`counter`了。
### require的内部处理流程
`require`命令是CommonJS规范之中,用来加载其他模块的命令。它其实不是一个全局命令,而是指向当前模块的`module.require`命令,而后者又调用Node的内部命令`Module._load`。
```javascript
Module._load = function(request, parent, isMain) {
// 1. 检查 Module._cache,是否缓存之中有指定模块
// 2. 如果缓存之中没有,就创建一个新的Module实例
// 3. 将它保存到缓存
// 4. 使用 module.load() 加载指定的模块文件,
// 读取文件内容之后,使用 module.compile() 执行文件代码
// 5. 如果加载/解析过程报错,就从缓存删除该模块
// 6. 返回该模块的 module.exports
};
```
上面的第4步,采用`module.compile()`执行指定模块的脚本,逻辑如下。
```javascript
Module.prototype._compile = function(content, filename) {
// 1. 生成一个require函数,指向module.require
// 2. 加载其他辅助方法到require
// 3. 将文件内容放到一个函数之中,该函数可调用 require
// 4. 执行该函数
};
```
上面的第1步和第2步,`require`函数及其辅助方法主要如下。
- `require()`: 加载外部模块
- `require.resolve()`:将模块名解析到一个绝对路径
- `require.main`:指向主模块
- `require.cache`:指向所有缓存的模块
- `require.extensions`:根据文件的后缀名,调用不同的执行函数
一旦`require`函数准备完毕,整个所要加载的脚本内容,就被放到一个新的函数之中,这样可以避免污染全局环境。该函数的参数包括`require`、`module`、`exports`,以及其他一些参数。
```javascript
(function (exports, require, module, __filename, __dirname) {
// YOUR CODE INJECTED HERE!
});
```
`Module._compile`方法是同步执行的,所以`Module._load`要等它执行完成,才会向用户返回`module.exports`的值。
<h2 id="12.3">package.json文件</h2>
## 概述
每个项目的根目录下面,一般都有一个`package.json`文件,定义了这个项目所需要的各种模块,以及项目的配置信息(比如名称、版本、许可证等元数据)。`npm install`命令根据这个配置文件,自动下载所需的模块,也就是配置项目所需的运行和开发环境。
下面是一个最简单的package.json文件,只定义两项元数据:项目名称和项目版本。
```javascript
{
"name" : "xxx",
"version" : "0.0.0",
}
```
上面代码说明,`package.json`文件内部就是一个JSON对象,该对象的每一个成员就是当前项目的一项设置。比如`name`就是项目名称,`version`是版本(遵守“大版本.次要版本.小版本”的格式)。
下面是一个更完整的package.json文件。
```javascript
{
"name": "Hello World",
"version": "0.0.1",
"author": "张三",
"description": "第一个node.js程序",
"keywords":["node.js","javascript"],
"repository": {
"type": "git",
"url": "https://path/to/url"
},
"license":"MIT",
"engines": {"node": "0.10.x"},
"bugs":{"url":"http://path/to/bug","email":"bug@example.com"},
"contributors":[{"name":"李四","email":"lisi@example.com"}],
"scripts": {
"start": "node index.js"
},
"dependencies": {
"express": "latest",
"mongoose": "~3.8.3",
"handlebars-runtime": "~1.0.12",
"express3-handlebars": "~0.5.0",
"MD5": "~1.2.0"
},
"devDependencies": {
"bower": "~1.2.8",
"grunt": "~0.4.1",
"grunt-contrib-concat": "~0.3.0",
"grunt-contrib-jshint": "~0.7.2",
"grunt-contrib-uglify": "~0.2.7",
"grunt-contrib-clean": "~0.5.0",
"browserify": "2.36.1",
"grunt-browserify": "~1.3.0",
}
}
```
下面详细解释package.json文件的各个字段。
## scripts字段
`scripts`指定了运行脚本命令的npm命令行缩写,比如start指定了运行`npm run start`时,所要执行的命令。
下面的设置指定了`npm run preinstall`、`npm run postinstall`、`npm run start`、`npm run test`时,所要执行的命令。
```javascript
"scripts": {
"preinstall": "echo here it comes!",
"postinstall": "echo there it goes!",
"start": "node index.js",
"test": "tap test/*.js"
}
```
## dependencies字段,devDependencies字段
`dependencies`字段指定了项目运行所依赖的模块,`devDependencies`指定项目开发所需要的模块。
它们都指向一个对象。该对象的各个成员,分别由模块名和对应的版本要求组成,表示依赖的模块及其版本范围。
```javascript
{
"devDependencies": {
"browserify": "~13.0.0",
"karma-browserify": "~5.0.1"
}
}
```
对应的版本可以加上各种限定,主要有以下几种:
- **指定版本**:比如`1.2.2`,遵循“大版本.次要版本.小版本”的格式规定,安装时只安装指定版本。
- **波浪号(tilde)+指定版本**:比如`~1.2.2`,表示安装1.2.x的最新版本(不低于1.2.2),但是不安装1.3.x,也就是说安装时不改变大版本号和次要版本号。
- **插入号(caret)+指定版本**:比如ˆ1.2.2,表示安装1.x.x的最新版本(不低于1.2.2),但是不安装2.x.x,也就是说安装时不改变大版本号。需要注意的是,如果大版本号为0,则插入号的行为与波浪号相同,这是因为此时处于开发阶段,即使是次要版本号变动,也可能带来程序的不兼容。
- **latest**:安装最新版本。
package.json文件可以手工编写,也可以使用`npm init`命令自动生成。
```bash
$ npm init
```
这个命令采用互动方式,要求用户回答一些问题,然后在当前目录生成一个基本的package.json文件。所有问题之中,只有项目名称(name)和项目版本(version)是必填的,其他都是选填的。
有了package.json文件,直接使用npm install命令,就会在当前目录中安装所需要的模块。
```bash
$ npm install
```
如果一个模块不在`package.json`文件之中,可以单独安装这个模块,并使用相应的参数,将其写入`package.json`文件之中。
```bash
$ npm install express --save
$ npm install express --save-dev
```
上面代码表示单独安装express模块,`--save`参数表示将该模块写入`dependencies`属性,`--save-dev`表示将该模块写入`devDependencies`属性。
## peerDependencies
有时,你的项目和所依赖的模块,都会同时依赖另一个模块,但是所依赖的版本不一样。比如,你的项目依赖A模块和B模块的1.0版,而A模块本身又依赖B模块的2.0版。
大多数情况下,这不构成问题,B模块的两个版本可以并存,同时运行。但是,有一种情况,会出现问题,就是这种依赖关系将暴露给用户。
最典型的场景就是插件,比如A模块是B模块的插件。用户安装的B模块是1.0版本,但是A插件只能和2.0版本的B模块一起使用。这时,用户要是将1.0版本的B的实例传给A,就会出现问题。因此,需要一种机制,在模板安装的时候提醒用户,如果A和B一起安装,那么B必须是2.0模块。
`peerDependencies`字段,就是用来供插件指定其所需要的主工具的版本。
```javascript
{
"name": "chai-as-promised",
"peerDependencies": {
"chai": "1.x"
}
}
```
上面代码指定,安装`chai-as-promised`模块时,主程序`chai`必须一起安装,而且`chai`的版本必须是`1.x`。如果你的项目指定的依赖是`chai`的2.0版本,就会报错。
注意,从npm 3.0版开始,`peerDependencies`不再会默认安装了。
## bin字段
bin项用来指定各个内部命令对应的可执行文件的位置。
```javascript
"bin": {
"someTool": "./bin/someTool.js"
}
```
上面代码指定,someTool 命令对应的可执行文件为 bin 子目录下的 someTool.js。Npm会寻找这个文件,在`node_modules/.bin/`目录下建立符号链接。在上面的例子中,someTool.js会建立符号链接`npm_modules/.bin/someTool`。由于`node_modules/.bin/`目录会在运行时加入系统的PATH变量,因此在运行npm时,就可以不带路径,直接通过命令来调用这些脚本。
因此,像下面这样的写法可以采用简写。
```javascript
scripts: {
start: './node_modules/someTool/someTool.js build'
}
// 简写为
scripts: {
start: 'someTool build'
}
```
所有`node_modules/.bin/`目录下的命令,都可以用`npm run [命令]`的格式运行。在命令行下,键入`npm run`,然后按tab键,就会显示所有可以使用的命令。
## main字段
`main`字段指定了加载该模块时的入门文件,默认是模块根目录下面的`index.js`。
## config字段
config字段用于向环境变量输出值。
下面是一个package.json文件。
```javascript
{
"name" : "foo",
"config" : { "port" : "8080" },
"scripts" : { "start" : "node server.js" }
}
```
然后,在`server.js`脚本就可以引用config字段的值。
```javascript
http.createServer(...).listen(process.env.npm_package_config_port)
```
用户可以改变这个值。
```bash
$ npm config set foo:port 80
```
## 其他
### browser字段
browser指定该模板供浏览器使用的版本。Browserify这样的浏览器打包工具,通过它就知道该打包那个文件。
```javascript
"browser": {
"tipso": "./node_modules/tipso/src/tipso.js"
},
```
### engines字段
engines指明了该项目所需要的node.js版本。
### man字段
man用来指定当前模块的man文档的位置。
```javascript
"man" :[ "./doc/calc.1" ]
```
### preferGlobal字段
preferGlobal的值是布尔值,表示当用户不将该模块安装为全局模块时(即不用--global参数),要不要显示警告,表示该模块的本意就是安装为全局模块。
### style字段
style指定供浏览器使用时,样式文件所在的位置。样式文件打包工具parcelify,通过它知道样式文件的打包位置。
```javascript
"style": [
"./node_modules/tipso/src/tipso.css"
]
```
<h2 id="12.4">npm模块管理器</h2>
## 简介
npm有两层含义。一层含义是Node.js的开放式模块登记和管理系统,网址为[http://npmjs.org](http://npmjs.org)。另一层含义是Node.js默认的模块管理器,是一个命令行下的软件,用来安装和管理node模块。
npm不需要单独安装。在安装node的时候,会连带一起安装npm。但是,node附带的npm可能不是最新版本,最好用下面的命令,更新到最新版本。
```bash
$ npm install npm@latest -g
```
上面的命令之所以最后一个参数是npm,是因为npm本身也是Node.js的一个模块。
Node安装完成后,可以用下面的命令,查看一下npm的帮助文件。
```bash
# npm命令列表
$ npm help
# 各个命令的简单用法
$ npm -l
```
下面的命令分别查看npm的版本和配置。
```bash
$ npm -v
$ npm config list -l
```
## npm init
`npm init`用来初始化生成一个新的`package.json`文件。它会向用户提问一系列问题,如果你觉得不用修改默认配置,一路回车就可以了。
如果使用了`-f`(代表force)、`-y`(代表yes),则跳过提问阶段,直接生成一个新的`package.json`文件。
```bash
$ npm init -y
```
## npm set
`npm set`用来设置环境变量。
```bash
$ 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`。
```bash
$ npm set save-exact true
```
上面命令设置加入模块时,`package.json`将记录模块的确切版本,而不是一个可选的版本范围。
## npm info
`npm info`命令可以查看每个模块的具体信息。比如,查看underscore模块的信息。
```bash
$ 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命令查询。
```bash
$ 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仓库,它后面可以跟字符串,也可以跟正则表达式。
```bash
$ npm search <搜索词>
```
下面是一个例子。
```bash
$ 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`命令以树型结构列出当前项目安装的所有模块,以及它们依赖的模块。
```bash
$ npm list
```
加上global参数,会列出全局安装的模块。
```bash
$ npm list -global
```
`npm list`命令也可以列出单个模块。
```bash
$ npm list underscore
```
## npm install
### 基本用法
Node模块采用`npm install`命令安装。
每个模块可以“全局安装”,也可以“本地安装”。“全局安装”指的是将一个模块安装到系统目录中,各个项目都可以调用。一般来说,全局安装只适用于工具模块,比如npm和grunt。“本地安装”指的是将一个模块下载到当前项目的`node_modules`子目录,然后只有在项目目录之中,才能调用这个模块。
```bash
# 本地安装
$ npm install <package name>
# 全局安装
$ sudo npm install -global <package name>
$ sudo npm install -g <package name>
```
`npm install`也支持直接输入Github代码库地址。
```bash
$ npm install git://github.com/package/path.git
$ npm install git://github.com/package/path.git#0.1.0
```
安装之前,`npm install`会先检查,`node_modules`目录之中是否已经存在指定模块。如果存在,就不再重新安装了,即使远程仓库已经有了一个新版本,也是如此。
如果你希望,一个模块不管是否安装过,npm 都要强制重新安装,可以使用`-f`或`--force`参数。
```bash
$ npm install <packageName> --force
```
如果你希望,所有模块都要强制重新安装,那就删除`node_modules`目录,重新执行`npm install`。
```bash
$ rm -rf node_modules
$ npm install
```
### 安装不同版本
install命令总是安装模块的最新版本,如果要安装模块的特定版本,可以在模块名后面加上@和版本号。
```bash
$ npm install sax@latest
$ npm install sax@0.1.1
$ npm install sax@">=0.1.0 <0.2.0"
```
如果使用`--save-exact`参数,会在package.json文件指定安装模块的确切版本。
```bash
$ npm install readable-stream --save --save-exact
```
install命令可以使用不同参数,指定所安装的模块属于哪一种性质的依赖关系,即出现在packages.json文件的哪一项中。
- --save:模块名将被添加到dependencies,可以简化为参数`-S`。
- --save-dev: 模块名将被添加到devDependencies,可以简化为参数`-D`。
```bash
$ npm install sax --save
$ npm install node-tap --save-dev
# 或者
$ npm install sax -S
$ npm install node-tap -D
```
如果要安装beta版本的模块,需要使用下面的命令。
```bash
# 安装最新的beta版
$ npm install <module-name>@beta (latest beta)
# 安装指定的beta版
$ npm install <module-name>@1.3.1-beta.3
```
`npm install`默认会安装dependencies字段和devDependencies字段中的所有模块,如果使用production参数,可以只安装dependencies字段的模块。
```bash
$ npm install --production
# 或者
$ NODE_ENV=production npm install
```
一旦安装了某个模块,就可以在代码中用require命令调用这个模块。
```javascript
var backbone = require('backbone')
console.log(backbone.VERSION)
```
## 避免系统权限
默认情况下,Npm全局模块都安装在系统目录(比如`/usr/local/lib/`),普通用户没有写入权限,需要用到`sudo`命令。这不是很方便,我们可以在没有root权限的情况下,安装全局模块。
首先,在主目录下新建配置文件`.npmrc`,然后在该文件中将`prefix`变量定义到主目录下面。
```bash
prefix = /home/yourUsername/npm
```
然后在主目录下新建`npm`子目录。
```bash
$ mkdir ~/npm
```
此后,全局安装的模块都会安装在这个子目录中,npm也会到`~/npm/bin`目录去寻找命令。
最后,将这个路径在`.bash_profile`文件(或`.bashrc`文件)中加入PATH变量。
```bash
export PATH=~/npm/bin:$PATH
```
## npm update,npm uninstall
`npm update`命令可以更新本地安装的模块。
```bash
# 升级当前项目的指定模块
$ npm update [package name]
# 升级全局安装的模块
$ npm update -global [package name]
```
它会先到远程仓库查询最新版本,然后查询本地版本。如果本地版本不存在,或者远程版本较新,就会安装。
使用`-S`或`--save`参数,可以在安装的时候更新`package.json`里面模块的版本号。
```javascript
// 更新之前的package.json
dependencies: {
dep1: "^1.1.1"
}
// 更新之后的package.json
dependencies: {
dep1: "^1.2.2"
}
```
注意,从npm v2.6.1 开始,`npm update`只更新顶层模块,而不更新依赖的依赖,以前版本是递归更新的。如果想取到老版本的效果,要使用下面的命令。
```bash
$ npm --depth 9999 update
```
`npm uninstall`命令,卸载已安装的模块。
```bash
$ npm uninstall [package name]
# 卸载全局模块
$ npm uninstall [package name] -global
```
## npm run
npm不仅可以用于模块管理,还可以用于执行脚本。`package.json`文件有一个`scripts`字段,可以用于指定脚本命令,供npm直接调用。
```javascript
{
"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内置了两个命令简写,`npm test`等同于执行`npm run test`,`npm start`等同于执行`npm run start`。
`npm run`会创建一个Shell,执行指定的命令,并临时将`node_modules/.bin`加入PATH变量,这意味着本地模块可以直接运行。
举例来说,你执行ESLint的安装命令。
```bash
$ npm i eslint --save-dev
```
运行上面的命令以后,会产生两个结果。首先,ESLint被安装到当前目录的`node_modules`子目录;其次,`node_modules/.bin`目录会生成一个符号链接`node_modules/.bin/eslint`,指向ESLint模块的可执行脚本。
然后,你就可以在`package.json`的`script`属性里面,不带路径的引用`eslint`这个脚本。
```javascript
{
"name": "Test Project",
"devDependencies": {
"eslint": "^1.10.3"
},
"scripts": {
"lint": "eslint ."
}
}
```
等到运行`npm run lint`的时候,它会自动执行`./node_modules/.bin/eslint .`。
如果直接运行`npm run`不给出任何参数,就会列出`scripts`属性下所有命令。
```bash
$ npm run
Available scripts in the user-service package:
lint
jshint **.js
test
mocha test/
```
下面是另一个`package.json`文件的例子。
```javascript
"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`项,定义了四个别名,每个别名都有对应的脚本命令。
```bash
$ npm run watch
$ npm run build
$ npm run start
$ npm run test
```
其中,`start`和`test`属于特殊命令,可以省略`run`。
```bash
$ npm start
$ npm test
```
如果希望一个操作的输出,是另一个操作的输入,可以借用Linux系统的管道命令,将两个操作连在一起。
```javascript
"build-js": "browserify browser/main.js | uglifyjs -mc > static/bundle.js"
```
但是,更方便的写法是引用其他`npm run`命令。
```javascript
"build": "npm run build-js && npm run build-css"
```
上面的写法是先运行`npm run build-js`,然后再运行`npm run build-css`,两个命令中间用`&&`连接。如果希望两个命令同时平行执行,它们中间可以用`&`连接。
下面是一个流操作的例子。
```javascript
"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脚本。
```javascript
#!/bin/bash
cd site/main
browserify browser/main.js | uglifyjs -mc > static/bundle.js
```
假定上面的脚本文件名为build.sh,并且权限为可执行,就可以在scripts属性中引用该文件。
```javascript
"build-js": "bin/build.sh"
```
### 参数
`npm run`命令还可以添加参数。
```javascript
"scripts": {
"test": "mocha test/"
}
```
上面代码指定`npm test`,实际运行`mocha test/`。如果要通过`npm test`命令,将参数传到mocha,则参数之前要加上两个连词线。
```bash
$ npm run test -- anothertest.js
# 等同于
$ mocha test/ anothertest.js
```
上面命令表示,mocha要运行所有`test`子目录的测试脚本,以及另外一个测试脚本`anothertest.js`。
`npm run`本身有一个参数`-s`,表示关闭npm本身的输出,只输出脚本产生的结果。
```bash
// 输出npm命令头
$ npm run test
// 不输出npm命令头
$ npm run -s test
```
### scripts脚本命令最佳实践
`scripts`字段的脚本命令,有一些最佳实践,可以方便开发。首先,安装`npm-run-all`模块。
```bash
$ npm install npm-run-all --save-dev
```
这个模块用于运行多个`scripts`脚本命令。
```bash
# 继发执行
$ 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`脚本命令,用于启动应用程序。
```javascript
"start": "npm-run-all --parallel dev serve"
```
上面命令并行执行`dev`脚本命令和`serve`脚本命令,等同于下面的形式。
```bash
$ npm run dev & npm run serve
```
如果start脚本没有配置,`npm start`命令默认执行下面的脚本,前提是模块的根目录存在一个server.js文件。
```bash
$ node server.js
```
(2)dev脚本命令
`dev`脚本命令,规定开发阶段所要做的处理,比如构建网页资源。
```javascript
"dev": "npm-run-all dev:*"
```
上面命令用于继发执行所有`dev`的子命令。
```javascript
"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文件。
```javascript
"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文件。
```javascript
"dev:autoprefix": "postcss --use autoprefixer --autoprefixer.browsers \"> 5%\" --output src/css/hoodie.css src/css/hoodie.css"
```
上面命令为css文件加上浏览器前缀,限制条件是只考虑市场份额大于5%的浏览器。
(3)serve脚本命令
`serve`脚本命令用于启动服务。
```javascript
"serve": "live-server dist/ --port=9090"
```
上面命令启动服务,用的是[live-server](http://npmjs.com/package/live-server)模块,将服务启动在9090端口,展示`dist`子目录。
`live-server`模块有三个功能。
- 启动一个HTTP服务器,展示指定目录的`index.html`文件,通过该文件加载各种网络资源,这是`file://`协议做不到的。
- 添加自动刷新功能。只要指定目录之中,文件有任何变化,它就会刷新页面。
- `npm run serve`命令执行以后,自动打开浏览器。、
以前,上面三个功能需要三个模块来完成:`http-server`、`live-reload`和`opener`,现在只要`live-server`一个模块就够了。
(4)test脚本命令
`test`脚本命令用于执行测试。
```javascript
"test": "npm-run-all test:*",
"test:lint": "sass-lint --verbose --config .sass-lint.yml src/sass/*"
```
上面命令规定,执行测试时,运行`lint`脚本,检查脚本之中的语法错误。
(5)prod脚本命令
`prod`脚本命令,规定进入生产环境时需要做的处理。
```javascript
"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`脚本命令用于展示帮助信息。
```javascript
"help": "markdown-chalk --input DEVELOPMENT.md"
```
上面命令之中,`markdown-chalk`模块用于将指定的markdown文件,转为彩色文本显示在终端之中。
(7)docs脚本命令
`docs`脚本命令用于生成文档。
```javascript
"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`。
```javascript
{
"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`
1. `test`
1. `posttest`
如果执行过程出错,就不会执行排在后面的脚本,即如果prelint脚本执行出错,就不会接着执行lint和postlint脚本。
下面是一个例子。
```javascript
{
"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`脚本,也可以手动执行它们。
```bash
$ npm run prepublish
```
下面是`post install`的例子。
```javascript
{
"postinstall": "node lib/post_install.js"
}
```
上面的这个命令,主要用于处理从Git仓库拉下来的源码。比如,有些源码是用TypeScript写的,可能需要转换一下。
下面是`publish`钩子的一个例子。
```javascript
{
"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](https://github.com/typicode/husky)模块为例。
```bash
$ npm install husky --save-dev
```
安装以后,就能在`package.json`添加`precommit`、`prepush`等钩子。
```javascript
{
"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。
```javascript
{
"scripts":{
"bundle": "mkdir -p build/$npm_package_version/"
}
}
```
运行`npm run bundle`以后,将会生成`build/1.2.5/`子目录。
`config`字段也可以用于设置内部字段。
```javascript
"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个或多个子目录。
## npm link
开发Npm模块的时候,有时我们会希望,边开发边试用。但是,常规情况下,使用一个模块,需要将其安装到`node_modules`目录之中,这对于开发中的模块,显然非常不方便。`npm link`就能起到这个作用,建立一个符号链接,在全局的`node_modules`目录之中,生成一个符号链接,指向模块的本地目录。
为了理解`npm link`,请设想这样一个场景。你开发了一个模块`myModule`,目录为`src/myModule`,你自己的项目`myProject`要用到这个模块,项目目录为`src/myProject`。每一次,你更新`myModul`e,就要用`npm publish`命令发布,然后切换到项目目录,使用`npm update`更新模块。这样显然很不方便,如果我们可以从项目目录建立一个符号链接,直接连到模块目录,就省去了中间步骤,项目可以直接使用最新版的模块。
首先,在模块目录(`src/myModule`)下运行`npm link`命令。
```bash
src/myModule$ npm link
```
上面的命令会在Npm的全局模块目录内,生成一个符号链接文件,该文件的名字就是`package.json`文件中指定的文件名。
```bash
/path/to/global/node_modules/myModule -> src/myModule
```
这个时候,已经可以全局调用`myModule`模块了。但是,如果我们要让这个模块安装在项目内,还要进行下面的步骤。
切换到项目目录,再次运行`npm link`命令,并指定模块名。
```bash
src/myProject$ npm link myModule
```
上面命令等同于生成了本地模块的符号链接。
```bash
src/myProject/node_modules/myModule -> /path/to/global/node_modules/myModule
```
然后,就可以在你的项目中,加载该模块了。
```javascript
var myModule = require('myModule');
```
这样一来,`myModule`的任何变化,都可以直接反映在`myProject`项目之中。但是,这样也出现了风险,任何在`myProject`目录中对`myModule`的修改,都会反映到模块的源码中。
如果你的项目不再需要该模块,可以在项目目录内使用`npm unlink`命令,删除符号链接。
```bash
src/myProject$ npm unlink myModule
```
## npm bin
`npm bin`命令显示相对于当前目录的,Node模块的可执行脚本所在的目录(即`.bin`目录)。
```bash
# 项目根目录下执行
$ npm bin
./node_modules/.bin
```
## npm adduser
`npm adduser`用于在npmjs.com注册一个用户。
```bash
$ npm adduser
Username: YOUR_USER_NAME
Password: YOUR_PASSWORD
Email: YOUR_EMAIL@domain.com
```
## npm publish
`npm publish`用于将当前模块发布到`npmjs.com`。执行之前,需要向`npmjs.com`申请用户名。
```bash
$ npm adduser
```
如果已经注册过,就使用下面的命令登录。
```bash
$ npm login
```
登录以后,就可以使用`npm publish`命令发布。
```bash
$ npm publish
```
如果当前模块是一个beta版,比如`1.3.1-beta.3`,那么发布的时候需要使用`tag`参数。
```bash
$ npm publish --tag beta
```
如果发布私有模块,模块初始化的时候,需要加上`scope`参数。只有npm的付费用户才能发布私有模块。
```bash
$ npm init --scope=<yourscope>
```
如果你的模块是用ES6写的,那么发布的时候,最好转成ES5。首先,需要安装Babel。
```javascript
$ npm install --save-dev babel-cli@6 babel-preset-es2015@6
```
然后,在`package.json`里面写入`build`脚本。
```javascript
"scripts": {
"build": "babel source --presets babel-preset-es2015 --out-dir distribution",
"prepublish": "npm run build"
}
```
运行上面的脚本,会将`source`目录里面的ES6源码文件,转为`distribution`目录里面的ES5源码文件。然后,在项目根目录下面创建两个文件`.npmignore`和`.gitignore`,分别写入以下内容。
```javascrip
// .npmignore
source
// .gitignore
node_modules
distribution
```
## npm deprecate
如果想废弃某个版本的模块,可以使用`npm deprecate`命令。
```bash
$ npm deprecate my-thing@"< 0.2.3" "critical bug fixed in v0.2.3"
```
运行上面的命令以后,小于`0.2.3`版本的模块的`package.json`都会写入一行警告,用户安装这些版本时,这行警告就会在命令行显示。
## npm owner
模块的维护者可以发布新版本。`npm owner`命令用于管理模块的维护者。
```bash
# 列出指定模块的维护者
$ npm owner ls <package name>
# 新增维护者
$ npm owner add <user> <package name>
# 删除维护者
$ npm owner rm <user> <package name>
```
<h2 id="12.5">fs 模块</h2>
fs是filesystem的缩写,该模块提供本地文件的读写能力,基本上是POSIX文件操作命令的简单包装。但是,这个模块几乎对所有操作提供异步和同步两种操作方式,供开发者选择。
## readFileSync()
readFileSync方法用于同步读取文件,返回一个字符串。
```javascript
var text = fs.readFileSync(fileName, "utf8");
// 将文件按行拆成数组
text.split(/\r?\n/).forEach(function (line) {
// ...
});
```
该方法的第一个参数是文件路径,第二个参数是文本文件编码,默认为utf8。
不同系统的行结尾字符不同,可以用下面的方法判断。
```javascript
// 方法一,查询现有的行结尾字符
var EOL = fileContents.indexOf("\r\n") >= 0 ? "\r\n" : "\n";
// 方法二,根据当前系统处理
var EOL = (process.platform === 'win32' ? '\r\n' : '\n')
```
## writeFileSync()
writeFileSync方法用于同步写入文件。
```
fs.writeFileSync(fileName, str, 'utf8');
```
它的第一个参数是文件路径,第二个参数是写入文件的字符串,第三个参数是文件编码,默认为utf8。
## exists(path, callback)
exists方法用来判断给定路径是否存在,然后不管结果如何,都会调用回调函数。
```javascript
fs.exists('/path/to/file', function (exists) {
util.debug(exists ? "it's there" : "no file!");
});
```
上面代码表明,回调函数的参数是一个表示文件是否存在的布尔值。
需要注意的是,不要在open方法之前调用exists方法,open方法本身就能检查文件是否存在。
下面的例子是如果给定目录存在,就删除它。
```javascript
if(fs.exists(outputFolder)) {
console.log("Removing "+outputFolder);
fs.rmdir(outputFolder);
}
```
## mkdir(),writeFile(),readfile()
mkdir方法用于新建目录。
```javascript
var fs = require('fs');
fs.mkdir('./helloDir',0777, function (err) {
if (err) throw err;
});
```
mkdir接受三个参数,第一个是目录名,第二个是权限值,第三个是回调函数。
writeFile方法用于写入文件。
```javascript
var fs = require('fs');
fs.writeFile('./helloDir/message.txt', 'Hello Node', function (err) {
if (err) throw err;
console.log('文件写入成功');
});
```
readfile方法用于读取文件内容。
```javascript
var fs = require('fs');
fs.readFile('./helloDir/message.txt','UTF-8' ,function (err, data) {
if (err) throw err;
console.log(data);
});
```
上面代码使用readFile方法读取文件。readFile方法的第一个参数是文件名,第二个参数是文件编码,第三个参数是回调函数。可用的文件编码包括“ascii”、“utf8”和“base64”。如果没有指定文件编码,返回的是原始的缓存二进制数据,这时需要调用buffer对象的toString方法,将其转为字符串。
```javascript
var fs = require('fs');
fs.readFile('example_log.txt', function (err, logData) {
if (err) throw err;
var text = logData.toString();
});
```
readFile方法是异步操作,所以必须小心,不要同时发起多个readFile请求。
```js
for(var i = 1; i <= 1000; i++) {
fs.readFile('./'+i+'.txt', function() {
// do something with the file
});
}
```
上面代码会同时发起1000个readFile异步请求,很快就会耗尽系统资源。
## mkdirSync(),writeFileSync(),readFileSync()
这三个方法是建立目录、写入文件、读取文件的同步版本。
```javascript
fs.mkdirSync('./helloDirSync',0777);
fs.writeFileSync('./helloDirSync/message.txt', 'Hello Node');
var data = fs.readFileSync('./helloDirSync/message.txt','UTF-8');
console.log('file created with contents:');
console.log(data);
```
对于流量较大的服务器,最好还是采用异步操作,因为同步操作时,只有前一个操作结束,才会开始后一个操作,如果某个操作特别耗时(常常发生在读写数据时),会导致整个程序停顿。
## readdir()
readdir方法用于读取目录,返回一个所包含的文件和子目录的数组。
```javascript
fs.readdir(process.cwd(), function (err, files) {
if (err) {
console.log(err);
return;
}
var count = files.length;
var results = {};
files.forEach(function (filename) {
fs.readFile(filename, function (data) {
results[filename] = data;
count--;
if (count <= 0) {
// 对所有文件进行处理
}
});
});
});
```
## stat()
stat方法的参数是一个文件或目录,它产生一个对象,该对象包含了该文件或目录的具体信息。我们往往通过该方法,判断正在处理的到底是一个文件,还是一个目录。
```javascript
var fs = require('fs');
fs.readdir('/etc/', function (err, files) {
if (err) throw err;
files.forEach( function (file) {
fs.stat('/etc/' + file, function (err, stats) {
if (err) throw err;
if (stats.isFile()) {
console.log("%s is file", file);
}
else if (stats.isDirectory ()) {
console.log("%s is a directory", file);
}
console.log('stats: %s',JSON.stringify(stats));
});
});
});
```
## watchfile(),unwatchfile()
watchfile方法监听一个文件,如果该文件发生变化,就会自动触发回调函数。
```javascript
var fs = require('fs');
fs.watchFile('./testFile.txt', function (curr, prev) {
console.log('the current mtime is: ' + curr.mtime);
console.log('the previous mtime was: ' + prev.mtime);
});
fs.writeFile('./testFile.txt', "changed", function (err) {
if (err) throw err;
console.log("file write complete");
});
```
unwatchfile方法用于解除对文件的监听。
## createReadStream()
createReadStream方法往往用于打开大型的文本文件,创建一个读取操作的数据流。所谓大型文本文件,指的是文本文件的体积很大,读取操作的缓存装不下,只能分成几次发送,每次发送会触发一个data事件,发送结束会触发end事件。
```javascript
var fs = require('fs');
function readLines(input, func) {
var remaining = '';
input.on('data', function(data) {
remaining += data;
var index = remaining.indexOf('\n');
var last = 0;
while (index > -1) {
var line = remaining.substring(last, index);
last = index + 1;
func(line);
index = remaining.indexOf('\n', last);
}
remaining = remaining.substring(last);
});
input.on('end', function() {
if (remaining.length > 0) {
func(remaining);
}
});
}
function func(data) {
console.log('Line: ' + data);
}
var input = fs.createReadStream('lines.txt');
readLines(input, func);
```
## createWriteStream()
createWriteStream方法创建一个写入数据流对象,该对象的write方法用于写入数据,end方法用于结束写入操作。
```javascript
var out = fs.createWriteStream(fileName, { encoding: "utf8" });
out.write(str);
out.end();
```
<h2 id="12.6">Path模块</h2>
## path.join()
`path.join`方法用于连接路径。该方法的主要用途在于,会正确使用当前系统的路径分隔符,Unix系统是”/“,Windows系统是”\“。
```javascript
var path = require('path');
path.join(mydir, "foo");
```
上面代码在Unix系统下,会返回路径`mydir/foo`。
## path.resolve()
`path.resolve`方法用于将相对路径转为绝对路径。
它可以接受多个参数,依次表示所要进入的路径,直到将最后一个参数转为绝对路径。如果根据参数无法得到绝对路径,就以当前所在路径作为基准。除了根目录,该方法的返回值都不带尾部的斜杠。
```javascript
// 格式
path.resolve([from ...], to)
// 实例
path.resolve('foo/bar', '/tmp/file/', '..', 'a/../subfile')
```
上面代码的实例,执行效果类似下面的命令。
```bash
$ cd foo/bar
$ cd /tmp/file/
$ cd ..
$ cd a/../subfile
$ pwd
```
更多例子。
```javascript
path.resolve('/foo/bar', './baz')
// '/foo/bar/baz'
path.resolve('/foo/bar', '/tmp/file/')
// '/tmp/file'
path.resolve('wwwroot', 'static_files/png/', '../gif/image.gif')
// 如果当前目录是/home/myself/node,返回
// /home/myself/node/wwwroot/static_files/gif/image.gif
```
该方法忽略非字符串的参数。
## accessSync()
`accessSync`方法用于同步读取一个路径。
下面的代码可以用于判断一个目录是否存在。
```javascript
function exists(pth, mode) {
try {
fs.accessSync(pth, mode);
return true;
} catch (e) {
return false;
}
}
```
## path.relative
`path.relative`方法接受两个参数,这两个参数都应该是绝对路径。该方法返回第二个路径想对于地一个路径的系那个相对路径。
```javascript
path.relative('/data/orandea/test/aaa', '/data/orandea/impl/bbb')
// '../../impl/bbb'
```
上面代码中,如果当前目录是`/data/orandea/test/aaa`,进入`path.relative`返回的相对路径,就会到达`/data/orandea/impl/bbb`。
如果`path.relative`方法的两个参数相同,则返回一个空字符串。
<h2 id="12.7">process对象</h2>
`process`对象是Node的一个全局对象,提供当前Node进程的信息。它可以在脚本的任意位置使用,不必通过`require`命令加载。该对象部署了`EventEmitter`接口。
## 进程信息
通过`process`对象,可以获知当前进程的很多信息。
### 退出码
进程退出时,会返回一个整数值,表示退出时的状态。这个整数值就叫做退出码。下面是常见的Node进程退出码。
- 0,正常退出
- 1,发生未捕获错误
- 5,V8执行错误
- 8,不正确的参数
- 128 + 信号值,如果Node接受到退出信号(比如SIGKILL或SIGHUP),它的退出码就是128加上信号值。由于128的二进制形式是10000000, 所以退出码的后七位就是信号值。
## 属性
process对象提供一系列属性,用于返回系统信息。
- **process.argv**:返回当前进程的命令行参数数组。
- **process.env**:返回一个对象,成员为当前Shell的环境变量,比如`process.env.HOME`。
- **process.installPrefix**:node的安装路径的前缀,比如`/usr/local`,则node的执行文件目录为`/usr/local/bin/node`。
- **process.pid**:当前进程的进程号。
- **process.platform**:当前系统平台,比如Linux。
- **process.title**:默认值为“node”,可以自定义该值。
- **process.version**:Node的版本,比如v0.10.18。
下面是主要属性的介绍。
### stdout,stdin,stderr
以下属性指向系统IO。
**(1)stdout**
stdout属性指向标准输出(文件描述符1)。它的write方法等同于console.log,可用在标准输出向用户显示内容。
```javascript
console.log = function(d) {
process.stdout.write(d + '\n');
};
```
下面代码表示将一个文件导向标准输出。
```javascript
var fs = require('fs');
fs.createReadStream('wow.txt')
.pipe(process.stdout);
```
上面代码中,由于process.stdout和process.stdin与其他进程的通信,都是流(stream)形式,所以必须通过pipe管道命令中介。
```javascript
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('wow.txt')
.pipe(zlib.createGzip())
.pipe(process.stdout);
```
上面代码通过pipe方法,先将文件数据压缩,然后再导向标准输出。
**(2)stdin**
stdin代表标准输入(文件描述符0)。
```javascript
process.stdin.pipe(process.stdout)
```
上面代码表示将标准输入导向标准输出。
由于stdin和stdout都部署了stream接口,所以可以使用stream接口的方法。
```javascript
process.stdin.setEncoding('utf8');
process.stdin.on('readable', function() {
var chunk = process.stdin.read();
if (chunk !== null) {
process.stdout.write('data: ' + chunk);
}
});
process.stdin.on('end', function() {
process.stdout.write('end');
});
```
**(3)stderr**
stderr属性指向标准错误(文件描述符2)。
### argv,execPath,execArgv
argv属性返回一个数组,由命令行执行脚本时的各个参数组成。它的第一个成员总是node,第二个成员是脚本文件名,其余成员是脚本文件的参数。
请看下面的例子,新建一个脚本文件argv.js。
```javascript
// argv.js
console.log("argv: ",process.argv);
```
在命令行下调用这个脚本,会得到以下结果。
```javascript
$ node argv.js a b c
[ 'node', '/path/to/argv.js', 'a', 'b', 'c' ]
```
上面代码表示,argv返回数组的成员依次是命令行的各个部分,真正的参数实际上是从`process.argv[2]`开始。要得到真正的参数部分,可以把argv.js改写成下面这样。
```javascript
// argv.js
var myArgs = process.argv.slice(2);
console.log(myArgs);
```
execPath属性返回执行当前脚本的Node二进制文件的绝对路径。
```javascript
> process.execPath
'/usr/local/bin/node'
>
```
execArgv属性返回一个数组,成员是命令行下执行脚本时,在Node可执行文件与脚本文件之间的命令行参数。
```bash
# script.js的代码为
# console.log(process.execArgv);
$ node --harmony script.js --version
```
### process.env
`process.env`属性返回一个对象,包含了当前Shell的所有环境变量。比如,`process.env.HOME`返回用户的主目录。
通常的做法是,新建一个环境变量`NODE_ENV`,用它确定当前所处的开发阶段,生产阶段设为`production`,开发阶段设为`develop`或`staging`,然后在脚本中读取`process.env.NODE_ENV`即可。
运行脚本时,改变环境变量,可以采用下面的写法。
```bash
$ export NODE_ENV=production && node app.js
# 或者
$ NODE_ENV=production node app.js
```
## 方法
process对象提供以下方法:
- **process.chdir()**:切换工作目录到指定目录。
- **process.cwd()**:返回运行当前脚本的工作目录的路径。
- **process.exit()**:退出当前进程。
- **process.getgid()**:返回当前进程的组ID(数值)。
- **process.getuid()**:返回当前进程的用户ID(数值)。
- **process.nextTick()**:指定回调函数在当前执行栈的尾部、下一次Event Loop之前执行。
- **process.on()**:监听事件。
- **process.setgid()**:指定当前进程的组,可以使用数字ID,也可以使用字符串ID。
- **process.setuid()**:指定当前进程的用户,可以使用数字ID,也可以使用字符串ID。
### process.cwd(),process.chdir()
`cwd`方法返回进程的当前目录(绝对路径),`chdir`方法用来切换目录。
```bash
> process.cwd()
'/home/aaa'
> process.chdir('/home/bbb')
> process.cwd()
'/home/bbb'
```
注意,`process.cwd()`与`__dirname`的区别。前者进程发起时的位置,后者是脚本的位置,两者可能是不一致的。比如,`node ./code/program.js`,对于`process.cwd()`来说,返回的是当前目录(`.`);对于`__dirname`来说,返回是脚本所在目录,即`./code/program.js`。
## process.nextTick()
`process.nextTick`将任务放到当前一轮事件循环(Event Loop)的尾部。
```bash
process.nextTick(function () {
console.log('下一次Event Loop即将开始!');
});
```
上面代码可以用`setTimeout(f,0)`改写,效果接近,但是原理不同。
```bash
setTimeout(function () {
console.log('已经到了下一轮Event Loop!');
}, 0)
```
`setTimeout(f,0)`是将任务放到下一轮事件循环的头部,因此`nextTick`会比它先执行。另外,`nextTick`的效率更高,因为不用检查是否到了指定时间。
根据Node的事件循环的实现,基本上,进入下一轮事件循环后的执行顺序如下。
1. `setTimeout(f,0)`
1. 各种到期的回调函数
1. `process.nextTick`
### process.exit()
`process.exit`方法用来退出当前进程,它可以接受一个数值参数。如果参数大于0,表示执行失败;如果等于0表示执行成功。
```bash
if (err) {
process.exit(1);
} else {
process.exit(0);
}
```
`process.exit()`执行时,会触发`exit`事件。
### process.on()
`process.on`方法用来监听各种事件,并指定回调函数。
```javascript
process.on('uncaughtException', function(err){
console.log('got an error: %s', err.message);
process.exit(1);
});
setTimeout(function(){
throw new Error('fail');
}, 100);
```
上面代码是`process`监听Node的一个全局性事件`uncaughtException`,只要有错误没有捕获,就会触发这个事件。
process支持的事件有以下一些。
- data事件:数据输出输入时触发
- SIGINT事件:接收到系统信号时触发
```javascript
process.on('SIGINT', function () {
console.log('Got a SIGINT. Goodbye cruel world');
process.exit(0);
});
```
使用时,向该进程发出系统信号,就会导致进程退出。
```bash
$ kill -s SIGINT [process_id]
```
SIGTERM信号表示内核要求当前进程停止,进程可以自行停止,也可以忽略这个信号。
```javascript
var http = require('http');
var server = http.createServer(function (req, res) {
});
process.on('SIGTERM', function () {
server.close(function () {
process.exit(0);
});
});
```
上面代码表示,进程接到SIGTERM信号之后,关闭服务器,然后退出进程。需要注意的是,这时进程不会马上退出,而是要回应完最后一个请求,处理完所有回调函数,然后再退出。
### process.kill()
process.kill方法用来对指定ID的线程发送信号,默认为SIGINT信号。
```javascript
process.on('SIGTERM', function(){
console.log('terminating');
process.exit(1);
});
setTimeout(function(){
console.log('sending SIGTERM to process %d', process.pid);
process.kill(process.pid, 'SIGTERM');
}, 500);
setTimeout(function(){
console.log('never called');
}, 1000);
```
上面代码中,500毫秒后向当前进程发送SIGTERM信号(终结进程),因此1000毫秒后的指定事件不会被触发。
## 事件
### exit事件
当前进程退出时,会触发`exit`事件,可以对该事件指定回调函数。
```javascript
process.on('exit', function () {
fs.writeFileSync('/tmp/myfile', '需要保存到硬盘的信息');
});
```
下面是一个例子,进程退出时,显示一段日志。
```javascript
process.on("exit", code =>
console.log("exiting with code: " + code))
```
注意,此时回调函数只能执行同步操作,不能包含异步操作,因为执行完回调函数,进程就会退出,无法监听到回调函数的操作结果。
```javascript
process.on('exit', function(code) {
// 不会执行
setTimeout(function() {
console.log('This will not run');
}, 0);
});
```
上面代码在`exit`事件的回调函数里面,指定了一个下一轮事件循环,所要执行的操作。这是无效的,不会得到执行。
### beforeExit事件
beforeExit事件在Node清空了Event Loop以后,再没有任何待处理的任务时触发。正常情况下,如果没有任何待处理的任务,Node进程会自动退出,设置beforeExit事件的监听函数以后,就可以提供一个机会,再部署一些任务,使得Node进程不退出。
beforeExit事件与exit事件的主要区别是,beforeExit的监听函数可以部署异步任务,而exit不行。
此外,如果是显式终止程序(比如调用process.exit()),或者因为发生未捕获的错误,而导致进程退出,这些场合不会触发beforeExit事件。因此,不能使用该事件替代exit事件。
### uncaughtException事件
当前进程抛出一个没有被捕捉的错误时,会触发`uncaughtException`事件。
```javascript
process.on('uncaughtException', function (err) {
console.error('An uncaught error occurred!');
console.error(err.stack);
throw new Error('未捕获错误');
});
```
部署`uncaughtException`事件的监听函数,是免于Node进程终止的最后措施,否则Node就要执行`process.exit()`。出于除错的目的,并不建议发生错误后,还保持进程运行。
抛出错误之前部署的异步操作,还是会继续执行。只有完成以后,Node进程才会退出。
```javascript
process.on('uncaughtException', function(err) {
console.log('Caught exception: ' + err);
});
setTimeout(function() {
console.log('本行依然执行');
}, 500);
// 下面的表达式抛出错误
nonexistentFunc();
```
上面代码中,抛出错误之后,此前setTimeout指定的回调函数亦然会执行。
### 信号事件
操作系统内核向Node进程发出信号,会触发信号事件。实际开发中,主要对SIGTERM和SIGINT信号部署监听函数,这两个信号在非Windows平台会导致进程退出,但是只要部署了监听函数,Node进程收到信号后就不会退出。
```javascript
// 读取标准输入,这主要是为了不让当前进程退出
process.stdin.resume();
process.on('SIGINT', function() {
console.log('SIGINT信号,按Control-D退出');
});
```
上面代码部署了SIGINT信号的监听函数,当用户按下Ctrl-C后,会显示提示文字。
<h2 id="12.8">Buffer对象</h2>
## 概述
Buffer对象是Node.js用来处理二进制数据的一个接口。JavaScript比较擅长处理Unicode数据,对于处理二进制格式的数据(比如TCP数据流),就不太擅长。Buffer对象就是为了解决这个问题而提供的。该对象也是一个构造函数,它的实例代表了V8引擎分配的一段内存,基本上是一个数组,成员都为整数值。
Buffer是Node原生提供的全局对象,可以直接使用,不需要`require('buffer')`。
Buffer对象与字符串的互相转换,需要指定编码格式。目前,Buffer对象支持以下编码格式。
- ascii
- utf8
- utf16le:UTF-16的小头编码,支持大于U+10000的四字节字符。
- ucs2:utf16le的别名。
- base64
- hex:将每个字节转为两个十六进制字符。
V8引擎将Buffer对象占用的内存,解释为一个整数数组,而不是二进制数组。所以,`new Uint32Array(new Buffer([1, 2, 3, 4]))`,生成的`Uint32Array`数组是一个4个成员的`Uint32Array`数组,而不是只有单个成员(`[0x1020304]`或者`[0x4030201]`)。
注意,这时二进制数组所对应的内存是从Buffer对象拷贝的,而不是共享的。二进制数组的`buffer`属性,保留指向原Buffer对象的指针。
二进制数组的操作,与Buffer对象的操作基本上是兼容的,只有轻微的差异。比如,二进制数组的`slice`方法返回原内存的拷贝,而Buffer对象的`slice`方法创造原内存的一个视图(view)。
## Buffer构造函数
Buffer作为构造函数,可以用`new`命令生成一个实例,它可以接受多种形式的参数。
```javascript
// 参数是整数,指定分配多少个字节内存
var hello = new Buffer(5);
// 参数是数组,数组成员必须是整数值
var hello = new Buffer([0x48, 0x65, 0x6c, 0x6c, 0x6f]);
hello.toString() // 'Hello'
// 参数是字符串(默认为utf8编码)
var hello = new Buffer('Hello');
// 参数是字符串(不省略编码)
var hello = new Buffer('Hello', 'utf8');
// 参数是另一个Buffer实例,等同于拷贝后者
var hello1 = new Buffer('Hello');
var hello2 = new Buffer(hello1);
```
下面是读取用户命令行输入的例子。
```javascript
var fs = require('fs');
var buffer = new Buffer(1024);
var readSize = fs.readSync(fs.openSync('/dev/tty', 'r'), buffer, 0, bufferSize);
var chunk = buffer.toString('utf8', 0, readSize);
console.log('INPUT: ' + chunk);
```
运行上面的程序结果如下。
```bash
# 输入任意内容,然后按回车键
foo
INPUT: foo
```
## 类的方法
### Buffer.isEncoding()
Buffer.isEncoding方法返回一个布尔值,表示Buffer实例是否为指定编码。
```javascript
Buffer.isEncoding('utf8')
// true
```
### Buffer.isBuffer()
Buffer.isBuffer方法接受一个对象作为参数,返回一个布尔值,表示该对象是否为Buffer实例。
```javascript
Buffer.isBuffer(Date) // false
```
### Buffer.byteLength()
Buffer.byteLength方法返回字符串实际占据的字节长度,默认编码方式为utf8。
```javascript
Buffer.byteLength('Hello', 'utf8') // 5
```
### Buffer.concat()
Buffer.concat方法将一组Buffer对象合并为一个Buffer对象。
```javascript
var i1 = new Buffer('Hello');
var i2 = new Buffer(' ');
var i3 = new Buffer('World');
Buffer.concat([i1, i2, i3]).toString()
// 'Hello World'
```
需要注意的是,如果Buffer.concat的参数数组只有一个成员,就直接返回该成员。如果有多个成员,就返回一个多个成员合并的新Buffer对象。
Buffer.concat方法还可以接受第二个参数,指定合并后Buffer对象的总长度。
```javascript
var i1 = new Buffer('Hello');
var i2 = new Buffer(' ');
var i3 = new Buffer('World');
Buffer.concat([i1, i2, i3], 10).toString()
// 'Hello Worl'
```
省略第二个参数时,Node内部会计算出这个值,然后再据此进行合并运算。因此,显式提供这个参数,能提供运行速度。
## 实例属性
### length
length属性返回Buffer对象所占据的内存长度。注意,这个值与Buffer对象的内容无关。
```javascript
buf = new Buffer(1234);
buf.length // 1234
buf.write("some string", 0, "ascii");
buf.length // 1234
```
上面代码中,不管写入什么内容,length属性总是返回Buffer对象的空间长度。如果想知道一个字符串所占据的字节长度,可以将其传入Buffer.byteLength方法。
length属性是可写的,但是这会导致未定义的行为,不建议使用。如果想修改Buffer对象的长度,建议使用slice方法返回一个新的Buffer对象。
## 实例方法
### write()
write方法可以向指定的Buffer对象写入数据。它的第一个参数是所写入的内容,第二个参数(可省略)是所写入的起始位置(从0开始),第三个参数(可省略)是编码方式,默认为utf8。
```javascript
var buf = new Buffer(5);
buf.write('He');
buf.write('l', 2);
buf.write('lo', 3);
console.log(buf.toString());
// "Hello"
```
### slice()
slice方法返回一个按照指定位置、从原对象切割出来的Buffer实例。它的两个参数分别为切割的起始位置和终止位置。
```javascript
var buf = new Buffer('just some data');
var chunk = buf.slice(5, 9);
chunk.toString()
// "some"
```
### toString()
toString方法将Buffer对象,按照指定编码(默认为utf8)转为字符串。
```javascript
var hello = new Buffer('Hello');
hello // <Buffer 48 65 6c 6c 6f>
hello.toString() // "Hello"
```
`toString`方法可以只返回指定位置内存的内容,它的第二个参数表示起始位置,第三个参数表示终止位置,两者都是从0开始计算。
```javascript
var buf = new Buffer('just some data');
console.log(buf.toString('ascii', 5, 9));
// "some"
```
### toJSON()
toJSON方法将Buffer实例转为JSON对象。如果JSON.stringify方法调用Buffer实例,默认会先调用toJSON方法。
```javascript
var buf = new Buffer('test');
var json = JSON.stringify(buf);
json // '[116,101,115,116]'
var copy = new Buffer(JSON.parse(json));
copy // <Buffer 74 65 73 74>
```
<h2 id="12.9">Events模块</h2>
## 概述
### 基本用法
`Events`模块是Node对“发布/订阅”模式(publish/subscribe)的实现。一个对象通过这个模块,向另一个对象传递消息。
该模块通过`EventEmitter`属性,提供了一个构造函数。该构造函数的实例具有on方法,可以用来监听指定事件,并触发回调函数。任意对象都可以发布指定事件,被`EventEmitter`实例的`on`方法监听到。
```javascript
var EventEmitter = require('events').EventEmitter;
var ee = new EventEmitter();
ee.on('someEvent', function () {
console.log('event has occured');
});
function f() {
console.log('start');
ee.emit('someEvent');
console.log('end');
}
f()
// start
// event has occured
// end
```
上面代码在加载`events`模块后,通过`EventEmitter`属性建立了一个`EventEmitter`对象实例,这个实例就是消息中心。然后,通过`on`方法为`someEvent`事件指定回调函数。最后,通过`emit`方法触发`someEvent`事件。
上面代码也表明,`EventEmitter`对象的事件触发和监听是同步的,即只有事件的回调函数执行以后,函数`f`才会继续执行。
### on方法
默认情况下,Node.js允许同一个事件最多可以指定10个回调函数。
```javascript
ee.on("someEvent", function () { console.log("event 1"); });
ee.on("someEvent", function () { console.log("event 2"); });
ee.on("someEvent", function () { console.log("event 3"); });
```
超过10个回调函数,会发出一个警告。这个门槛值可以通过setMaxListeners方法改变。
```javascript
ee.setMaxListeners(20);
```
### emit方法
EventEmitter实例的emit方法,用来触发事件。它的第一个参数是事件名称,其余参数都会依次传入回调函数。
```javascript
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);
```
## EventEmitter接口的部署
Events模块的作用,还在于其他模块可以部署EventEmitter接口,从而也能够订阅和发布消息。
```javascript
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方法,提供了另一种继承EventEmitter的写法。
```javascript
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接口。下面是使用这个模块的例子。
```javascript
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);
});
```
## 事件类型
Events模块默认支持两个事件。
- newListener事件:添加新的回调函数时触发。
- removeListener事件:移除回调时触发。
```javascript
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事件。
## EventEmitter实例的方法
### once方法
该方法类似于on方法,但是回调函数只触发一次。
```javascript
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事件,但是回调函数只会在第一次调用时运行。
下面代码指定,一旦服务器连通,只调用一次的回调函数。
```javascript
server.once('connection', function (stream) {
console.log('Ah, we have our first user!');
});
```
该方法返回一个EventEmitter对象,因此可以链式加载监听函数。
### removeListener方法
该方法用于移除回调函数。它接受两个参数,第一个是事件名称,第二个是回调函数名称。这就是说,不能用于移除匿名函数。
```javascript
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方法。
```javascript
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);
```
**(3)removeAllListeners方法**
该方法用于移除某个事件的所有回调函数。
```javascript
var EventEmitter = require('events').EventEmitter;
var emitter = new EventEmitter;
// some code here
emitter.removeAllListeners("firstConnection");
```
如果不带参数,则表示移除所有事件的所有回调函数。
```javascript
emitter.removeAllListeners();
```
**(4)listener方法**
该方法接受一个事件名称作为参数,返回该事件所有回调函数组成的数组。
```javascript
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方法取消了回调函数。
<h2 id="12.10">stream接口</h2>
## Stream是什么?
”流“(stream)这个概念,最简单的理解,就是在数据还没有接收完成时,就开始处理它。
```javascript
var fs = require('fs');
fs.createReadStream('./data/customers.csv').pipe(process.stdout);
```
上面代码中,`fs.createReadStream`方法以”流“的方式读取文件,这可以在文件还没有读取完的情况下,就输出到标准输出。这显然对大文件的读取非常有利。
Unix操作系统从很早以前,就有Stream(流)这个概念,它是不同进程之间传递数据的一种方式。管道命令Pipe就起到在不同命令之间,连接Stream的作用。
Stream把较大的数据,拆成很小的部分。只要命令部署了Stream接口,就可以把一个流的输出接到另一个流的输入。Node引入了这个概念,通过Stream为异步读写数据提供的统一接口。无论是硬盘数据、网络数据,还是内存数据,都可以采用这个接口读写。
读写数据有两种方式。一种方式是同步处理,即先将数据全部读入内存,然后处理。它的优点是符合直觉,流程非常自然,缺点是如果遇到大文件,要花很长时间,可能要过很久才能进入数据处理的步骤。另一种方式就是Stream方式,它是系统读取外部数据实际上的方式,即每次只读入数据的一小块,像“流水”一样。所以,Stream方式就是每当系统读入了一小块数据,就会触发一个事件,发出“新数据块”的信号,只要监听这个事件,就能掌握进展,做出相应处理,这样就提高了程序的性能。
Stream接口最大特点就是通过事件通信,具有readable、writable、drain、data、end、close等事件,既可以读取数据,也可以写入数据。读写数据时,每读入(或写入)一段数据,就会触发一次data事件,全部读取(或写入)完毕,触发end事件。如果发生错误,则触发error事件。
一个对象只要部署了Stream接口,就可以从读取数据,或者写入数据。Node内部很多涉及IO处理的对象,都部署了Stream接口,比如HTTP连接、文件读写、标准输入输出等。
## 基本用法
Node的I/O操作都是异步的,所以与磁盘和网络的交互,都要通过回调函数。一个典型的写文件操作,可能像下面这样。
```javascript
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
fs.readFile(__dirname + '/data.txt', function (err, data) {
res.end(data);
});
});
server.listen(8000);
```
上面的代码有一个问题,那就是它必须将整个data.txt文件读入内存,然后再输入。如果data.txt非常大,就会占用大量的内容。一旦有多个并发请求,操作就会变得非常缓慢,用户不得不等很久,才能得到结果。
由于参数req和res都部署了Stream接口,可以使用`fs.createReadStream()`替代`fs.readFile()`,就能解决这个问题。
```javascript
var http = require('http');
var fs = require('fs');
var server = http.createServer(function (req, res) {
var stream = fs.createReadStream(__dirname + '/data.txt');
stream.pipe(res);
});
server.listen(8000);
```
Stream接口的最大特点,就是数据会发出node和data事件,内置的pipe方法会处理这两个事件。
数据流通过pipe方法,可以方便地导向其他具有Stream接口的对象。
```javascript
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('wow.txt')
.pipe(zlib.createGzip())
.pipe(process.stdout);
```
上面代码先打开文本文件wow.txt,然后压缩,再导向标准输出。
```javascript
fs.createReadStream('wow.txt')
.pipe(zlib.createGzip())
.pipe(fs.createWriteStream('wow.gz'));
```
上面代码压缩文件wow.txt以后,又将其写回压缩文件。
下面代码新建一个Stream实例,然后指定写入事件和终止事件的回调函数,再将其接到标准输入之上。
```javascript
var stream = require('stream');
var Stream = stream.Stream;
var ws = new Stream;
ws.writable = true;
ws.write = function(data) {
console.log("input=" + data);
}
ws.end = function(data) {
console.log("bye");
}
process.stdin.pipe(ws);
```
调用上面的脚本,会产生以下结果。
```bash
$ node pipe_out.js
hello
input=hello
^d
bye
```
上面代码调用脚本下,键入hello,会输出`input=hello`。然后按下ctrl-d,会输出bye。使用管道命令,可以看得更清楚。
```bash
$ echo hello | node pipe_out.js
input=hello
bye
```
Stream接口分成三类。
- 可读数据流接口,用于读取数据。
- 可写数据流接口,用于写入数据。
- 双向数据流接口,用于读取和写入数据,比如Node的tcp sockets、zlib、crypto都部署了这个接口。
## 可读数据流
“可读数据流”用来产生数据。它表示数据的来源,只要一个对象提供“可读数据流”,就表示你可以从其中读取数据。
```javascript
var Readable = require('stream').Readable;
var rs = new Readable;
rs.push('beep ');
rs.push('boop\n');
rs.push(null);
rs.pipe(process.stdout);
```
上面代码产生了一个可写数据流,最后将其写入标注输出。可读数据流的push方法,用来将数据输入缓存。
`rs.push(null)`中的null,用来告诉rs,数据输入完毕。
“可读数据流”有两种状态:流动态和暂停态。处于流动态时,数据会尽快地从数据源导向用户的程序;处于暂停态时,必须显式调用`stream.read()`等指令,“可读数据流”才会释放数据。刚刚新建的时候,“可读数据流”处于暂停态。
三种方法可以让暂停态转为流动态。
- 添加data事件的监听函数
- 调用resume方法
- 调用pipe方法将数据送往一个可写数据流
如果转为流动态时,没有data事件的监听函数,也没有pipe方法的目的地,那么数据将遗失。
以下两种方法可以让流动态转为暂停态。
- 不存在pipe方法的目的地时,调用pause方法
- 存在pipe方法的目的地时,移除所有data事件的监听函数,并且调用unpipe方法,移除所有pipe方法的目的地
注意,只移除data事件的监听函数,并不会自动引发数据流进入“暂停态”。另外,存在pipe方法的目的地时,调用pause方法,并不能保证数据流总是处于暂停态,一旦那些目的地发出数据请求,数据流有可能会继续提供数据。
每当系统有新的数据,该接口可以监听到data事件,从而回调函数。
```javascript
var fs = require('fs');
var readableStream = fs.createReadStream('file.txt');
var data = '';
readableStream.setEncoding('utf8');
readableStream.on('data', function(chunk) {
data+=chunk;
});
readableStream.on('end', function() {
console.log(data);
});
```
上面代码中,fs模块的createReadStream方法,是部署了Stream接口的文件读取方法。该方法对指定的文件,返回一个对象。该对象只要监听data事件,回调函数就能读到数据。
除了data事件,监听readable事件,也可以读到数据。
```javascript
var fs = require('fs');
var readableStream = fs.createReadStream('file.txt');
var data = '';
var chunk;
readableStream.setEncoding('utf8');
readableStream.on('readable', function() {
while ((chunk=readableStream.read()) !== null) {
data += chunk;
}
});
readableStream.on('end', function() {
console.log(data)
});
```
readable事件表示系统缓冲之中有可读的数据,使用read方法去读出数据。如果没有数据可读,read方法会返回null。
“可读数据流”除了read方法,还有以下方法。
- Readable.pause() :暂停数据流。已经存在的数据,也不再触发data事件,数据将保留在缓存之中,此时的数据流称为静态数据流。如果对静态数据流再次调用pause方法,数据流将重新开始流动,但是缓存中现有的数据,不会再触发data事件。
- Readable.resume():恢复暂停的数据流。
- readable.unpipe():从管道中移除目的地数据流。如果该方法使用时带有参数,会阻止“可读数据流”进入某个特定的目的地数据流。如果使用时不带有参数,则会移除所有的目的地数据流。
### read()
read方法从系统缓存读取并返回数据。如果读不到数据,则返回null。
该方法可以接受一个整数作为参数,表示所要读取数据的数量,然后会返回该数量的数据。如果读不到足够数量的数据,返回null。如果不提供这个参数,默认返回系统缓存之中的所有数据。
只在“暂停态”时,该方法才有必要手动调用。“流动态”时,该方法是自动调用的,直到系统缓存之中的数据被读光。
```javascript
var readable = getReadableStreamSomehow();
readable.on('readable', function() {
var chunk;
while (null !== (chunk = readable.read())) {
console.log('got %d bytes of data', chunk.length);
}
});
```
如果该方法返回一个数据块,那么它就触发了data事件。
### _read()
可读数据流的_read方法,可以将数据放入可读数据流。
```javascript
var Readable = require('stream').Readable;
var rs = Readable();
var c = 97;
rs._read = function () {
rs.push(String.fromCharCode(c++));
if (c > 'z'.charCodeAt(0)) rs.push(null);
};
rs.pipe(process.stdout);
```
运行结果如下。
```bash
$ node read1.js
abcdefghijklmnopqrstuvwxyz
```
### setEncoding()
调用该方法,会使得数据流返回指定编码的字符串,而不是缓存之中的二进制对象。比如,调用`setEncoding('utf8')`,数据流会返回UTF-8字符串,调用`setEncoding('hex')`,数据流会返回16进制的字符串。
该方法会正确处理多字节的字符,而缓存的方法`buf.toString(encoding)`不会。所以如果想要从数据流读取字符串,应该总是使用该方法。
```javascript
var readable = getReadableStreamSomehow();
readable.setEncoding('utf8');
readable.on('data', function(chunk) {
assert.equal(typeof chunk, 'string');
console.log('got %d characters of string data', chunk.length);
});
```
### resume()
resume方法会使得“可读数据流”继续释放data事件,即转为流动态。
```javascript
var readable = getReadableStreamSomehow();
readable.resume();
readable.on('end', function(chunk) {
console.log('数据流到达尾部,未读取任务数据');
});
```
上面代码中,调用resume方法使得数据流进入流动态,只定义end事件的监听函数,不定义data事件的监听函数,表示不从数据流读取任何数据,只监听数据流到达尾部。
### pause()
pause方法使得流动态的数据流,停止释放data事件,转而进入暂停态。任何此时已经可以读到的数据,都将停留在系统缓存。
```javascript
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
console.log('读取%d字节的数据', chunk.length);
readable.pause();
console.log('接下来的1秒内不读取数据');
setTimeout(function() {
console.log('数据恢复读取');
readable.resume();
}, 1000);
});
```
### isPaused()
该方法返回一个布尔值,表示“可读数据流”被客户端手动暂停(即调用了pause方法),目前还没有调用resume方法。
```javascript
var readable = new stream.Readable
readable.isPaused() // === false
readable.pause()
readable.isPaused() // === true
readable.resume()
readable.isPaused() // === false
```
### pipe()
pipe方法是自动传送数据的机制,就像管道一样。它从“可读数据流”读出所有数据,将其写出指定的目的地。整个过程是自动的。
```javascript
src.pipe(dst)
```
pipe方法必须在可读数据流上调用,它的参数必须是可写数据流。
```javascript
var fs = require('fs');
var readableStream = fs.createReadStream('file1.txt');
var writableStream = fs.createWriteStream('file2.txt');
readableStream.pipe(writableStream);
```
上面代码使用pipe方法,将file1的内容写入file2。整个过程由pipe方法管理,不用手动干预,所以可以将传送数据写得很简洁。
pipe方法返回目的地的数据流,因此可以使用链式写法,将多个数据流操作连在一起。
```javascript
a.pipe(b).pipe(c).pipe(d)
// 等同于
a.pipe(b);
b.pipe(c);
c.pipe(d);
```
下面是一个例子。
```javascript
var fs = require('fs');
var zlib = require('zlib');
fs.createReadStream('input.txt.gz')
.pipe(zlib.createGunzip())
.pipe(fs.createWriteStream('output.txt'));
```
上面代码采用链式写法,先读取文件,然后进行压缩,最后输出。
下面的写法模拟了Unix系统的cat命令,将标准输出写入标准输入。
```javascript
process.stdin.pipe(process.stdout);
```
当来源地的数据流读取完成,默认会调用目的地的end方法,就不再能够写入。对pipe方法传入第二个参数`{ end: false }`,可以让目的地的数据流保持打开。
```javascript
reader.pipe(writer, { end: false });
reader.on('end', function() {
writer.end('Goodbye\n');
});
```
上面代码中,目的地数据流默认不会调用end方法,只能手动调用,因此“Goodbye”会被写入。
### unpipe()
该方法移除pipe方法指定的数据流目的地。如果没有参数,则移除所有的pipe方法目的地。如果有参数,则移除该参数指定的目的地。如果没有匹配参数的目的地,则不会产生任何效果。
```javascript
var readable = getReadableStreamSomehow();
var writable = fs.createWriteStream('file.txt');
readable.pipe(writable);
setTimeout(function() {
console.log('停止写入file.txt');
readable.unpipe(writable);
console.log('手动关闭file.txt的写入数据流');
writable.end();
}, 1000);
```
上面代码写入file.txt的时间,只有1秒钟,然后就停止写入。
### 事件
(1)readable
readable事件在数据流能够向外提供数据时触发。
```javascript
var readable = getReadableStreamSomehow();
readable.on('readable', function() {
// there is some data to read now
});
```
下面是一个例子。
```javascript
process.stdin.on('readable', function () {
var buf = process.stdin.read();
console.dir(buf);
});
```
上面代码将标准输入的数据读出。
read方法接受一个整数作为参数,表示以多少个字节为单位进行读取。
```javascript
process.stdin.on('readable', function () {
var buf = process.stdin.read(3);
console.dir(buf);
});
```
上面代码将以3个字节为单位进行输出内容。
(2)data
对于那些没有显式暂停的数据流,添加data事件监听函数,会将数据流切换到流动态,尽快向外提供数据。
```javascript
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
console.log('got %d bytes of data', chunk.length);
});
```
(3)end
无法再读取到数据时,会触发end事件。也就是说,只有当前数据被完全读取完,才会触发end事件,比如不停地调用read方法。
```javascript
var readable = getReadableStreamSomehow();
readable.on('data', function(chunk) {
console.log('got %d bytes of data', chunk.length);
});
readable.on('end', function() {
console.log('there will be no more data.');
});
```
(4)close
数据源关闭时,close事件被触发。并不是所有的数据流都支持这个事件。
(5)error
当读取数据发生错误时,error事件被触发。
## 可写数据流
“可写数据流”允许你将数据写入某个目的地。它是数据写入的一种抽象,不同的数据目的地部署了这个接口以后,就可以用统一的方法写入。
以下是部署了可写数据流的一些场合。
- 客户端的http requests
- 服务器的http responses
- fs write streams
- zlib streams
- crypto streams
- tcp sockets
- child process stdin
- process.stdout, process.stderr
下面是fs模块的可写数据流的例子。
```javascript
var fs = require('fs');
var readableStream = fs.createReadStream('file1.txt');
var writableStream = fs.createWriteStream('file2.txt');
readableStream.setEncoding('utf8');
readableStream.on('data', function(chunk) {
writableStream.write(chunk);
});
```
上面代码中,fs模块的createWriteStream方法针对特定文件,创建了一个“可写数据流”,本质上就是对写入操作部署了Stream接口。然后,“可写数据流”的write方法,可以将数据写入文件。
### write()
write方法用于向“可写数据流”写入数据。它接受两个参数,一个是写入的内容,可以是字符串,也可以是一个stream对象(比如可读数据流),另一个是写入完成后的回调函数。
它返回一个布尔值,表示本次数据是否处理完成。如果返回true,就表示可以写入新的数据了。如果等待写入的数据被缓存了,就返回false。不过,在返回false的情况下,也可以继续传入新的数据等待写入。只是这时,新的数据不会真的写入,只会缓存在内存中。为了避免内存消耗,比较好的做法还是等待该方法返回true,然后再写入。
```javascript
var fs = require('fs');
var ws = fs.createWriteStream('message.txt');
ws.write('beep ');
setTimeout(function () {
ws.end('boop\n');
}, 1000);
```
上面代码调用end方法,数据就不再写入了。
### cork(),uncork()
cork方法可以强制等待写入的数据进入缓存。当调用uncork方法或end方法时,缓存的数据就会吐出。
### setDefaultEncoding()
setDefaultEncoding方法用于将写入的数据编码成新的格式。它返回一个布尔值,表示编码是否成功,如果返回false就表示编码失败。
### end()
end方法用于终止“可写数据流”。该方法可以接受三个参数,全部都是可选参数。第一个参数是最后所要写入的数据,可以是字符串,也可以是stream对象;第二个参数是写入编码;第三个参数是一个回调函数,finish事件触发时,会调用这个回调函数。
```javascript
var file = fs.createWriteStream('example.txt');
file.write('hello, ');
file.end('world!');
```
上面代码会在数据写入结束时,在尾部写入“world!”。
调用end方法之后,再写入数据会报错。
```javascript
var file = fs.createWriteStream('example.txt');
file.end('world!');
file.write('hello, '); // 报错
```
### 事件
(1)drain事件
`writable.write(chunk)`返回false以后,当缓存数据全部写入完成,可以继续写入时,会触发drain事件。
```javascript
function writeOneMillionTimes(writer, data, encoding, callback) {
var i = 1000000;
write();
function write() {
var ok = true;
do {
i -= 1;
if (i === 0) {
writer.write(data, encoding, callback);
} else {
ok = writer.write(data, encoding);
}
} while (i > 0 && ok);
if (i > 0) {
writer.once('drain', write);
}
}
}
```
上面代码是一个写入100万次的例子,通过drain事件得到可以继续写入的通知。
(2)finish事件
调用end方法时,所有缓存的数据释放,触发finish事件。该事件的回调函数没有参数。
```javascript
var writer = getWritableStreamSomehow();
for (var i = 0; i < 100; i ++) {
writer.write('hello, #' + i + '!\n');
}
writer.end('this is the end\n');
writer.on('finish', function() {
console.error('all writes are now complete.');
});
```
(3)pipe事件
“可写数据流”调用pipe方法,将数据流导向写入目的地时,触发该事件。
该事件的回调函数,接受发出该事件的“可读数据流”对象作为参数。
```javascript
var writer = getWritableStreamSomehow();
var reader = getReadableStreamSomehow();
writer.on('pipe', function(src) {
console.error('something is piping into the writer');
assert.equal(src, reader);
});
reader.pipe(writer);
```
(4)unpipe事件
“可读数据流”调用unpipe方法,将可写数据流移出写入目的地时,触发该事件。
该事件的回调函数,接受发出该事件的“可读数据流”对象作为参数。
```javascript
var writer = getWritableStreamSomehow();
var reader = getReadableStreamSomehow();
writer.on('unpipe', function(src) {
console.error('something has stopped piping into the writer');
assert.equal(src, reader);
});
reader.pipe(writer);
reader.unpipe(writer);
```
(5)error事件
如果写入数据或pipe数据时发生错误,就会触发该事件。
该事件的回调函数,接受一个Error对象作为参数。
## HTTP请求
HTTP对象使用Stream接口,实现网络数据的读写。
```javascript
var http = require('http');
var server = http.createServer(function (req, res) {
// req is an http.IncomingMessage, which is a Readable Stream
// res is an http.ServerResponse, which is a Writable Stream
var body = '';
// we want to get the data as utf8 strings
// If you don't set an encoding, then you'll get Buffer objects
req.setEncoding('utf8');
// Readable streams emit 'data' events once a listener is added
req.on('data', function (chunk) {
body += chunk;
});
// the end event tells you that you have entire body
req.on('end', function () {
try {
var data = JSON.parse(body);
} catch (er) {
// uh oh! bad json!
res.statusCode = 400;
return res.end('error: ' + er.message);
}
// write back something interesting to the user:
res.write(typeof data);
res.end();
});
});
server.listen(1337);
// $ curl localhost:1337 -d '{}'
// object
// $ curl localhost:1337 -d '"foo"'
// string
// $ curl localhost:1337 -d 'not json'
// error: Unexpected token o
```
data事件表示读取或写入了一块数据。
```javascript
req.on('data', function(buf){
// Do something with the Buffer
});
```
使用req.setEncoding方法,可以设定字符串编码。
```javascript
req.setEncoding('utf8');
req.on('data', function(str){
// Do something with the String
});
```
end事件,表示读取或写入数据完毕。
```javascript
var http = require('http');
http.createServer(function(req, res){
res.writeHead(200);
req.on('data', function(data){
res.write(data);
});
req.on('end', function(){
res.end();
});
}).listen(3000);
```
上面代码相当于建立了“回声”服务,将HTTP请求的数据体,用HTTP回应原样发送回去。
system模块提供了pump方法,有点像Linux系统的管道功能,可以将一个数据流,原封不动得转给另一个数据流。所以,上面的例子也可以用pump方法实现。
```javascript
var http = require('http'),
sys = require('sys');
http.createServer(function(req, res){
res.writeHead(200);
sys.pump(req, res);
}).listen(3000);
```
## fs模块
fs模块的createReadStream方法用于新建读取数据流,createWriteStream方法用于新建写入数据流。使用这两个方法,可以做出一个用于文件复制的脚本copy.js。
```javascript
// copy.js
var fs = require('fs');
console.log(process.argv[2], '->', process.argv[3]);
var readStream = fs.createReadStream(process.argv[2]);
var writeStream = fs.createWriteStream(process.argv[3]);
readStream.on('data', function (chunk) {
writeStream.write(chunk);
});
readStream.on('end', function () {
writeStream.end();
});
readStream.on('error', function (err) {
console.log("ERROR", err);
});
writeStream.on('error', function (err) {
console.log("ERROR", err);
});d all your errors, you wouldn't need to use domains.
```
上面代码非常容易理解,使用的时候直接提供源文件路径和目标文件路径,就可以了。
```bash
node cp.js src.txt dest.txt
```
Streams对象都具有pipe方法,起到管道作用,将一个数据流输入另一个数据流。所以,上面代码可以重写成下面这样:
```javascript
var fs = require('fs');
console.log(process.argv[2], '->', process.argv[3]);
var readStream = fs.createReadStream(process.argv[2]);
var writeStream = fs.createWriteStream(process.argv[3]);
readStream.on('open', function () {
readStream.pipe(writeStream);
});
readStream.on('end', function () {
writeStream.end();
});
```
## 错误处理
下面是压缩后发送文件的代码。
```javascript
http.createServer(function (req, res) {
// set the content headers
fs.createReadStream('filename.txt')
.pipe(zlib.createGzip())
.pipe(res)
})
```
上面的代码没有部署错误处理机制,一旦发生错误,就无法处理。所以,需要加上error事件的监听函数。
```javascript
http.createServer(function (req, res) {
// set the content headers
fs.createReadStream('filename.txt')
.on('error', onerror)
.pipe(zlib.createGzip())
.on('error', onerror)
.pipe(res)
function onerror(err) {
console.error(err.stack)
}
})
```
上面的代码还是存在问题,如果客户端中断下载,写入的数据流就会收不到close事件,一直处于等待状态,从而造成内存泄漏。因此,需要使用[on-finished模块](https://github.com/jshttp/on-finished)用来处理这种情况。
```javascript
http.createServer(function (req, res) {
var stream = fs.createReadStream('filename.txt')
// set the content headers
stream
.on('error', onerror)
.pipe(zlib.createGzip())
.on('error', onerror)
.pipe(res)
onFinished(res, function () {
// make sure the stream is always destroyed
stream.destroy()
})
})
```
<h2 id="12.11">Child Process模块</h2>
child_process模块用于新建子进程。子进程的运行结果储存在系统缓存之中(最大200KB),等到子进程运行结束以后,主进程再用回调函数读取子进程的运行结果。
## exec()
`exec`方法用于执行bash命令,它的参数是一个命令字符串。
```javascript
var exec = require('child_process').exec;
var ls = exec('ls -l', function (error, stdout, stderr) {
if (error) {
console.log(error.stack);
console.log('Error code: ' + error.code);
}
console.log('Child Process STDOUT: ' + stdout);
});
```
上面代码的`exec`方法用于新建一个子进程,然后缓存它的运行结果,运行结束后调用回调函数。
`exec`方法最多可以接受两个参数,第一个参数是所要执行的shell命令,第二个参数是回调函数,该函数接受三个参数,分别是发生的错误、标准输出的显示结果、标准错误的显示结果。
由于标准输出和标准错误都是流对象(stream),可以监听data事件,因此上面的代码也可以写成下面这样。
```javascript
var exec = require('child_process').exec;
var child = exec('ls -l');
child.stdout.on('data', function(data) {
console.log('stdout: ' + data);
});
child.stderr.on('data', function(data) {
console.log('stdout: ' + data);
});
child.on('close', function(code) {
console.log('closing code: ' + code);
});
```
上面的代码还表明,子进程本身有`close`事件,可以设置回调函数。
上面的代码还有一个好处。监听data事件以后,可以实时输出结果,否则只有等到子进程结束,才会输出结果。所以,如果子进程运行时间较长,或者是持续运行,第二种写法更好。
下面是另一个例子,假定有一个child.js文件。
```javascript
// child.js
var exec = require('child_process').exec;
exec('node -v', function(error, stdout, stderr) {
console.log('stdout: ' + stdout);
console.log('stderr: ' + stderr);
if (error !== null) {
console.log('exec error: ' + error);
}
});
```
运行后,该文件的输出结果如下。
```bash
$ node child.js
stdout: v0.11.14
stderr:
```
exec方法会直接调用bash(`/bin/sh`程序)来解释命令,所以如果有用户输入的参数,exec方法是不安全的。
```javascript
var path = ";user input";
child_process.exec('ls -l ' + path, function (err, data) {
console.log(data);
});
```
上面代码表示,在bash环境下,`ls -l; user input`会直接运行。如果用户输入恶意代码,将会带来安全风险。因此,在有用户输入的情况下,最好不使用`exec`方法,而是使用`execFile`方法。
## execSync()
`execSync`是`exec`的同步执行版本。
它可以接受两个参数,第一个参数是所要执行的命令,第二个参数用来配置执行环境。
```javascript
var execSync = require("child_process").execSync;
var SEPARATOR = process.platform === 'win32' ? ';' : ':';
var env = Object.assign({}, process.env);
env.PATH = path.resolve('./node_modules/.bin') + SEPARATOR + env.PATH;
function myExecSync(cmd) {
var output = execSync(cmd, {
cwd: process.cwd(),
env: env
});
console.log(output);
}
myExecSync('eslint .');
```
上面代码中,`execSync`方法的第二个参数是一个对象。该对象的`cwd`属性指定脚本的当前目录,`env`属性指定环境变量。上面代码将`./node_modules/.bin`目录,存入`$PATH`变量。这样就可以不加路径,引用项目内部的模块命令了,比如`eslint`命令实际执行的是`./node_modules/.bin/eslint`。
## execFile()
execFile方法直接执行特定的程序,参数作为数组传入,不会被bash解释,因此具有较高的安全性。
```javascript
var child_process = require('child_process');
var path = ".";
child_process.execFile('/bin/ls', ['-l', path], function (err, result) {
console.log(result)
});
```
上面代码中,假定`path`来自用户输入,如果其中包含了分号或反引号,ls程序不理解它们的含义,因此也就得不到运行结果,安全性就得到了提高。
## spawn()
spawn方法创建一个子进程来执行特定命令,用法与execFile方法类似,但是没有回调函数,只能通过监听事件,来获取运行结果。它属于异步执行,适用于子进程长时间运行的情况。
```javascript
var child_process = require('child_process');
var path = '.';
var ls = child_process.spawn('/bin/ls', ['-l', path]);
ls.stdout.on('data', function (data) {
console.log('stdout: ' + data);
});
ls.stderr.on('data', function (data) {
console.log('stderr: ' + data);
});
ls.on('close', function (code) {
console.log('child process exited with code ' + code);
});
```
spawn方法接受两个参数,第一个是可执行文件,第二个是参数数组。
spawn对象返回一个对象,代表子进程。该对象部署了EventEmitter接口,它的`data`事件可以监听,从而得到子进程的输出结果。
spawn方法与exec方法非常类似,只是使用格式略有区别。
```javascript
child_process.exec(command, [options], callback)
child_process.spawn(command, [args], [options])
```
## fork()
fork方法直接创建一个子进程,执行Node脚本,`fork('./child.js')` 相当于 `spawn('node', ['./child.js'])` 。与spawn方法不同的是,fork会在父进程与子进程之间,建立一个通信管道,用于进程之间的通信。
```javascript
var n = child_process.fork('./child.js');
n.on('message', function(m) {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
```
上面代码中,fork方法返回一个代表进程间通信管道的对象,对该对象可以监听message事件,用来获取子进程返回的信息,也可以向子进程发送信息。
child.js脚本的内容如下。
```javascript
process.on('message', function(m) {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
```
上面代码中,子进程监听message事件,并向父进程发送信息。
## send()
使用 child_process.fork() 生成新进程之后,就可以用 child.send(message, [sendHandle]) 向新进程发送消息。新进程中通过监听message事件,来获取消息。
下面的例子是主进程的代码。
```javascript
var cp = require('child_process');
var n = cp.fork(__dirname + '/sub.js');
n.on('message', function(m) {
console.log('PARENT got message:', m);
});
n.send({ hello: 'world' });
```
下面是子进程sub.js代码。
```javascript
process.on('message', function(m) {
console.log('CHILD got message:', m);
});
process.send({ foo: 'bar' });
```
<h2 id="12.12">Http模块</h2>
## 基本用法
### 处理GET请求
`http`模块主要用于搭建HTTP服务。使用Node搭建HTTP服务器非常简单。
```javascript
var http = require('http');
http.createServer(function (request, response){
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end('Hello World\n');
}).listen(8080, '127.0.0.1');
console.log('Server running on port 8080.');
```
上面代码第一行`var http = require("http")`,表示加载`http`模块。然后,调用`http`模块的`createServer`方法,创造一个服务器实例。
`ceateServer`方法接受一个函数作为参数,该函数的`request`参数是一个对象,表示客户端的HTTP请求;`response`参数也是一个对象,表示服务器端的HTTP回应。`response.writeHead`方法用来写入HTTP回应的头信息;`response.end`方法用来写入HTTP回应的具体内容,以及回应完成后关闭本次对话。最后的`listen(8080)`表示启动服务器实例,监听本机的8080端口。
将上面这几行代码保存成文件`app.js`,然后执行该脚本,服务器就开始运行了。
```bash
$ node app.js
```
这时命令行窗口将显示一行提示“Server running at port 8080.”。打开浏览器,访问http://localhost:8080,网页显示“Hello world!”。
上面的例子是收到请求后生成网页,也可以事前写好网页,存在文件中,然后利用`fs`模块读取网页文件,将其返回。
```javascript
var http = require('http');
var fs = require('fs');
http.createServer(function (request, response){
fs.readFile('data.txt', function readData(err, data) {
response.writeHead(200, {'Content-Type': 'text/plain'});
response.end(data);
});
// 或者
fs.createReadStream(`${__dirname}/index.html`).pipe(response);
}).listen(8080, '127.0.0.1');
console.log('Server running on port 8080.');
```
下面的修改则是根据不同网址的请求,显示不同的内容,已经相当于做出一个网站的雏形了。
```javascript
var http = require("http");
http.createServer(function(req, res) {
// 主页
if (req.url == "/") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("Welcome to the homepage!");
}
// About页面
else if (req.url == "/about") {
res.writeHead(200, { "Content-Type": "text/html" });
res.end("Welcome to the about page!");
}
// 404错误
else {
res.writeHead(404, { "Content-Type": "text/plain" });
res.end("404 error! File not found.");
}
}).listen(8080, "localhost");
```
回调函数的req(request)对象,拥有以下属性。
- url:发出请求的网址。
- method:HTTP请求的方法。
- headers:HTTP请求的所有HTTP头信息。
### 处理POST请求
当客户端采用POST方法发送数据时,服务器端可以对data和end两个事件,设立监听函数。
```javascript
var http = require('http');
http.createServer(function (req, res) {
var content = "";
req.on('data', function (chunk) {
content += chunk;
});
req.on('end', function () {
res.writeHead(200, {"Content-Type": "text/plain"});
res.write("You've sent: " + content);
res.end();
});
}).listen(8080);
```
data事件会在数据接收过程中,每收到一段数据就触发一次,接收到的数据被传入回调函数。end事件则是在所有数据接收完成后触发。
对上面代码稍加修改,就可以做出文件上传的功能。
```javascript
"use strict";
var http = require('http');
var fs = require('fs');
var destinationFile, fileSize, uploadedBytes;
http.createServer(function (request, response) {
response.writeHead(200);
destinationFile = fs.createWriteStream("destination.md");
request.pipe(destinationFile);
fileSize = request.headers['content-length'];
uploadedBytes = 0;
request.on('data', function (d) {
uploadedBytes += d.length;
var p = (uploadedBytes / fileSize) * 100;
response.write("Uploading " + parseInt(p, 0) + " %\n");
});
request.on('end', function () {
response.end("File Upload Complete");
});
}).listen(3030, function () {
console.log("server started");
});
```
## 发出请求
### get()
get方法用于发出get请求。
```javascript
function getTestPersonaLoginCredentials(callback) {
return http.get({
host: 'personatestuser.org',
path: '/email'
}, function(response) {
var body = '';
response.on('data', function(d) {
body += d;
});
response.on('end', function() {
var parsed = JSON.parse(body);
callback({
email: parsed.email,
password: parsed.pass
});
});
});
},
```
### request()
request方法用于发出HTTP请求,它的使用格式如下。
```javascript
http.request(options[, callback])
```
request方法的options参数,可以是一个对象,也可以是一个字符串。如果是字符串,就表示这是一个URL,Node内部就会自动调用`url.parse()`,处理这个参数。
options对象可以设置如下属性。
- host:HTTP请求所发往的域名或者IP地址,默认是localhost。
- hostname:该属性会被`url.parse()`解析,优先级高于host。
- port:远程服务器的端口,默认是80。
- localAddress:本地网络接口。
- socketPath:Unix网络套接字,格式为host:port或者socketPath。
- method:指定HTTP请求的方法,格式为字符串,默认为GET。
- path:指定HTTP请求的路径,默认为根路径(/)。可以在这个属性里面,指定查询字符串,比如`/index.html?page=12`。如果这个属性里面包含非法字符(比如空格),就会抛出一个错误。
- headers:一个对象,包含了HTTP请求的头信息。
- auth:一个代表HTTP基本认证的字符串`user:password`。
- agent:控制缓存行为,如果HTTP请求使用了agent,则HTTP请求默认为`Connection: keep-alive`,它的可能值如下:
- undefined(默认):对当前host和port,使用全局Agent。
- Agent:一个对象,会传入agent属性。
- false:不缓存连接,默认HTTP请求为`Connection: close`。
- keepAlive:一个布尔值,表示是否保留socket供未来其他请求使用,默认等于false。
- keepAliveMsecs:一个整数,当使用KeepAlive的时候,设置多久发送一个TCP KeepAlive包,使得连接不要被关闭。默认等于1000,只有keepAlive设为true的时候,该设置才有意义。
request方法的callback参数是可选的,在response事件发生时触发,而且只触发一次。
`http.request()`返回一个`http.ClientRequest`类的实例。它是一个可写数据流,如果你想通过POST方法发送一个文件,可以将文件写入这个ClientRequest对象。
下面是发送POST请求的一个例子。
```javascript
var postData = querystring.stringify({
'msg' : 'Hello World!'
});
var options = {
hostname: 'www.google.com',
port: 80,
path: '/upload',
method: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Content-Length': postData.length
}
};
var req = http.request(options, function(res) {
console.log('STATUS: ' + res.statusCode);
console.log('HEADERS: ' + JSON.stringify(res.headers));
res.setEncoding('utf8');
res.on('data', function (chunk) {
console.log('BODY: ' + chunk);
});
});
req.on('error', function(e) {
console.log('problem with request: ' + e.message);
});
// write data to request body
req.write(postData);
req.end();
```
注意,上面代码中,`req.end()`必须被调用,即使没有在请求体内写入任何数据,也必须调用。因为这表示已经完成HTTP请求。
发送过程的任何错误(DNS错误、TCP错误、HTTP解析错误),都会在request对象上触发error事件。
## 搭建HTTPs服务器
搭建HTTPs服务器需要有SSL证书。对于向公众提供服务的网站,SSL证书需要向证书颁发机构购买;对于自用的网站,可以自制。
自制SSL证书需要OpenSSL,具体命令如下。
```bash
$ openssl genrsa -out key.pem
$ openssl req -new -key key.pem -out csr.pem
$ openssl x509 -req -days 9999 -in csr.pem -signkey key.pem -out cert.pem
$ rm csr.pem
```
上面的命令生成两个文件:ert.pem(证书文件)和 key.pem(私钥文件)。有了这两个文件,就可以运行HTTPs服务器了。
Node内置Https支持。
```javascript
var server = https.createServer({
key: privateKey,
cert: certificate,
ca: certificateAuthorityCertificate
}, app);
```
Node.js提供一个https模块,专门用于处理加密访问。
```javascript
var https = require('https');
var fs = require('fs');
var options = {
key: fs.readFileSync('key.pem'),
cert: fs.readFileSync('cert.pem')
};
var a = https.createServer(options, function (req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
```
上面代码显示,HTTPs服务器与HTTP服务器的最大区别,就是createServer方法多了一个options参数。运行以后,就可以测试是否能够正常访问。
```bash
curl -k https://localhost:8000
```
## 模块属性
(1)HTTP请求的属性
- headers:HTTP请求的头信息。
- url:请求的路径。
## 模块方法
(1)http模块的方法
- createServer(callback):创造服务器实例。
(2)服务器实例的方法
- listen(port):启动服务器监听指定端口。
(3)HTTP回应的方法
- setHeader(key, value):指定HTTP头信息。
- write(str):指定HTTP回应的内容。
- end():发送HTTP回应。
<h2 id="12.13">assert 模块</h2>
assert模块是Node的内置模块,主要用于断言。如果表达式不符合预期,就抛出一个错误。该模块提供11个方法,但只有少数几个是常用的。
## assert()
assert方法接受两个参数,当第一个参数对应的布尔值为true时,不会有任何提示,返回undefined。当第一个参数对应的布尔值为false时,会抛出一个错误,该错误的提示信息就是第二个参数设定的字符串。
```javascript
// 格式
assert(value, message)
// 例子
var assert = require('assert');
function add (a, b) {
return a + b;
}
var expected = add(1,2);
assert( expected === 3, '预期1加2等于3');
```
上面代码不会有任何输出,因为assert方法的第一个参数是true。
```javascript
assert( expected === 4, '预期1加2等于3')
// AssertionError: 预期1加2等于3
```
上面代码会抛出一个错误,因为assert方法的第一个参数是false。
## assert.ok()
ok是assert方法的另一个名字,与assert方法完全一样。
## assert.equal()
equal方法接受三个参数,第一个参数是实际值,第二个是预期值,第三个是错误的提示信息。
```javascript
// 格式
assert.equal(actual, expected, [message])
assert.equal(true, value, message);
// 等同于
assert(value, message);
// 例子
var assert = require('assert');
function add (a, b) {
return a + b;
}
var expected = add(1,2);
// 以下三句效果相同
assert(expected == 3, '预期1+2等于3');
assert.ok(expected == 3, '预期1+2等于3');
assert.equal(expected, 3, '预期1+2等于3');
```
equal方法内部使用的是相等运算符(==),而不是严格运算符(===),进行比较运算。
## assert.notEqual()
notEqual方法的用法与equal方法类似,但只有在实际值等于预期值时,才会抛出错误。
```javascript
// 格式
assert.notEqual(actual, expected, [message])
// 用法
var assert = require('assert');
function add (a, b) {
return a + b;
}
var expected = add(1,2);
// 以下三种写法效果相同
assert(expected != 4, '预期不等于4');
assert.ok(expected != 4, '预期不等于4');
assert.notEqual(expected, 4, '预期不等于4');
```
notEqual方法内部使用不相等运算符(!=),而不是严格不相等运算符(!==),进行比较运算。
## assert.deepEqual()
deepEqual方法用来比较两个对象。只要它们的属性一一对应,且值都相等,就认为两个对象相等,否则抛出一个错误。
```javascript
// 格式
assert.deepEqual(actual, expected, [message])
// 例子
var assert = require('assert');
var list1 = [1, 2, 3, 4, 5];
var list2 = [1, 2, 3, 4, 5];
assert.deepEqual(list1, list2, '预期两个数组应该有相同的属性');
var person1 = { "name":"john", "age":"21" };
var person2 = { "name":"john", "age":"21" };
assert.deepEqual(person1, person2, '预期两个对象应该有相同的属性');
```
## assert.notDeepEqual()
notDeepEqual方法与deepEqual方法正好相反,用来断言两个对象是否不相等。
```javascript
// 格式
assert.notDeepEqual(actual, expected, [message])
// 例子
var assert = require('assert');
var list1 = [1, 2, ,3, 4, 5];
var list2 = [1, 2, 3, 4, 5];
assert.notDeepEqual(list1, list2, '预期两个对象不相等');
var person1 = { "name":"john", "age":"21" };
var person2 = { "name":"jane", "age":"19" };
// deepEqual checks the elements in the objects are identical
assert.notDeepEqual(person1, person2, '预期两个对象不相等');
```
## assert.strictEqual()
strictEqual方法使用严格相等运算符(===),比较两个表达式。
```javascript
// 格式
assert.strictEqual(actual, expected, [message])
// 例子
var assert = require('assert');
assert.strictEqual(1, '1', '预期严格相等');
// AssertionError: 预期严格相等
```
## assert.notStrictEqual()
assert.notStrictEqual方法使用严格不相等运算符(!==),比较两个表达式。
```javascript
// 格式
assert.notStrictEqual(actual, expected, [message])
// 例子
var assert = require('assert');
assert.notStrictEqual(1, true, '预期严格不相等');
```
## assert.throws()
throws方法预期某个代码块会抛出一个错误,且抛出的错误符合指定的条件。
```javascript
// 格式
assert.throws(block, [error], [message])
// 例一,抛出的错误符合某个构造函数
assert.throws(
function() {
throw new Error("Wrong value");
},
Error,
'不符合预期的错误类型'
);
// 例二、抛出错误的提示信息符合正则表达式
assert.throws(
function() {
throw new Error("Wrong value");
},
/value/,
'不符合预期的错误类型'
);
// 例三、抛出的错误符合自定义函数的校验
assert.throws(
function() {
throw new Error("Wrong value");
},
function(err) {
if ( (err instanceof Error) && /value/.test(err) ) {
return true;
}
},
'不符合预期的错误类型'
);
```
## assert.doesNotThrow()
doesNotThrow方法与throws方法正好相反,预期某个代码块不抛出错误。
```javascript
// 格式
assert.doesNotThrow(block, [message])
// 用法
assert.doesNotThrow(
function() {
console.log("Nothing to see here");
},
'预期不抛出错误'
);
```
## assert.ifError()
ifError方法断言某个表达式是否false,如果该表达式对应的布尔值等于true,就抛出一个错误。它对于验证回调函数的第一个参数十分有用,如果该参数为true,就表示有错误。
```javascript
// 格式
assert.ifError(value)
// 用法
function sayHello(name, callback) {
var error = false;
var str = "Hello "+name;
callback(error, str);
}
// use the function
sayHello('World', function(err, value){
assert.ifError(err);
// ...
})
```
## assert.fail()
fail方法用于抛出一个错误。
```javascript
// 格式
assert.fail(actual, expected, message, operator)
// 例子
var assert = require('assert');
assert.fail(21, 42, 'Test Failed', '###')
// AssertionError: Test Failed
assert.fail(21, 21, 'Test Failed', '###')
// AssertionError: Test Failed
assert.fail(21, 42, undefined, '###')
// AssertionError: 21 ### 42
```
该方法共有四个参数,但是不管参数是什么值,它总是抛出一个错误。如果message参数对应的布尔值不为false,抛出的错误信息就是message,否则错误信息就是“实际值 + 分隔符 + 预期值”。
<h2 id="12.14">Cluster模块</h2>
## 概述
### 基本用法
Node.js默认单进程运行,对于32位系统最高可以使用512MB内存,对于64位最高可以使用1GB内存。对于多核CPU的计算机来说,这样做效率很低,因为只有一个核在运行,其他核都在闲置。cluster模块就是为了解决这个问题而提出的。
cluster模块允许设立一个主进程和若干个worker进程,由主进程监控和协调worker进程的运行。worker之间采用进程间通信交换消息,cluster模块内置一个负载均衡器,采用Round-robin算法协调各个worker进程之间的负载。运行时,所有新建立的链接都由主进程完成,然后主进程再把TCP连接分配给指定的worker进程。
```javascript
var cluster = require('cluster');
var os = require('os');
if (cluster.isMaster){
for (var i = 0, n = os.cpus().length; i < n; i += 1){
cluster.fork();
}
} else {
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world\n");
}).listen(8000);
}
```
上面代码先判断当前进程是否为主进程(cluster.isMaster),如果是的,就按照CPU的核数,新建若干个worker进程;如果不是,说明当前进程是worker进程,则在该进程启动一个服务器程序。
上面这段代码有一个缺点,就是一旦work进程挂了,主进程无法知道。为了解决这个问题,可以在主进程部署online事件和exit事件的监听函数。
```javascript
var cluster = require('cluster');
if(cluster.isMaster) {
var numWorkers = require('os').cpus().length;
console.log('Master cluster setting up ' + numWorkers + ' workers...');
for(var i = 0; i < numWorkers; i++) {
cluster.fork();
}
cluster.on('online', function(worker) {
console.log('Worker ' + worker.process.pid + ' is online');
});
cluster.on('exit', function(worker, code, signal) {
console.log('Worker ' + worker.process.pid + ' died with code: ' + code + ', and signal: ' + signal);
console.log('Starting a new worker');
cluster.fork();
});
}
```
上面代码中,主进程一旦监听到worker进程的exit事件,就会重启一个worker进程。worker进程一旦启动成功,可以正常运行了,就会发出online事件。
### worker对象
worker对象是`cluster.fork()`的返回值,代表一个worker进程。
它的属性和方法如下。
(1)worker.id
worker.id返回当前worker的独一无二的进程编号。这个编号也是cluster.workers中指向当前进程的索引值。
(2)worker.process
所有的worker进程都是用child_process.fork()生成的。child_process.fork()返回的对象,就被保存在worker.process之中。通过这个属性,可以获取worker所在的进程对象。
(3)worker.send()
该方法用于在主进程中,向子进程发送信息。
```javascript
if (cluster.isMaster) {
var worker = cluster.fork();
worker.send('hi there');
} else if (cluster.isWorker) {
process.on('message', function(msg) {
process.send(msg);
});
}
```
上面代码的作用是,worker进程对主进程发出的每个消息,都做回声。
在worker进程中,要向主进程发送消息,使用`process.send(message)`;要监听主进程发出的消息,使用下面的代码。
```javascript
process.on('message', function(message) {
console.log(message);
});
```
发出的消息可以字符串,也可以是JSON对象。下面是一个发送JSON对象的例子。
```javascript
worker.send({
type: 'task 1',
from: 'master',
data: {
// the data that you want to transfer
}
});
```
### cluster.workers对象
该对象只有主进程才有,包含了所有worker进程。每个成员的键值就是一个worker进程对象,键名就是该worker进程的worker.id属性。
```javascript
function eachWorker(callback) {
for (var id in cluster.workers) {
callback(cluster.workers[id]);
}
}
eachWorker(function(worker) {
worker.send('big announcement to all workers');
});
```
上面代码用来遍历所有worker进程。
当前socket的data事件,也可以用id属性识别worker进程。
```javascript
socket.on('data', function(id) {
var worker = cluster.workers[id];
});
```
## cluster模块的属性与方法
### isMaster,isWorker
isMaster属性返回一个布尔值,表示当前进程是否为主进程。这个属性由process.env.NODE_UNIQUE_ID决定,如果process.env.NODE_UNIQUE_ID为未定义,就表示该进程是主进程。
isWorker属性返回一个布尔值,表示当前进程是否为work进程。它与isMaster属性的值正好相反。
### fork()
fork方法用于新建一个worker进程,上下文都复制主进程。只有主进程才能调用这个方法。
该方法返回一个worker对象。
### kill()
kill方法用于终止worker进程。它可以接受一个参数,表示系统信号。
如果当前是主进程,就会终止与worker.process的联络,然后将系统信号法发向worker进程。如果当前是worker进程,就会终止与主进程的通信,然后退出,返回0。
在以前的版本中,该方法也叫做 worker.destroy() 。
### listening事件
worker进程调用listen方面以后,“listening”就传向该进程的服务器,然后传向主进程。
该事件的回调函数接受两个参数,一个是当前worker对象,另一个是地址对象,包含网址、端口、地址类型(IPv4、IPv6、Unix socket、UDP)等信息。这对于那些服务多个网址的Node应用程序非常有用。
```javascript
cluster.on('listening', function(worker, address) {
console.log("A worker is now connected to " + address.address + ":" + address.port);
});
```
## 不中断地重启Node服务
### 思路
重启服务需要关闭后再启动,利用cluster模块,可以做到先启动一个worker进程,再把原有的所有work进程关闭。这样就能实现不中断地重启Node服务。
首先,主进程向worker进程发出重启信号。
```javascript
workers[wid].send({type: 'shutdown', from: 'master'});
```
worker进程监听message事件,一旦发现内容是shutdown,就退出。
```javascript
process.on('message', function(message) {
if(message.type === 'shutdown') {
process.exit(0);
}
});
```
下面是一个关闭所有worker进程的函数。
```javascript
function restartWorkers() {
var wid, workerIds = [];
for(wid in cluster.workers) {
workerIds.push(wid);
}
workerIds.forEach(function(wid) {
cluster.workers[wid].send({
text: 'shutdown',
from: 'master'
});
setTimeout(function() {
if(cluster.workers[wid]) {
cluster.workers[wid].kill('SIGKILL');
}
}, 5000);
});
};
```
### 实例
下面是一个完整的实例,先是主进程的代码master.js。
```javascript
var cluster = require('cluster');
console.log('started master with ' + process.pid);
// 新建一个worker进程
cluster.fork();
process.on('SIGHUP', function () {
console.log('Reloading...');
var new_worker = cluster.fork();
new_worker.once('listening', function () {
// 关闭所有其他worker进程
for(var id in cluster.workers) {
if (id === new_worker.id.toString()) continue;
cluster.workers[id].kill('SIGTERM');
}
});
});
```
上面代码中,主进程监听SIGHUP事件,如果发生该事件就关闭其他所有worker进程。之所以是SIGHUP事件,是因为nginx服务器监听到这个信号,会创造一个新的worker进程,重新加载配置文件。另外,关闭worker进程时,主进程发送SIGTERM信号,这是因为Node允许多个worker进程监听同一个端口。
下面是worker进程的代码server.js。
```javascript
var cluster = require('cluster');
if (cluster.isMaster) {
require('./master');
return;
}
var express = require('express');
var http = require('http');
var app = express();
app.get('/', function (req, res) {
res.send('ha fsdgfds gfds gfd!');
});
http.createServer(app).listen(8080, function () {
console.log('http://localhost:8080');
});
```
使用时代码如下。
```bash
$ node server.js
started master with 10538
http://localhost:8080
```
然后,向主进程连续发出两次SIGHUP信号。
```bash
$ kill -SIGHUP 10538
$ kill -SIGHUP 10538
```
主进程会连续两次新建一个worker进程,然后关闭所有其他worker进程,显示如下。
```bash
Reloading...
http://localhost:8080
Reloading...
http://localhost:8080
```
最后,向主进程发出SIGTERM信号,关闭主进程。
```bash
$ kill 10538
```
## PM2模块
PM2模块是cluster模块的一个包装层。它的作用是尽量将cluster模块抽象掉,让用户像使用单进程一样,部署多进程Node应用。
```javascript
// app.js
var http = require('http');
http.createServer(function(req, res) {
res.writeHead(200);
res.end("hello world");
}).listen(8080);
```
上面代码是标准的Node架设Web服务器的方式,然后用PM2从命令行启动这段代码。
```javascript
$ pm2 start app.js -i 4
```
上面代码的i参数告诉PM2,这段代码应该在cluster_mode启动,且新建worker进程的数量是4个。如果i参数的值是0,那么当前机器有几个CPU内核,PM2就会启动几个worker进程。
如果一个worker进程由于某种原因挂掉了,会立刻重启该worker进程。
```bash
# 重启所有worker进程
$ pm2 reload all
```
每个worker进程都有一个id,可以用下面的命令查看单个worker进程的详情。
```bash
$ pm2 show <worker id>
```
正确情况下,PM2采用fork模式新建worker进程,即主进程fork自身,产生一个worker进程。`pm2 reload`命令则会用spawn方式启动,即一个接一个启动worker进程,一个新的worker启动成功,再杀死一个旧的worker进程。采用这种方式,重新部署新版本时,服务器就不会中断服务。
```bash
$ pm2 reload <脚本文件名>
```
关闭worker进程的时候,可以部署下面的代码,让worker进程监听shutdown消息。一旦收到这个消息,进行完毕收尾清理工作再关闭。
```javascript
process.on('message', function(msg) {
if (msg === 'shutdown') {
close_all_connections();
delete_logs();
server.close();
process.exit(0);
}
});
```
<h2 id="12.15">OS模块</h2>
os模块提供与操作系统相关的方法。
## API
### os.tmpdir()
`os.tmpdir`方法返回操作系统默认的临时文件目录。
## Socket通信
下面例子列出当前系列的所有IP地址。
```javascript
var os = require('os');
var interfaces = os.networkInterfaces();
for (item in interfaces) {
console.log('Network interface name: ' + item);
for (att in interfaces[item]) {
var address = interfaces[item][att];
console.log('Family: ' + address.family);
console.log('IP Address: ' + address.address);
console.log('Is Internal: ' + address.internal);
console.log('');
}
console.log('==================================');
}
```
<h2 id="12.16">Net模块和DNS模块</h2>
net模块用于底层的网络通信。
## 服务器端Socket接口
来看一个简单的Telnet服务的[例子](https://gist.github.com/atdt/4037228)。
```javascript
var net = require('net');
var port = 1081;
var logo = fs.readFileSync('logo.txt');
var ps1 = '\n\n>>> ';
net.createServer( function ( socket ) {
socket.write( logo );
socket.write( ps1 );
socket.on( 'data', recv.bind( null, socket ) );
} ).listen( port );
```
上面代码,在1081端口架设了一个服务。可以用telnet访问这个服务。
```bash
$ telnet localhost 1081
```
一旦telnet连入以后,就会显示提示符`>>>`,输入命令以后,就会调用回调函数`recv`。
```javascript
function recv( socket, data ) {
if ( data === 'quit' ) {
socket.end( 'Bye!\n' );
return;
}
request( { uri: baseUrl + data }, function ( error, response, body ) {
if ( body && body.length ) {
$ = cheerio.load( body );
socket.write( $( '#mw-content-text p' ).first().text() + '\n' );
} else {
socket.write( 'Error: ' + response.statusCode );
}
socket.write( ps1 );
} );
}
```
上面代码中,如果输入的命令是`quit`,然后就退出telnet。如果是其他命令,就发起远程请求读取数据,并显示在屏幕上。
下面代码是另一个例子,用到了更多的接口。
```javascript
var serverPort = 9099;
var net = require('net');
var server = net.createServer(function(client) {
console.log('client connected');
console.log('client IP Address: ' + client.remoteAddress);
console.log('is IPv6: ' + net.isIPv6(client.remoteAddress));
console.log('total server connections: ' + server.connections);
// Waiting for data from the client.
client.on('data', function(data) {
console.log('received data: ' + data.toString());
// Write data to the client socket.
client.write('hello from server');
});
// Closed socket event from the client.
client.on('end', function() {
console.log('client disconnected');
});
});
server.on('error',function(err){
console.log(err);
server.close();
});
server.listen(serverPort, function() {
console.log('server started on port ' + serverPort);
});
```
上面代码中,createServer方法建立了一个服务端,一旦收到客户端发送的数据,就发出回应,同时还监听客户端是否中断通信。最后,listen方法打开服务端。
## 客户端Socket接口
客户端Socket接口用来向服务器发送数据。
```javascript
var serverPort = 9099;
var server = 'localhost';
var net = require('net');
console.log('connecting to server...');
var client = net.connect({server:server,port:serverPort},function(){
console.log('client connected');
// send data
console.log('send data to server');
client.write('greeting from client socket');
});
client.on('data', function(data) {
console.log('received data: ' + data.toString());
client.end();
});
client.on('error',function(err){
console.log(err);
});
client.on('end', function() {
console.log('client disconnected');
});
```
上面代码连接服务器之后,就向服务器发送数据,然后监听服务器返回的数据。
## DNS模块
DNS模块用于解析域名。resolve4方法用于IPv4环境,resolve6方法用于IPv6环境,lookup方法在以上两种环境都可以使用,返回IP地址(address)和当前环境(IPv4或IPv6)。
```javascript
var dns = require('dns');
dns.resolve4('www.pecollege.net', function (err, addresses) {
if (err)
console.log(err);
console.log('addresses: ' + JSON.stringify(addresses));
});
dns.lookup('www.pecollege.net', function (err, address, family) {
if (err)
console.log(err);
console.log('addresses: ' + JSON.stringify(address));
console.log('family: ' + JSON.stringify(family));
});
```
<h2 id="12.17">Express框架</h2>
## 概述
Express是目前最流行的基于Node.js的Web开发框架,可以快速地搭建一个完整功能的网站。
Express上手非常简单,首先新建一个项目目录,假定叫做hello-world。
```bash
$ mkdir hello-world
```
进入该目录,新建一个package.json文件,内容如下。
```javascript
{
"name": "hello-world",
"description": "hello world test app",
"version": "0.0.1",
"private": true,
"dependencies": {
"express": "4.x"
}
}
```
上面代码定义了项目的名称、描述、版本等,并且指定需要4.0版本以上的Express。
然后,就可以安装了。
```bash
$ npm install
```
执行上面的命令以后,在项目根目录下,新建一个启动文件,假定叫做index.js。
```javascript
var express = require('express');
var app = express();
app.use(express.static(__dirname + '/public'));
app.listen(8080);
```
然后,运行上面的启动脚本。
```bash
$ node index
```
现在就可以访问`http://localhost:8080`,它会在浏览器中打开当前目录的public子目录(严格来说,是打开public目录的index.html文件)。如果public目录之中有一个图片文件`my_image.png`,那么可以用`http://localhost:8080/my_image.png`访问该文件。
你也可以在index.js之中,生成动态网页。
```javascript
// index.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.listen(3000);
```
然后,在命令行下运行启动脚本,就可以在浏览器中访问项目网站了。
```bash
$ node index
```
上面代码会在本机的3000端口启动一个网站,网页显示Hello World。
启动脚本index.js的`app.get`方法,用于指定不同的访问路径所对应的回调函数,这叫做“路由”(routing)。上面代码只指定了根目录的回调函数,因此只有一个路由记录。实际应用中,可能有多个路由记录。
```javascript
// index.js
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.get('/customer', function(req, res){
res.send('customer page');
});
app.get('/admin', function(req, res){
res.send('admin page');
});
app.listen(3000);
```
这时,最好就把路由放到一个单独的文件中,比如新建一个routes子目录。
```javascript
// routes/index.js
module.exports = function (app) {
app.get('/', function (req, res) {
res.send('Hello world');
});
app.get('/customer', function(req, res){
res.send('customer page');
});
app.get('/admin', function(req, res){
res.send('admin page');
});
};
```
然后,原来的index.js就变成下面这样。
```javascript
// index.js
var express = require('express');
var app = express();
var routes = require('./routes')(app);
app.listen(3000);
```
## 运行原理
### 底层:http模块
Express框架建立在node.js内置的http模块上。http模块生成服务器的原始代码如下。
```javascript
var http = require("http");
var app = http.createServer(function(request, response) {
response.writeHead(200, {"Content-Type": "text/plain"});
response.end("Hello world!");
});
app.listen(3000, "localhost");
```
上面代码的关键是http模块的createServer方法,表示生成一个HTTP服务器实例。该方法接受一个回调函数,该回调函数的参数,分别为代表HTTP请求和HTTP回应的request对象和response对象。
Express框架的核心是对http模块的再包装。上面的代码用Express改写如下。
```javascript
var express = require('express');
var app = express();
app.get('/', function (req, res) {
res.send('Hello world!');
});
app.listen(3000);
```
比较两段代码,可以看到它们非常接近。原来是用`http.createServer`方法新建一个app实例,现在则是用Express的构造方法,生成一个Epress实例。两者的回调函数都是相同的。Express框架等于在http模块之上,加了一个中间层。
### 什么是中间件
简单说,中间件(middleware)就是处理HTTP请求的函数。它最大的特点就是,一个中间件处理完,再传递给下一个中间件。App实例在运行过程中,会调用一系列的中间件。
每个中间件可以从App实例,接收三个参数,依次为request对象(代表HTTP请求)、response对象(代表HTTP回应),next回调函数(代表下一个中间件)。每个中间件都可以对HTTP请求(request对象)进行加工,并且决定是否调用next方法,将request对象再传给下一个中间件。
一个不进行任何操作、只传递request对象的中间件,就是下面这样。
```javascript
function uselessMiddleware(req, res, next) {
next();
}
```
上面代码的next就是下一个中间件。如果它带有参数,则代表抛出一个错误,参数为错误文本。
```javascript
function uselessMiddleware(req, res, next) {
next('出错了!');
}
```
抛出错误以后,后面的中间件将不再执行,直到发现一个错误处理函数为止。
### use方法
use是express注册中间件的方法,它返回一个函数。下面是一个连续调用两个中间件的例子。
```javascript
var express = require("express");
var http = require("http");
var app = express();
app.use(function(request, response, next) {
console.log("In comes a " + request.method + " to " + request.url);
next();
});
app.use(function(request, response) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Hello world!\n");
});
http.createServer(app).listen(1337);
```
上面代码使用`app.use`方法,注册了两个中间件。收到HTTP请求后,先调用第一个中间件,在控制台输出一行信息,然后通过`next`方法,将执行权传给第二个中间件,输出HTTP回应。由于第二个中间件没有调用`next`方法,所以request对象就不再向后传递了。
`use`方法内部可以对访问路径进行判断,据此就能实现简单的路由,根据不同的请求网址,返回不同的网页内容。
```javascript
var express = require("express");
var http = require("http");
var app = express();
app.use(function(request, response, next) {
if (request.url == "/") {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the homepage!\n");
} else {
next();
}
});
app.use(function(request, response, next) {
if (request.url == "/about") {
response.writeHead(200, { "Content-Type": "text/plain" });
} else {
next();
}
});
app.use(function(request, response) {
response.writeHead(404, { "Content-Type": "text/plain" });
response.end("404 error!\n");
});
http.createServer(app).listen(1337);
```
上面代码通过`request.url`属性,判断请求的网址,从而返回不同的内容。注意,`app.use`方法一共登记了三个中间件,只要请求路径匹配,就不会将执行权交给下一个中间件。因此,最后一个中间件会返回404错误,即前面的中间件都没匹配请求路径,找不到所要请求的资源。
除了在回调函数内部判断请求的网址,use方法也允许将请求网址写在第一个参数。这代表,只有请求路径匹配这个参数,后面的中间件才会生效。无疑,这样写更加清晰和方便。
```javascript
app.use('/path', someMiddleware);
```
上面代码表示,只对根目录的请求,调用某个中间件。
因此,上面的代码可以写成下面的样子。
```javascript
var express = require("express");
var http = require("http");
var app = express();
app.use("/home", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the homepage!\n");
});
app.use("/about", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
response.end("Welcome to the about page!\n");
});
app.use(function(request, response) {
response.writeHead(404, { "Content-Type": "text/plain" });
response.end("404 error!\n");
});
http.createServer(app).listen(1337);
```
## Express的方法
### all方法和HTTP动词方法
针对不同的请求,Express提供了use方法的一些别名。比如,上面代码也可以用别名的形式来写。
```javascript
var express = require("express");
var http = require("http");
var app = express();
app.all("*", function(request, response, next) {
response.writeHead(200, { "Content-Type": "text/plain" });
next();
});
app.get("/", function(request, response) {
response.end("Welcome to the homepage!");
});
app.get("/about", function(request, response) {
response.end("Welcome to the about page!");
});
app.get("*", function(request, response) {
response.end("404!");
});
http.createServer(app).listen(1337);
```
上面代码的all方法表示,所有请求都必须通过该中间件,参数中的“*”表示对所有路径有效。get方法则是只有GET动词的HTTP请求通过该中间件,它的第一个参数是请求的路径。由于get方法的回调函数没有调用next方法,所以只要有一个中间件被调用了,后面的中间件就不会再被调用了。
除了get方法以外,Express还提供post、put、delete方法,即HTTP动词都是Express的方法。
这些方法的第一个参数,都是请求的路径。除了绝对匹配以外,Express允许模式匹配。
```javascript
app.get("/hello/:who", function(req, res) {
res.end("Hello, " + req.params.who + ".");
});
```
上面代码将匹配“/hello/alice”网址,网址中的alice将被捕获,作为req.params.who属性的值。需要注意的是,捕获后需要对网址进行检查,过滤不安全字符,上面的写法只是为了演示,生产中不应这样直接使用用户提供的值。
如果在模式参数后面加上问号,表示该参数可选。
```javascript
app.get('/hello/:who?',function(req,res) {
if(req.params.id) {
res.end("Hello, " + req.params.who + ".");
}
else {
res.send("Hello, Guest.");
}
});
```
下面是一些更复杂的模式匹配的例子。
```javascript
app.get('/forum/:fid/thread/:tid', middleware)
// 匹配/commits/71dbb9c
// 或/commits/71dbb9c..4c084f9这样的git格式的网址
app.get(/^\/commits\/(\w+)(?:\.\.(\w+))?$/, function(req, res){
var from = req.params[0];
var to = req.params[1] || 'HEAD';
res.send('commit range ' + from + '..' + to);
});
```
### set方法
set方法用于指定变量的值。
```javascript
app.set("views", __dirname + "/views");
app.set("view engine", "jade");
```
上面代码使用set方法,为系统变量“views”和“view engine”指定值。
### response对象
**(1)response.redirect方法**
response.redirect方法允许网址的重定向。
```javascript
response.redirect("/hello/anime");
response.redirect("http://www.example.com");
response.redirect(301, "http://www.example.com");
```
**(2)response.sendFile方法**
response.sendFile方法用于发送文件。
```javascript
response.sendFile("/path/to/anime.mp4");
```
**(3)response.render方法**
response.render方法用于渲染网页模板。
```javascript
app.get("/", function(request, response) {
response.render("index", { message: "Hello World" });
});
```
上面代码使用render方法,将message变量传入index模板,渲染成HTML网页。
### requst对象
**(1)request.ip**
request.ip属性用于获得HTTP请求的IP地址。
**(2)request.files**
request.files用于获取上传的文件。
### 搭建HTTPs服务器
使用Express搭建HTTPs加密服务器,也很简单。
```javascript
var fs = require('fs');
var options = {
key: fs.readFileSync('E:/ssl/myserver.key'),
cert: fs.readFileSync('E:/ssl/myserver.crt'),
passphrase: '1234'
};
var https = require('https');
var express = require('express');
var app = express();
app.get('/', function(req, res){
res.send('Hello World Expressjs');
});
var server = https.createServer(options, app);
server.listen(8084);
console.log('Server is running on port 8084');
```
## 项目开发实例
### 编写启动脚本
上一节使用express命令自动建立项目,也可以不使用这个命令,手动新建所有文件。
先建立一个项目目录(假定这个目录叫做demo)。进入该目录,新建一个package.json文件,写入项目的配置信息。
```javascript
{
"name": "demo",
"description": "My First Express App",
"version": "0.0.1",
"dependencies": {
"express": "3.x"
}
}
```
在项目目录中,新建文件app.js。项目的代码就放在这个文件里面。
```javascript
var express = require('express');
var app = express();
```
上面代码首先加载express模块,赋给变量express。然后,生成express实例,赋给变量app。
接着,设定express实例的参数。
```javascript
// 设定port变量,意为访问端口
app.set('port', process.env.PORT || 3000);
// 设定views变量,意为视图存放的目录
app.set('views', path.join(__dirname, 'views'));
// 设定view engine变量,意为网页模板引擎
app.set('view engine', 'jade');
app.use(express.favicon());
app.use(express.logger('dev'));
app.use(express.bodyParser());
app.use(express.methodOverride());
app.use(app.router);
// 设定静态文件目录,比如本地文件
// 目录为demo/public/images,访问
// 网址则显示为http://localhost:3000/images
app.use(express.static(path.join(__dirname, 'public')));
```
上面代码中的set方法用于设定内部变量,use方法用于调用express的中间件。
最后,调用实例方法listen,让其监听事先设定的端口(3000)。
```javascript
app.listen(app.get('port'));
```
这时,运行下面的命令,就可以在浏览器访问http://127.0.0.1:3000。
```bash
node app.js
```
网页提示“Cannot GET /”,表示没有为网站的根路径指定可以显示的内容。所以,下一步就是配置路由。
### 配置路由
所谓“路由”,就是指为不同的访问路径,指定不同的处理方法。
**(1)指定根路径**
在app.js之中,先指定根路径的处理方法。
```javascript
app.get('/', function(req, res) {
res.send('Hello World');
});
```
上面代码的get方法,表示处理客户端发出的GET请求。相应的,还有app.post、app.put、app.del(delete是JavaScript保留字,所以改叫del)方法。
get方法的第一个参数是访问路径,正斜杠(/)就代表根路径;第二个参数是回调函数,它的req参数表示客户端发来的HTTP请求,res参数代表发向客户端的HTTP回应,这两个参数都是对象。在回调函数内部,使用HTTP回应的send方法,表示向浏览器发送一个字符串。然后,运行下面的命令。
```bash
node app.js
```
此时,在浏览器中访问http://127.0.0.1:3000,网页就会显示“Hello World”。
如果需要指定HTTP头信息,回调函数就必须换一种写法,要使用setHeader方法与end方法。
```javascript
app.get('/', function(req, res){
var body = 'Hello World';
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Length', body.length);
res.end(body);
});
```
**(2)指定特定路径**
上面是处理根目录的情况,下面再举一个例子。假定用户访问/api路径,希望返回一个JSON字符串。这时,get可以这样写。
```javascript
app.get('/api', function(request, response) {
response.send({name:"张三",age:40});
});
```
上面代码表示,除了发送字符串,send方法还可以直接发送对象。重新启动node以后,再访问路径/api,浏览器就会显示一个JSON对象。
```javascript
{
"name": "张三",
"age": 40
}
```
我们也可以把app.get的回调函数,封装成模块。先在routes目录下面建立一个api.js文件。
```javascript
// routes/api.js
exports.index = function (req, res){
res.json(200, {name:"张三",age:40});
}
```
然后,在app.js中加载这个模块。
```javascript
// app.js
var api = require('./routes/api');
app.get('/api', api.index);
```
现在访问时,就会显示与上一次同样的结果。
如果只向浏览器发送简单的文本信息,上面的方法已经够用;但是如果要向浏览器发送复杂的内容,还是应该使用网页模板。
### 静态网页模板
在项目目录之中,建立一个子目录views,用于存放网页模板。
假定这个项目有三个路径:根路径(/)、自我介绍(/about)和文章(/article)。那么,app.js可以这样写:
```javascript
var express = require('express');
var app = express();
app.get('/', function(req, res) {
res.sendfile('./views/index.html');
});
app.get('/about', function(req, res) {
res.sendfile('./views/about.html');
});
app.get('/article', function(req, res) {
res.sendfile('./views/article.html');
});
app.listen(3000);
```
上面代码表示,三个路径分别对应views目录中的三个模板:index.html、about.html和article.html。另外,向服务器发送信息的方法,从send变成了sendfile,后者专门用于发送文件。
假定index.html的内容如下:
```html
<html>
<head>
<title>首页</title>
</head>
<body>
<h1>Express Demo</h1>
<footer>
<p>
<a href="/">首页</a> - <a href="/about">自我介绍</a> - <a href="/article">文章</a>
</p>
</footer>
</body>
</html>
```
上面代码是一个静态网页。如果想要展示动态内容,就必须使用动态网页模板。
## 动态网页模板
网站真正的魅力在于动态网页,下面我们来看看,如何制作一个动态网页的网站。
### 安装模板引擎
Express支持多种模板引擎,这里采用Handlebars模板引擎的服务器端版本[hbs](https://github.com/donpark/hbs)模板引擎。
先安装hbs。
```html
npm install hbs --save-dev
```
上面代码将hbs模块,安装在项目目录的子目录node_modules之中。save-dev参数表示,将依赖关系写入package.json文件。安装以后的package.json文件变成下面这样:
```javascript
// package.json文件
{
"name": "demo",
"description": "My First Express App",
"version": "0.0.1",
"dependencies": {
"express": "3.x"
},
"devDependencies": {
"hbs": "~2.3.1"
}
}
```
安装模板引擎之后,就要改写app.js。
```javascript
// app.js文件
var express = require('express');
var app = express();
// 加载hbs模块
var hbs = require('hbs');
// 指定模板文件的后缀名为html
app.set('view engine', 'html');
// 运行hbs模块
app.engine('html', hbs.__express);
app.get('/', function (req, res){
res.render('index');
});
app.get('/about', function(req, res) {
res.render('about');
});
app.get('/article', function(req, res) {
res.render('article');
});
```
上面代码改用render方法,对网页模板进行渲染。render方法的参数就是模板的文件名,默认放在子目录views之中,后缀名已经在前面指定为html,这里可以省略。所以,res.render('index') 就是指,把子目录views下面的index.html文件,交给模板引擎hbs渲染。
### 新建数据脚本
渲染是指将数据代入模板的过程。实际运用中,数据都是保存在数据库之中的,这里为了简化问题,假定数据保存在一个脚本文件中。
在项目目录中,新建一个文件blog.js,用于存放数据。blog.js的写法符合CommonJS规范,使得它可以被require语句加载。
```javascript
// blog.js文件
var entries = [
{"id":1, "title":"第一篇", "body":"正文", "published":"6/2/2013"},
{"id":2, "title":"第二篇", "body":"正文", "published":"6/3/2013"},
{"id":3, "title":"第三篇", "body":"正文", "published":"6/4/2013"},
{"id":4, "title":"第四篇", "body":"正文", "published":"6/5/2013"},
{"id":5, "title":"第五篇", "body":"正文", "published":"6/10/2013"},
{"id":6, "title":"第六篇", "body":"正文", "published":"6/12/2013"}
];
exports.getBlogEntries = function (){
return entries;
}
exports.getBlogEntry = function (id){
for(var i=0; i < entries.length; i++){
if(entries[i].id == id) return entries[i];
}
}
```
### 新建网页模板
接着,新建模板文件index.html。
```html
<!-- views/index.html文件 -->
<h1>文章列表</h1>
{{"{{"}}#each entries}}
<p>
<a href="/article/{{"{{"}}id}}">{{"{{"}}title}}</a><br/>
Published: {{"{{"}}published}}
</p>
{{"{{"}}/each}}
```
模板文件about.html。
```html
<!-- views/about.html文件 -->
<h1>自我介绍</h1>
<p>正文</p>
```
模板文件article.html。
```html
<!-- views/article.html文件 -->
<h1>{{"{{"}}blog.title}}</h1>
Published: {{"{{"}}blog.published}}
<p/>
{{"{{"}}blog.body}}
```
可以看到,上面三个模板文件都只有网页主体。因为网页布局是共享的,所以布局的部分可以单独新建一个文件layout.html。
```html
<!-- views/layout.html文件 -->
<html>
<head>
<title>{{"{{"}}title}}</title>
</head>
<body>
{{"{{{"}}body}}}
<footer>
<p>
<a href="/">首页</a> - <a href="/about">自我介绍</a>
</p>
</footer>
</body>
</html>
```
### 渲染模板
最后,改写app.js文件。
```javascript
// app.js文件
var express = require('express');
var app = express();
var hbs = require('hbs');
// 加载数据模块
var blogEngine = require('./blog');
app.set('view engine', 'html');
app.engine('html', hbs.__express);
app.use(express.bodyParser());
app.get('/', function(req, res) {
res.render('index',{title:"最近文章", entries:blogEngine.getBlogEntries()});
});
app.get('/about', function(req, res) {
res.render('about', {title:"自我介绍"});
});
app.get('/article/:id', function(req, res) {
var entry = blogEngine.getBlogEntry(req.params.id);
res.render('article',{title:entry.title, blog:entry});
});
app.listen(3000);
```
上面代码中的render方法,现在加入了第二个参数,表示模板变量绑定的数据。
现在重启node服务器,然后访问http://127.0.0.1:3000。
```bash
node app.js
```
可以看得,模板已经使用加载的数据渲染成功了。
### 指定静态文件目录
模板文件默认存放在views子目录。这时,如果要在网页中加载静态文件(比如样式表、图片等),就需要另外指定一个存放静态文件的目录。
```javascript
app.use(express.static('public'));
```
上面代码在文件app.js之中,指定静态文件存放的目录是public。于是,当浏览器发出非HTML文件请求时,服务器端就到public目录寻找这个文件。比如,浏览器发出如下的样式表请求:
```javascript
<link href="/bootstrap/css/bootstrap.css" rel="stylesheet">
```
服务器端就到public/bootstrap/css/目录中寻找bootstrap.css文件。
## Express.Router用法
从Express 4.0开始,路由器功能成了一个单独的组件`Express.Router`。它好像小型的express应用程序一样,有自己的use、get、param和route方法。
### 基本用法
首先,`Express.Router`是一个构造函数,调用后返回一个路由器实例。然后,使用该实例的HTTP动词方法,为不同的访问路径,指定回调函数;最后,挂载到某个路径。
```javascript
var router = express.Router();
router.get('/', function(req, res) {
res.send('首页');
});
router.get('/about', function(req, res) {
res.send('关于');
});
app.use('/', router);
```
上面代码先定义了两个访问路径,然后将它们挂载到根目录。如果最后一行改为app.use('/app', router),则相当于为`/app`和`/app/about`这两个路径,指定了回调函数。
这种路由器可以自由挂载的做法,为程序带来了更大的灵活性,既可以定义多个路由器实例,也可以为将同一个路由器实例挂载到多个路径。
### router.route方法
router实例对象的route方法,可以接受访问路径作为参数。
```javascript
var router = express.Router();
router.route('/api')
.post(function(req, res) {
// ...
})
.get(function(req, res) {
Bear.find(function(err, bears) {
if (err) res.send(err);
res.json(bears);
});
});
app.use('/', router);
```
### router中间件
use方法为router对象指定中间件,即在数据正式发给用户之前,对数据进行处理。下面就是一个中间件的例子。
```javascript
router.use(function(req, res, next) {
console.log(req.method, req.url);
next();
});
```
上面代码中,回调函数的next参数,表示接受其他中间件的调用。函数体中的next(),表示将数据传递给下一个中间件。
注意,中间件的放置顺序很重要,等同于执行顺序。而且,中间件必须放在HTTP动词方法之前,否则不会执行。
### 对路径参数的处理
router对象的param方法用于路径参数的处理,可以
```javascript
router.param('name', function(req, res, next, name) {
// 对name进行验证或其他处理……
console.log(name);
req.name = name;
next();
});
router.get('/hello/:name', function(req, res) {
res.send('hello ' + req.name + '!');
});
```
上面代码中,get方法为访问路径指定了name参数,param方法则是对name参数进行处理。注意,param方法必须放在HTTP动词方法之前。
### app.route
假定app是Express的实例对象,Express 4.0为该对象提供了一个route属性。app.route实际上是express.Router()的缩写形式,除了直接挂载到根路径。因此,对同一个路径指定get和post方法的回调函数,可以写成链式形式。
```javascript
app.route('/login')
.get(function(req, res) {
res.send('this is the login form');
})
.post(function(req, res) {
console.log('processing');
res.send('processing the login form!');
});
```
上面代码的这种写法,显然非常简洁清晰。
## 上传文件
首先,在网页插入上传文件的表单。
```html
<form action="/pictures/upload" method="POST" enctype="multipart/form-data">
Select an image to upload:
<input type="file" name="image">
<input type="submit" value="Upload Image">
</form>
```
然后,服务器脚本建立指向`/upload`目录的路由。这时可以安装multer模块,它提供了上传文件的许多功能。
```javascript
var express = require('express');
var router = express.Router();
var multer = require('multer');
var uploading = multer({
dest: __dirname + '../public/uploads/',
// 设定限制,每次最多上传1个文件,文件大小不超过1MB
limits: {fileSize: 1000000, files:1},
})
router.post('/upload', uploading, function(req, res) {
})
module.exports = router
```
上面代码是上传文件到本地目录。下面是上传到Amazon S3的例子。
首先,在S3上面新增CORS配置文件。
```xml
<?xml version="1.0" encoding="UTF-8"?>
<CORSConfiguration xmlns="http://s3.amazonaws.com/doc/2006-03-01/">
<CORSRule>
<AllowedOrigin>*</AllowedOrigin>
<AllowedMethod>GET</AllowedMethod>
<AllowedMethod>POST</AllowedMethod>
<AllowedMethod>PUT</AllowedMethod>
<AllowedHeader>*</AllowedHeader>
</CORSRule>
</CORSConfiguration>
```
上面的配置允许任意电脑向你的bucket发送HTTP请求。
然后,安装aws-sdk。
```bash
$ npm install aws-sdk --save
```
下面是服务器脚本。
```javascript
var express = require('express');
var router = express.Router();
var aws = require('aws-sdk');
router.get('/', function(req, res) {
res.render('index')
})
var AWS_ACCESS_KEY = 'your_AWS_access_key'
var AWS_SECRET_KEY = 'your_AWS_secret_key'
var S3_BUCKET = 'images_upload'
router.get('/sign', function(req, res) {
aws.config.update({accessKeyId: AWS_ACCESS_KEY, secretAccessKey: AWS_SECRET_KEY});
var s3 = new aws.S3()
var options = {
Bucket: S3_BUCKET,
Key: req.query.file_name,
Expires: 60,
ContentType: req.query.file_type,
ACL: 'public-read'
}
s3.getSignedUrl('putObject', options, function(err, data){
if(err) return res.send('Error with S3')
res.json({
signed_request: data,
url: 'https://s3.amazonaws.com/' + S3_BUCKET + '/' + req.query.file_name
})
})
})
module.exports = router
```
上面代码中,用户访问`/sign`路径,正确登录后,会收到一个JSON对象,里面是S3返回的数据和一个暂时用来接收上传文件的URL,有效期只有60秒。
浏览器代码如下。
```javascript
// HTML代码为
// <br>Please select an image
// <input type="file" id="image">
// <br>
// <img id="preview">
document.getElementById("image").onchange = function() {
var file = document.getElementById("image").files[0]
if (!file) return
sign_request(file, function(response) {
upload(file, response.signed_request, response.url, function() {
document.getElementById("preview").src = response.url
})
})
}
function sign_request(file, done) {
var xhr = new XMLHttpRequest()
xhr.open("GET", "/sign?file_name=" + file.name + "&file_type=" + file.type)
xhr.onreadystatechange = function() {
if(xhr.readyState === 4 && xhr.status === 200) {
var response = JSON.parse(xhr.responseText)
done(response)
}
}
xhr.send()
}
function upload(file, signed_request, url, done) {
var xhr = new XMLHttpRequest()
xhr.open("PUT", signed_request)
xhr.setRequestHeader('x-amz-acl', 'public-read')
xhr.onload = function() {
if (xhr.status === 200) {
done()
}
}
xhr.send(file)
}
```
上面代码首先监听file控件的change事件,一旦有变化,就先向服务器要求一个临时的上传URL,然后向该URL上传文件。
<h2 id="12.18">Koa框架</h2>
Koa是一个类似于Express的Web开发框架,创始人也是同一个人。它的主要特点是,使用了ES6的Generator函数,进行了架构的重新设计。也就是说,Koa的原理和内部结构很像Express,但是语法和内部结构进行了升级。
官方[faq](https://github.com/koajs/koa/blob/master/docs/faq.md#why-isnt-koa-just-express-40)有这样一个问题:”为什么koa不是Express 4.0?“,回答是这样的:”Koa与Express有很大差异,整个设计都是不同的,所以如果将Express 3.0按照这种写法升级到4.0,就意味着重写整个程序。所以,我们觉得创造一个新的库,是更合适的做法。“
## Koa应用
一个Koa应用就是一个对象,包含了一个middleware数组,这个数组由一组Generator函数组成。这些函数负责对HTTP请求进行各种加工,比如生成缓存、指定代理、请求重定向等等。
```javascript
var koa = require('koa');
var app = koa();
app.use(function *(){
this.body = 'Hello World';
});
app.listen(3000);
```
上面代码中,变量app就是一个Koa应用。它监听3000端口,返回一个内容为Hello World的网页。
app.use方法用于向middleware数组添加Generator函数。
listen方法指定监听端口,并启动当前应用。它实际上等同于下面的代码。
```javascript
var http = require('http');
var koa = require('koa');
var app = koa();
http.createServer(app.callback()).listen(3000);
```
## 中间件
Koa的中间件很像Express的中间件,也是对HTTP请求进行处理的函数,但是必须是一个Generator函数。而且,Koa的中间件是一个级联式(Cascading)的结构,也就是说,属于是层层调用,第一个中间件调用第二个中间件,第二个调用第三个,以此类推。上游的中间件必须等到下游的中间件返回结果,才会继续执行,这点很像递归。
中间件通过当前应用的use方法注册。
```javascript
app.use(function* (next){
var start = new Date; // (1)
yield next; // (2)
var ms = new Date - start; // (3)
console.log('%s %s - %s', this.method, this.url, ms); // (4)
});
```
上面代码中,`app.use`方法的参数就是中间件,它是一个Generator函数,最大的特征就是function命令与参数之间,必须有一个星号。Generator函数的参数next,表示下一个中间件。
Generator函数内部使用yield命令,将程序的执行权转交给下一个中间件,即`yield next`,要等到下一个中间件返回结果,才会继续往下执行。上面代码中,Generator函数体内部,第一行赋值语句首先执行,开始计时,第二行yield语句将执行权交给下一个中间件,当前中间件就暂停执行。等到后面的中间件全部执行完成,执行权就回到原来暂停的地方,继续往下执行,这时才会执行第三行,计算这个过程一共花了多少时间,第四行将这个时间打印出来。
下面是一个两个中间件级联的例子。
```javascript
app.use(function *() {
this.body = "header\n";
yield saveResults.call(this);
this.body += "footer\n";
});
function *saveResults() {
this.body += "Results Saved!\n";
}
```
上面代码中,第一个中间件调用第二个中间件saveResults,它们都向`this.body`写入内容。最后,`this.body`的输出如下。
```javascript
header
Results Saved!
footer
```
只要有一个中间件缺少`yield next`语句,后面的中间件都不会执行,这一点要引起注意。
```javascript
app.use(function *(next){
console.log('>> one');
yield next;
console.log('<< one');
});
app.use(function *(next){
console.log('>> two');
this.body = 'two';
console.log('<< two');
});
app.use(function *(next){
console.log('>> three');
yield next;
console.log('<< three');
});
```
上面代码中,因为第二个中间件少了`yield next`语句,第三个中间件并不会执行。
如果想跳过一个中间件,可以直接在该中间件的第一行语句写上`return yield next`。
```javascript
app.use(function* (next) {
if (skip) return yield next;
})
```
由于Koa要求中间件唯一的参数就是next,导致如果要传入其他参数,必须另外写一个返回Generator函数的函数。
```javascript
function logger(format) {
return function *(next){
var str = format
.replace(':method', this.method)
.replace(':url', this.url);
console.log(str);
yield next;
}
}
app.use(logger(':method :url'));
```
上面代码中,真正的中间件是logger函数的返回值,而logger函数是可以接受参数的。
### 多个中间件的合并
由于中间件的参数统一为next(意为下一个中间件),因此可以使用`.call(this, next)`,将多个中间件进行合并。
```javascript
function *random(next) {
if ('/random' == this.path) {
this.body = Math.floor(Math.random()*10);
} else {
yield next;
}
};
function *backwards(next) {
if ('/backwards' == this.path) {
this.body = 'sdrawkcab';
} else {
yield next;
}
}
function *pi(next) {
if ('/pi' == this.path) {
this.body = String(Math.PI);
} else {
yield next;
}
}
function *all(next) {
yield random.call(this, backwards.call(this, pi.call(this, next)));
}
app.use(all);
```
上面代码中,中间件all内部,就是依次调用random、backwards、pi,后一个中间件就是前一个中间件的参数。
Koa内部使用koa-compose模块,进行同样的操作,下面是它的源码。
```javascript
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
yield *next;
}
}
function *noop(){}
```
上面代码中,middleware是中间件数组。前一个中间件的参数是后一个中间件,依次类推。如果最后一个中间件没有next参数,则传入一个空函数。
## 路由
可以通过`this.path`属性,判断用户请求的路径,从而起到路由作用。
```javascript
app.use(function* (next) {
if (this.path === '/') {
this.body = 'we are at home!';
} else {
yield next;
}
})
// 等同于
app.use(function* (next) {
if (this.path !== '/') return yield next;
this.body = 'we are at home!';
})
```
下面是多路径的例子。
```javascript
let koa = require('koa')
let app = koa()
// normal route
app.use(function* (next) {
if (this.path !== '/') {
return yield next
}
this.body = 'hello world'
});
// /404 route
app.use(function* (next) {
if (this.path !== '/404') {
return yield next;
}
this.body = 'page not found'
});
// /500 route
app.use(function* (next) {
if (this.path !== '/500') {
return yield next;
}
this.body = 'internal server error'
});
app.listen(8080)
```
上面代码中,每一个中间件负责一个路径,如果路径不符合,就传递给下一个中间件。
复杂的路由需要安装koa-router插件。
```javascript
var app = require('koa')();
var Router = require('koa-router');
var myRouter = new Router();
myRouter.get('/', function *(next) {
this.response.body = 'Hello World!';
});
app.use(myRouter.routes());
app.listen(3000);
```
上面代码对根路径设置路由。
Koa-router实例提供一系列动词方法,即一种HTTP动词对应一种方法。典型的动词方法有以下五种。
- router.get()
- router.post()
- router.put()
- router.del()
- router.patch()
这些动词方法可以接受两个参数,第一个是路径模式,第二个是对应的控制器方法(中间件),定义用户请求该路径时服务器行为。
```javascript
router.get('/', function *(next) {
this.body = 'Hello World!';
});
```
上面代码中,`router.get`方法的第一个参数是根路径,第二个参数是对应的函数方法。
注意,路径匹配的时候,不会把查询字符串考虑在内。比如,`/index?param=xyz`匹配路径`/index`。
有些路径模式比较复杂,Koa-router允许为路径模式起别名。起名时,别名要添加为动词方法的第一个参数,这时动词方法变成接受三个参数。
```javascript
router.get('user', '/users/:id', function *(next) {
// ...
});
```
上面代码中,路径模式`\users\:id`的名字就是`user`。路径的名称,可以用来引用对应的具体路径,比如url方法可以根据路径名称,结合给定的参数,生成具体的路径。
```javascript
router.url('user', 3);
// => "/users/3"
router.url('user', { id: 3 });
// => "/users/3"
```
上面代码中,user就是路径模式的名称,对应具体路径`/users/:id`。url方法的第二个参数3,表示给定id的值是3,因此最后生成的路径是`/users/3`。
Koa-router允许为路径统一添加前缀。
```javascript
var router = new Router({
prefix: '/users'
});
router.get('/', ...); // 等同于"/users"
router.get('/:id', ...); // 等同于"/users/:id"
```
路径的参数通过`this.params`属性获取,该属性返回一个对象,所有路径参数都是该对象的成员。
```javascript
// 访问 /programming/how-to-node
router.get('/:category/:title', function *(next) {
console.log(this.params);
// => { category: 'programming', title: 'how-to-node' }
});
```
param方法可以针对命名参数,设置验证条件。
```javascript
router
.get('/users/:user', function *(next) {
this.body = this.user;
})
.param('user', function *(id, next) {
var users = [ '0号用户', '1号用户', '2号用户'];
this.user = users[id];
if (!this.user) return this.status = 404;
yield next;
})
```
上面代码中,如果`/users/:user`的参数user对应的不是有效用户(比如访问`/users/3`),param方法注册的中间件会查到,就会返回404错误。
redirect方法会将某个路径的请求,重定向到另一个路径,并返回301状态码。
```javascript
router.redirect('/login', 'sign-in');
// 等同于
router.all('/login', function *() {
this.redirect('/sign-in');
this.status = 301;
});
```
redirect方法的第一个参数是请求来源,第二个参数是目的地,两者都可以用路径模式的别名代替。
## context对象
中间件当中的this表示上下文对象context,代表一次HTTP请求和回应,即一次访问/回应的所有信息,都可以从上下文对象获得。context对象封装了request和response对象,并且提供了一些辅助方法。每次HTTP请求,就会创建一个新的context对象。
```javascript
app.use(function *(){
this; // is the Context
this.request; // is a koa Request
this.response; // is a koa Response
});
```
context对象的很多方法,其实是定义在ctx.request对象或ctx.response对象上面,比如,ctx.type和ctx.length对应于ctx.response.type和ctx.response.length,ctx.path和ctx.method对应于ctx.request.path和ctx.request.method。
context对象的全局属性。
- request:指向Request对象
- response:指向Response对象
- req:指向Node的request对象
- res:指向Node的response对象
- app:指向App对象
- state:用于在中间件传递信息。
```javascript
this.state.user = yield User.find(id);
```
上面代码中,user属性存放在`this.state`对象上面,可以被另一个中间件读取。
context对象的全局方法。
- throw():抛出错误,直接决定了HTTP回应的状态码。
- assert():如果一个表达式为false,则抛出一个错误。
```javascript
this.throw(403);
this.throw('name required', 400);
this.throw('something exploded');
this.throw(400, 'name required');
// 等同于
var err = new Error('name required');
err.status = 400;
throw err;
```
assert方法的例子。
```javascript
// 格式
ctx.assert(value, [msg], [status], [properties])
// 例子
this.assert(this.user, 401, 'User not found. Please login!');
```
以下模块解析POST请求的数据。
- co-body
- https://github.com/koajs/body-parser
- https://github.com/koajs/body-parsers
```javascript
var parse = require('co-body');
// in Koa handler
var body = yield parse(this);
```
## 错误处理机制
Koa提供内置的错误处理机制,任何中间件抛出的错误都会被捕捉到,引发向客户端返回一个500错误,而不会导致进程停止,因此也就不需要forever这样的模块重启进程。
```javascript
app.use(function *() {
throw new Error();
});
```
上面代码中,中间件内部抛出一个错误,并不会导致Koa应用挂掉。Koa内置的错误处理机制,会捕捉到这个错误。
当然,也可以额外部署自己的错误处理机制。
```javascript
app.use(function *() {
try {
yield saveResults();
} catch (err) {
this.throw(400, '数据无效');
}
});
```
上面代码自行部署了try...catch代码块,一旦产生错误,就用`this.throw`方法抛出。该方法可以将指定的状态码和错误信息,返回给客户端。
对于未捕获错误,可以设置error事件的监听函数。
```javascript
app.on('error', function(err){
log.error('server error', err);
});
```
error事件的监听函数还可以接受上下文对象,作为第二个参数。
```javascript
app.on('error', function(err, ctx){
log.error('server error', err, ctx);
});
```
如果一个错误没有被捕获,koa会向客户端返回一个500错误“Internal Server Error”。
this.throw方法用于向客户端抛出一个错误。
```javascript
this.throw(403);
this.throw('name required', 400);
this.throw(400, 'name required');
this.throw('something exploded');
this.throw('name required', 400)
// 等同于
var err = new Error('name required');
err.status = 400;
throw err;
```
`this.throw`方法的两个参数,一个是错误码,另一个是报错信息。如果省略状态码,默认是500错误。
`this.assert`方法用于在中间件之中断言,用法类似于Node的assert模块。
```javascript
this.assert(this.user, 401, 'User not found. Please login!');
```
上面代码中,如果this.user属性不存在,会抛出一个401错误。
由于中间件是层级式调用,所以可以把`try { yield next }`当成第一个中间件。
```javascript
app.use(function *(next) {
try {
yield next;
} catch (err) {
this.status = err.status || 500;
this.body = err.message;
this.app.emit('error', err, this);
}
});
app.use(function *(next) {
throw new Error('some error');
})
```
## cookie
cookie的读取和设置。
```javascript
this.cookies.get('view');
this.cookies.set('view', n);
```
get和set方法都可以接受第三个参数,表示配置参数。其中的signed参数,用于指定cookie是否加密。如果指定加密的话,必须用`app.keys`指定加密短语。
```javascript
app.keys = ['secret1', 'secret2'];
this.cookies.set('name', '张三', { signed: true });
```
this.cookie的配置对象的属性如下。
- signed:cookie是否加密。
- expires:cookie何时过期
- path:cookie的路径,默认是“/”。
- domain:cookie的域名。
- secure:cookie是否只有https请求下才发送。
- httpOnly:是否只有服务器可以取到cookie,默认为true。
## session
```javascript
var session = require('koa-session');
var koa = require('koa');
var app = koa();
app.keys = ['some secret hurr'];
app.use(session(app));
app.use(function *(){
var n = this.session.views || 0;
this.session.views = ++n;
this.body = n + ' views';
})
app.listen(3000);
console.log('listening on port 3000');
```
## Request对象
Request对象表示HTTP请求。
(1)this.request.header
返回一个对象,包含所有HTTP请求的头信息。它也可以写成`this.request.headers`。
(2)this.request.method
返回HTTP请求的方法,该属性可读写。
(3)this.request.length
返回HTTP请求的Content-Length属性,取不到值,则返回undefined。
(4)this.request.path
返回HTTP请求的路径,该属性可读写。
(5)this.request.href
返回HTTP请求的完整路径,包括协议、端口和url。
```javascript
this.request.href
// http://example.com/foo/bar?q=1
```
(6)this.request.querystring
返回HTTP请求的查询字符串,不含问号。该属性可读写。
(7)this.request.search
返回HTTP请求的查询字符串,含问号。该属性可读写。
(8)this.request.host
返回HTTP请求的主机(含端口号)。
(9)this.request.hostname
返回HTTP的主机名(不含端口号)。
(10)this.request.type
返回HTTP请求的Content-Type属性。
```javascript
var ct = this.request.type;
// "image/png"
```
(11)this.request.charset
返回HTTP请求的字符集。
```javascript
this.request.charset
// "utf-8"
```
(12)this.request.query
返回一个对象,包含了HTTP请求的查询字符串。如果没有查询字符串,则返回一个空对象。该属性可读写。
比如,查询字符串`color=blue&size=small`,会得到以下的对象。
```javascript
{
color: 'blue',
size: 'small'
}
```
(13)this.request.fresh
返回一个布尔值,表示缓存是否代表了最新内容。通常与If-None-Match、ETag、If-Modified-Since、Last-Modified等缓存头,配合使用。
```javascript
this.response.set('ETag', '123');
// 检查客户端请求的内容是否有变化
if (this.request.fresh) {
this.response.status = 304;
return;
}
// 否则就表示客户端的内容陈旧了,
// 需要取出新内容
this.response.body = yield db.find('something');
```
(14)this.request.stale
返回`this.request.fresh`的相反值。
(15)this.request.protocol
返回HTTP请求的协议,https或者http。
(16)this.request.secure
返回一个布尔值,表示当前协议是否为https。
(17)this.request.ip
返回发出HTTP请求的IP地址。
(18)this.request.subdomains
返回一个数组,表示HTTP请求的子域名。该属性必须与app.subdomainOffset属性搭配使用。app.subdomainOffset属性默认为2,则域名“tobi.ferrets.example.com”返回["ferrets", "tobi"],如果app.subdomainOffset设为3,则返回["tobi"]。
(19)this.request.is(types...)
返回指定的类型字符串,表示HTTP请求的Content-Type属性是否为指定类型。
```javascript
// Content-Type为 text/html; charset=utf-8
this.request.is('html'); // 'html'
this.request.is('text/html'); // 'text/html'
this.request.is('text/*', 'text/html'); // 'text/html'
// Content-Type为 application/json
this.request.is('json', 'urlencoded'); // 'json'
this.request.is('application/json'); // 'application/json'
this.request.is('html', 'application/*'); // 'application/json'
```
如果不满足条件,返回false;如果HTTP请求不含数据,则返回undefined。
```javascript
this.is('html'); // false
```
它可以用于过滤HTTP请求,比如只允许请求下载图片。
```javascript
if (this.is('image/*')) {
// process
} else {
this.throw(415, 'images only!');
}
```
(20)this.request.accepts(types)
检查HTTP请求的Accept属性是否可接受,如果可接受,则返回指定的媒体类型,否则返回false。
```javascript
// Accept: text/html
this.request.accepts('html');
// "html"
// Accept: text/*, application/json
this.request.accepts('html');
// "html"
this.request.accepts('text/html');
// "text/html"
this.request.accepts('json', 'text');
// => "json"
this.request.accepts('application/json');
// => "application/json"
// Accept: text/*, application/json
this.request.accepts('image/png');
this.request.accepts('png');
// false
// Accept: text/*;q=.5, application/json
this.request.accepts(['html', 'json']);
this.request.accepts('html', 'json');
// "json"
// No Accept header
this.request.accepts('html', 'json');
// "html"
this.request.accepts('json', 'html');
// => "json"
```
如果accepts方法没有参数,则返回所有支持的类型(text/html,application/xhtml+xml,image/webp,application/xml,*/*)。
如果accepts方法的参数有多个参数,则返回最佳匹配。如果都不匹配则返回false,并向客户端抛出一个406”Not Acceptable“错误。
如果HTTP请求没有Accept字段,那么accepts方法返回它的第一个参数。
accepts方法可以根据不同Accept字段,向客户端返回不同的字段。
```javascript
switch (this.request.accepts('json', 'html', 'text')) {
case 'json': break;
case 'html': break;
case 'text': break;
default: this.throw(406, 'json, html, or text only');
}
```
(21)this.request.acceptsEncodings(encodings)
该方法根据HTTP请求的Accept-Encoding字段,返回最佳匹配,如果没有合适的匹配,则返回false。
```javascript
// Accept-Encoding: gzip
this.request.acceptsEncodings('gzip', 'deflate', 'identity');
// "gzip"
this.request.acceptsEncodings(['gzip', 'deflate', 'identity']);
// "gzip"
```
注意,acceptEncodings方法的参数必须包括identity(意为不编码)。
如果HTTP请求没有Accept-Encoding字段,acceptEncodings方法返回所有可以提供的编码方法。
```javascript
// Accept-Encoding: gzip, deflate
this.request.acceptsEncodings();
// ["gzip", "deflate", "identity"]
```
如果都不匹配,acceptsEncodings方法返回false,并向客户端抛出一个406“Not Acceptable”错误。
(22)this.request.acceptsCharsets(charsets)
该方法根据HTTP请求的Accept-Charset字段,返回最佳匹配,如果没有合适的匹配,则返回false。
```javascript
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
this.request.acceptsCharsets('utf-8', 'utf-7');
// => "utf-8"
this.request.acceptsCharsets(['utf-7', 'utf-8']);
// => "utf-8"
```
如果acceptsCharsets方法没有参数,则返回所有可接受的匹配。
```javascript
// Accept-Charset: utf-8, iso-8859-1;q=0.2, utf-7;q=0.5
this.request.acceptsCharsets();
// ["utf-8", "utf-7", "iso-8859-1"]
```
如果都不匹配,acceptsCharsets方法返回false,并向客户端抛出一个406“Not Acceptable”错误。
(23)this.request.acceptsLanguages(langs)
该方法根据HTTP请求的Accept-Language字段,返回最佳匹配,如果没有合适的匹配,则返回false。
```javascript
// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages('es', 'en');
// "es"
this.request.acceptsLanguages(['en', 'es']);
// "es"
```
如果acceptsCharsets方法没有参数,则返回所有可接受的匹配。
```javascript
// Accept-Language: en;q=0.8, es, pt
this.request.acceptsLanguages();
// ["es", "pt", "en"]
```
如果都不匹配,acceptsLanguages方法返回false,并向客户端抛出一个406“Not Acceptable”错误。
(24)this.request.socket
返回HTTP请求的socket。
(25)this.request.get(field)
返回HTTP请求指定的字段。
## Response对象
Response对象表示HTTP回应。
(1)this.response.header
返回HTTP回应的头信息。
(2)this.response.socket
返回HTTP回应的socket。
(3)this.response.status
返回HTTP回应的状态码。默认情况下,该属性没有值。该属性可读写,设置时等于一个整数。
(4)this.response.message
返回HTTP回应的状态信息。该属性与`this.response.message`是配对的。该属性可读写。
(5)this.response.length
返回HTTP回应的Content-Length字段。该属性可读写,如果没有设置它的值,koa会自动从this.request.body推断。
(6)this.response.body
返回HTTP回应的信息体。该属性可读写,它的值可能有以下几种类型。
- 字符串:Content-Type字段默认为text/html或text/plain,字符集默认为utf-8,Content-Length字段同时设定。
- 二进制Buffer:Content-Type字段默认为application/octet-stream,Content-Length字段同时设定。
- Stream:Content-Type字段默认为application/octet-stream。
- JSON对象:Content-Type字段默认为application/json。
- null(表示没有信息体)
如果`this.response.status`没设置,Koa会自动将其设为200或204。
(7)this.response.get(field)
返回HTTP回应的指定字段。
```javascript
var etag = this.get('ETag');
```
注意,get方法的参数是区分大小写的。
(8)this.response.set()
设置HTTP回应的指定字段。
```javascript
this.set('Cache-Control', 'no-cache');
```
set方法也可以接受一个对象作为参数,同时为多个字段指定值。
```javascript
this.set({
'Etag': '1234',
'Last-Modified': date
});
```
(9)this.response.remove(field)
移除HTTP回应的指定字段。
(10)this.response.type
返回HTTP回应的Content-Type字段,不包括“charset”参数的部分。
```javascript
var ct = this.reponse.type;
// "image/png"
```
该属性是可写的。
```javascript
this.reponse.type = 'text/plain; charset=utf-8';
this.reponse.type = 'image/png';
this.reponse.type = '.png';
this.reponse.type = 'png';
```
设置type属性的时候,如果没有提供charset参数,Koa会判断是否自动设置。如果`this.response.type`设为html,charset默认设为utf-8;但如果`this.response.type`设为text/html,就不会提供charset的默认值。
(10)this.response.is(types...)
该方法类似于`this.request.is()`,用于检查HTTP回应的类型是否为支持的类型。
它可以在中间件中起到处理不同格式内容的作用。
```javascript
var minify = require('html-minifier');
app.use(function *minifyHTML(next){
yield next;
if (!this.response.is('html')) return;
var body = this.response.body;
if (!body || body.pipe) return;
if (Buffer.isBuffer(body)) body = body.toString();
this.response.body = minify(body);
});
```
上面代码是一个中间件,如果输出的内容类型为HTML,就会进行最小化处理。
(11)this.response.redirect(url, [alt])
该方法执行302跳转到指定网址。
```javascript
this.redirect('back');
this.redirect('back', '/index.html');
this.redirect('/login');
this.redirect('http://google.com');
```
如果redirect方法的第一个参数是back,将重定向到HTTP请求的Referrer字段指定的网址,如果没有该字段,则重定向到第二个参数或“/”网址。
如果想修改302状态码,或者修改body文字,可以采用下面的写法。
```javascript
this.status = 301;
this.redirect('/cart');
this.body = 'Redirecting to shopping cart';
```
(12)this.response.attachment([filename])
该方法将HTTP回应的Content-Disposition字段,设为“attachment”,提示浏览器下载指定文件。
(13)this.response.headerSent
该方法返回一个布尔值,检查是否HTTP回应已经发出。
(14)this.response.lastModified
该属性以Date对象的形式,返回HTTP回应的Last-Modified字段(如果该字段存在)。该属性可写。
```javascript
this.response.lastModified = new Date();
```
(15)this.response.etag
该属性设置HTTP回应的ETag字段。
```javascript
this.response.etag = crypto.createHash('md5').update(this.body).digest('hex');
```
注意,不能用该属性读取ETag字段。
(16)this.response.vary(field)
该方法将参数添加到HTTP回应的Vary字段。
## CSRF攻击
CSRF攻击是指用户的session被劫持,用来冒充用户的攻击。
koa-csrf插件用来防止CSRF攻击。原理是在session之中写入一个秘密的token,用户每次使用POST方法提交数据的时候,必须含有这个token,否则就会抛出错误。
```javascript
var koa = require('koa');
var session = require('koa-session');
var csrf = require('koa-csrf');
var route = require('koa-route');
var app = module.exports = koa();
app.keys = ['session key', 'csrf example'];
app.use(session(app));
app.use(csrf());
app.use(route.get('/token', token));
app.use(route.post('/post', post));
function* token () {
this.body = this.csrf;
}
function* post() {
this.body = {ok: true};
}
app.listen(3000);
```
POST请求含有token,可以是以下几种方式之一,koa-csrf插件就能获得token。
- 表单的_csrf字段
- 查询字符串的_csrf字段
- HTTP请求头信息的x-csrf-token字段
- HTTP请求头信息的x-xsrf-token字段
## 数据压缩
koa-compress模块可以实现数据压缩。
```javascript
app.use(require('koa-compress')())
app.use(function* () {
this.type = 'text/plain'
this.body = fs.createReadStream('filename.txt')
})
```
## 源码解读
每一个网站就是一个app,它由`lib/application`定义。
```javascript
function Application() {
if (!(this instanceof Application)) return new Application;
this.env = process.env.NODE_ENV || 'development';
this.subdomainOffset = 2;
this.middleware = [];
this.context = Object.create(context);
this.request = Object.create(request);
this.response = Object.create(response);
}
var app = Application.prototype;
exports = module.exports = Application;
```
`app.use()`用于注册中间件,即将Generator函数放入中间件数组。
```javascript
app.use = function(fn){
if (!this.experimental) {
// es7 async functions are allowed
assert(fn && 'GeneratorFunction' == fn.constructor.name, 'app.use() requires a generator function');
}
debug('use %s', fn._name || fn.name || '-');
this.middleware.push(fn);
return this;
};
```
`app.listen()`就是`http.createServer(app.callback()).listen(...)`的缩写。
```javascript
app.listen = function(){
debug('listen');
var server = http.createServer(this.callback());
return server.listen.apply(server, arguments);
};
app.callback = function(){
var mw = [respond].concat(this.middleware);
var fn = this.experimental
? compose_es7(mw)
: co.wrap(compose(mw));
var self = this;
if (!this.listeners('error').length) this.on('error', this.onerror);
return function(req, res){
res.statusCode = 404;
var ctx = self.createContext(req, res);
onFinished(res, ctx.onerror);
fn.call(ctx).catch(ctx.onerror);
}
};
```
上面代码中,`app.callback()`会返回一个函数,用来处理HTTP请求。它的第一行`mw = [respond].concat(this.middleware)`,表示将respond函数(这也是一个Generator函数)放入`this.middleware`,现在mw就变成了`[respond, S1, S2, S3]`。
`compose(mw)`将中间件数组转为一个层层调用的Generator函数。
```javascript
function compose(middleware){
return function *(next){
if (!next) next = noop();
var i = middleware.length;
while (i--) {
next = middleware[i].call(this, next);
}
yield *next;
}
}
function *noop(){}
```
上面代码中,下一个generator函数总是上一个Generator函数的参数,从而保证了层层调用。
`var fn = co.wrap(gen)`则是将Generator函数包装成一个自动执行的函数,并且返回一个Promise。
```javascript
//co package
co.wrap = function (fn) {
return function () {
return co.call(this, fn.apply(this, arguments));
};
};
```
由于`co.wrap(compose(mw))`执行后,返回的是一个Promise,所以可以对其使用catch方法指定捕捉错误的回调函数`fn.call(ctx).catch(ctx.onerror)`。
将所有的上下文变量都放进context对象。
```javascript
app.createContext = function(req, res){
var context = Object.create(this.context);
var request = context.request = Object.create(this.request);
var response = context.response = Object.create(this.response);
context.app = request.app = response.app = this;
context.req = request.req = response.req = req;
context.res = request.res = response.res = res;
request.ctx = response.ctx = context;
request.response = response;
response.request = request;
context.onerror = context.onerror.bind(context);
context.originalUrl = request.originalUrl = req.url;
context.cookies = new Cookies(req, res, this.keys);
context.accept = request.accept = accepts(req);
context.state = {};
return context;
};
```
真正处理HTTP请求的是下面这个Generator函数。
```javascript
function *respond(next) {
yield *next;
// allow bypassing koa
if (false === this.respond) return;
var res = this.res;
if (res.headersSent || !this.writable) return;
var body = this.body;
var code = this.status;
// ignore body
if (statuses.empty[code]) {
// strip headers
this.body = null;
return res.end();
}
if ('HEAD' == this.method) {
if (isJSON(body)) this.length = Buffer.byteLength(JSON.stringify(body));
return res.end();
}
// status body
if (null == body) {
this.type = 'text';
body = this.message || String(code);
this.length = Buffer.byteLength(body);
return res.end(body);
}
// responses
if (Buffer.isBuffer(body)) return res.end(body);
if ('string' == typeof body) return res.end(body);
if (body instanceof Stream) return body.pipe(res);
// body: json
body = JSON.stringify(body);
this.length = Buffer.byteLength(body);
res.end(body);
}
```