💎一站式轻松地调用各大LLM模型接口,支持GPT4、智谱、星火、月之暗面及文生图 广告
<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)+指定版本**:比如&#710;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); } ```