七天学会NodeJS

zhouk_49

贡献于2014-03-25

字数:0 关键词: Node.js 开发

1.0 七天学会 NodeJS http://www.himysql.com 整理精 装PDF版 1 目录目录 版权声明版权声明 ........................................................................................................................................................................................................................................................................................................iviv 第 1 章 NodeJS基础第 1 章 NodeJS基础 ......................................................................................................................................................................................................................................................................11 1.1 什么是NodeJS.........................................................................................................................1 1.2 有啥用处..................................................................................................................................1 1.3 如何安装..................................................................................................................................2 1.4 如何运行..................................................................................................................................2 1.5 模块..........................................................................................................................................3 1.6 二进制模块..............................................................................................................................6 1.7 小结..........................................................................................................................................6 第 2 章 代码的组织和部署第 2 章 代码的组织和部署..................................................................................................................................................................................................................................................77 2.1 模块路径解析规则..................................................................................................................7 2.2 包(package) ........................................................................................................................8 2.3 命令行程序............................................................................................................................10 2.4 工程目录................................................................................................................................11 2.5 NPM.......................................................................................................................................12 2.6 小结........................................................................................................................................16 第 3 章 文件操作第 3 章 文件操作..............................................................................................................................................................................................................................................................................1717 3.1 开门红....................................................................................................................................17 3.2 API走马观花.........................................................................................................................18 3.3 遍历目录................................................................................................................................25 3.4 文本编码................................................................................................................................28 3.5 小结........................................................................................................................................30 第 4 章 网络操作第 4 章 网络操作..............................................................................................................................................................................................................................................................................3232 4.1 开门红....................................................................................................................................32 4.2 API走马观花.........................................................................................................................33 4.3 灵机一点................................................................................................................................43 4.4 小结........................................................................................................................................44 第 5 章 进程管理第 5 章 进程管理..............................................................................................................................................................................................................................................................................4545 5.1 开门红....................................................................................................................................45 ii 5.2 API走马观花.........................................................................................................................45 5.3 应用场景................................................................................................................................46 5.4 小结........................................................................................................................................50 第 6 章 异步编程第 6 章 异步编程..............................................................................................................................................................................................................................................................................5252 6.1 回调........................................................................................................................................52 6.2 代码设计模式........................................................................................................................54 6.3 域............................................................................................................................................60 6.4 小结........................................................................................................................................63 第 7 章 大示例第 7 章 大示例......................................................................................................................................................................................................................................................................................6464 7.1 需求........................................................................................................................................64 7.2 第一次迭代............................................................................................................................64 7.3 第二次迭代............................................................................................................................67 7.4 第三次迭代............................................................................................................................70 7.5 第四次迭代............................................................................................................................72 7.6 后续迭代................................................................................................................................74 7.7 小结........................................................................................................................................74 第 8 章 关于我们第 8 章 关于我们..............................................................................................................................................................................................................................................................................7575 8.1 工作机会................................................................................................................................75 目录 iii 版权声明版权声明 本 PDF 为了方便移动设备阅读,根据网络资源整理而成,版权由原作者所有。本书内容来自 http://nqdeng.github.io/7-days-nodejs/ ,在此向原作者表示感谢。 iv 第第 1 章章 NodeJS基础基础 1.1 什么是什么是NodeJS JS是脚本语言,脚本语言都需要一个解析器才能运行。对于写在HTML页面里的JS,浏览器充当 了解析器的角色。而对于需要独立运行的JS,NodeJS就是一个解析器。 每一种解析器都是一个运行环境,不但允许JS定义各种数据结构,进行各种计算,还允许JS使用 运行环境提供的内置对象和方法做一些事情。例如运行在浏览器中的JS的用途是操作DOM,浏 览器就提供了 document 之类的内置对象。而运行在NodeJS中的JS的用途是操作磁盘文件或搭 建HTTP服务器,NodeJS就相应提供了 fs、http 等内置对象。 1.2 有啥用处有啥用处 尽管存在一听说可以直接运行JS文件就觉得很酷的同学,但大多数同学在接触新东西时首先关心 的是有啥用处,以及能带来啥价值。 NodeJS的作者说,他创造NodeJS的目的是为了实现高性能Web服务器,他首先看重的是事件机 制和异步IO模型的优越性,而不是JS。但是他需要选择一种编程语言实现他的想法,这种编程语 言不能自带IO功能,并且需要能良好支持事件机制。JS没有自带IO功能,天生就用于处理浏览 器中的DOM事件,并且拥有一大群程序员,因此就成为了天然的选择。 如他所愿,NodeJS在服务端活跃起来,出现了大批基于NodeJS的Web服务。而另一方面, NodeJS让前端众如获神器,终于可以让自己的能力覆盖范围跳出浏览器窗口,更大批的前端工 具如雨后春笋。 因此,对于前端而言,虽然不是人人都要拿NodeJS写一个服务器程序,但简单可至使用命令交 互模式调试JS代码片段,复杂可至编写工具提升工作效率。 NodeJS生态圈正欣欣向荣。 第 1 章 NodeJS基础 1 1.3 如何安装如何安装 1.3.1 安装程序安装程序 NodeJS提供了一些安装程序,都可以在 nodejs.org 这里下载并安装。 Windows系统下,选择和系统版本匹配的 .msi 后缀的安装文件。Mac OS X系统下,选择 .pkg 后缀的安装文件。 1.3.2 编译安装编译安装 Linux系统下没有现成的安装程序可用,虽然一些发行版可以使用apt-get之类的方式安装,但不 一定能安装到最新版。因此Linux系统下一般使用以下方式编译方式安装NodeJS。 1. 确保系统下g++版本在4.6以上,python版本在2.6以上。 2. 从 nodejs.org 下载 tar.gz 后缀的NodeJS最新版源代码包并解压到某个位置。 3. 进入解压到的目录,使用以下命令编译和安装。 $ ./configure $ make $ sudo make install 1.4 如何运行如何运行 打开终端,键入 node 进入命令交互模式,可以输入一条代码语句后立即执行并显示结果,例 如: $ node > console.log(('Hello World!')) Hello World! 如果要运行一大段代码的话,可以先写一个JS文件再运行。例如有以下 hello.js。 functionfunction hello(){ console.log("Hello World!"); } hello(); 七天学会NodeJS http://www.himysql.com 整理精装PDF版 2 写好后在终端下键入 node hello.js 运行,结果如下: $ node hello.js Hello World! 1.4.1 权限问题权限问题 在Linux系统下,使用NodeJS监听80或443端口提供HTTP(S)服务时需要root权限,有两种方式可 以做到。 一种方式是使用 sudo 命令运行NodeJS。例如通过以下命令运行的 server.js 中有权限使用80 和443端口。一般推荐这种方式,可以保证仅为有需要的JS脚本提供root权限。 $ sudo node server.js 另一种方式是使用 chmod +s 命令让NodeJS总是以root权限运行,具体做法如下。因为这种方式 让任何JS脚本都有了root权限,不太安全,因此在需要很考虑安全的系统下不推荐使用。 $ sudo chown root /usr/local/bin/node $ sudo chmod +s /usr/local/bin/node 1.5 模块模块 编写稍大一点的程序时一般都会将代码模块化。在NodeJS中,一般将代码合理拆分到不同的JS 文件中,每一个文件就是一个模块,而文件路径就是模块名。 在编写每个模块时,都有 require、exports、module 三个预先定义好的变量可供使用。 1.5.1 require require 函数用于在当前模块中加载和使用别的模块,传入一个模块名,返回一个模块导出对 象。模块名可使用相对路径(以 ./ 开头),或者是绝对路径(以 / 或 C: 之类的盘符开头)。 另外,模块名中的 .js 扩展名可以省略。以下是一个例子。 varvar foo1 == require('./foo'); varvar foo2 == require('./foo.js'); varvar foo3 == require('/home/user/foo'); 第 1 章 NodeJS基础 3 varvar foo4 == require('/home/user/foo.js'); // foo1至foo4中保存的是同一个模块的导出对象。 另外,可以使用以下方式加载和使用一个JSON文件,模块名中 .json 扩展名不可省略。 varvar data == require('./data.json'); 1.5.2 exports exports 对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过 require 函数使用当前模块时得到的就是当前模块的 exports 对象。以下例子中导出了一个公有方法。 exports.hello == functionfunction () { console.log("Hello World!"); } 1.5.3 module 通过 module 对象可以访问到当前模块的一些相关信息,但最多的用途是替换当前模块的导出 对象。例如模块导出对象默认是一个普通对象,如果想改成一个函数的话,可以使用以下方式。 module.exports == functionfunction () { console.log("Hello World!"); } 以上代码中,模块默认导出对象被替换为一个函数。 1.5.4 模块初始化模块初始化 一个模块中的JS代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对 象。之后,缓存起来的导出对象被重复利用。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 4 1.5.5 主模块主模块 通过命令行参数传递给NodeJS以启动程序的模块被称为主模块。主模块负责调度组成整个程序 的其它模块完成工作。例如通过以下命令启动程序时, main.js 就是主模块。 $ node main.js 1.5.6 完整示例完整示例 例如有以下目录。 - /home/user/hello/ - util/ counter.js main.js 其中 counter.js 内容如下: varvar i == 0; functionfunction count() { returnreturn ++++i; } exports.count == count; 该模块内部定义了一个私有变量 i,并在 exports 对象导出了一个公有方法 count。 主模块 main.js 内容如下: varvar counter1 == require('./util/counter'); varvar counter2 == require('./util/counter'); console.log(counter1.count()); console.log(counter2.count()); console.log(counter2.count()); 第 1 章 NodeJS基础 5 运行该程序的结果如下: $ node main.js 1 2 3 可以看到, counter.js 并没有因为被require了两次而初始化两次。 1.6 二进制模块二进制模块 虽然一般我们使用JS编写模块,但NodeJS也支持使用C/C++编写二进制模块。编译好的二进制模 块除了文件扩展名是 .node 外,和JS模块的使用方式相同。虽然二进制模块能使用操作系统提 供的所有功能,拥有无限的潜能,但对于前端同学而言编写过于困难,并且难以跨平台使用,因 此不在本教程的覆盖范围内。 1.7 小结小结 本章介绍了有关NodeJS的基本概念和使用方法,总结起来有以下知识点: • NodeJS是一个JS脚本解析器,任何操作系统下安装NodeJS本质上做的事情都是把 NodeJS 执行程序复制到一个目录,然后保证这个目录在系统PATH环境变量下,以便终 端下可以使用node命令。 • 终端下直接输入 node 命令可进入命令交互模式,很适合用来测试一些JS代码片段,比 如正则表达式。 • NodeJS使用 CMD 模块系统,主模块作为程序入口点,所有模块在执行过程中只初始化 一次。 • 除非JS模块不能满足需求,否则不要轻易使用二进制模块,否则你的用户会叫苦连天。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 6 第第 2 章章 代码的组织和部署代码的组织和部署 有经验的C程序员在编写一个新程序时首先从make文件写起。同样的,使用NodeJS编写程序前, 为了有个良好的开端,首先需要准备好代码的目录结构和部署方式,就如同修房子要先搭脚手 架。本章将介绍与之相关的各种知识。 2.1 模块路径解析规则模块路径解析规则 我们已经知道,require 函数支持斜杠(/)或盘符(C:)开头的绝对路径,也支持 ./ 开头的 相对路径。但这两种路径在模块之间建立了强耦合关系,一旦某个模块文件的存放位置需要变 更,使用该模块的其它模块的代码也需要跟着调整,变得牵一发动全身。因此,require 函数 支持第三种形式的路径,写法类似于 foo/bar,并依次按照以下规则解析路径,直到找到模块 位置。 1. 内置模块 如果传递给 require 函数的是NodeJS内置模块名称,不做路径解析,直接返回内部模 块的导出对象,例如 require('fs')。 2. node_modules目录 NodeJS定义了一个特殊的 node_modules 目录用于存放模块。例如某个模块的绝对路 径是 /home/user/hello.js,在该模块中使用 require('foo/bar') 方式加载模 块时,则NodeJS依次尝试使用以下路径。 ◦ /home/user/node_modules/foo/bar ◦ /home/node_modules/foo/bar ◦ /node_modules/foo/bar 3. NODE_PATH环境变量 与PATH环境变量类似,NodeJS允许通过NODE_PATH环境变量来指定额外的模块搜索路 径。NODE_PATH环境变量中包含一到多个目录路径,路径之间在*nix下使用 : 分隔, 在Windows下使用;分隔。例如定义了以下NODE_PATH环境变量: 第 2 章 代码的组织和部署 7 NODE_PATH=/home/user/lib:/home/lib 当使用 require('foo/bar') 的方式加载模块时,则NodeJS依次尝试以下路径。 ◦ /home/user/lib/foo/bar ◦ /home/lib/foo/bar 2.2 包(包(package)) 我们已经知道了JS模块的基本单位是单个JS文件,但复杂些的模块往往由多个子模块组成。为了 便于管理和使用,我们可以把由多个子模块组成的大模块称做包,并把所有子模块放在同一个目 录里。 在组成一个包的所有子模块中,需要有一个入口模块,入口模块的导出对象被作为包的导出对 象。例如有以下目录结构。 - /home/user/lib/ - cat/ head.js body.js main.js 其中 cat 目录定义了一个包,其中包含了3个子模块。main.js 作为入口模块,其内容如下: varvar head == require('./head'); varvar body == require('./body'); exports.create == functionfunction (name) { returnreturn { name:: name, head:: head.create(), body:: body.create() }; }; 在其它模块里使用包的时候,需要加载包的入口模块。接着上例,使用 require('/home/ user/lib/cat/main') 能达到目的,但是入口模块名称出现在路径里看上去不是个好主意。 因此我们需要做点额外的工作,让包使用起来更像是单个模块。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 8 2.2.1 index.js 当模块的文件名是 index.js,加载模块时可以使用模块所在目录的路径代替模块文件路径, 因此接着上例,以下两条语句等价。 varvar cat == require('/home/user/lib/cat'); varvar cat == require('/home/user/lib/cat/index'); 这样处理后,就只需要把包目录路径传递给 require 函数,感觉上整个目录被当作单个模块使 用,更有整体感。 2.2.2 package.json 如果想自定义入口模块的文件名和存放位置,就需要在包目录下包含一个 package.json 文 件,并在其中指定入口模块的路径。上例中的 cat 模块可以重构如下。 - /home/user/lib/ - cat/ + doc/ - lib/ head.js body.js main.js + tests/ package.json 其中 package.json 内容如下。 { "name": "cat", "main": "./lib/main.js" } 如此一来,就同样可以使用 require('/home/user/lib/cat') 的方式加载模块。NodeJS 会根据包目录下的 package.json 找到入口模块所在位置。 第 2 章 代码的组织和部署 9 2.3 命令行程序命令行程序 使用NodeJS编写的东西,要么是一个包,要么是一个命令行程序,而前者最终也会用于开发后 者。因此我们在部署代码时需要一些技巧,让用户觉得自己是在使用一个命令行程序。 例如我们用NodeJS写了个程序,可以把命令行参数原样打印出来。该程序很简单,在主模块内 实现了所有功能。并且写好后,我们把该程序部署在 /home/user/bin/node-echo.js 这个 位置。为了在任何目录下都能运行该程序,我们需要使用以下终端命令。 $ node /home/user/bin/node-echo.js Hello World Hello World 这种使用方式看起来不怎么像是一个命令行程序,下边的才是我们期望的方式。 $ node-echo Hello World *nix 在*nix系统下,我们可以把JS文件当作shell脚本来运行,从而达到上述目的,具体步骤如下: 1. 在shell脚本中,可以通过 #! 注释来指定当前脚本使用的解析器。所以我们首先在 node-echo.js 文件顶部增加以下一行注释,表明当前脚本使用NodeJS解析。 #! /usr/bin/env node NodeJS会忽略掉位于JS模块首行的 #! 注释,不必担心这行注释是非法语句。 2. 然后,我们使用以下命令赋予 node-echo.js 文件执行权限。 $ chmod +x /home/user/bin/node-echo.js 3. 最后,我们在PATH环境变量中指定的某个目录下,例如在 /usr/local/bin 下边创建 一个软链文件,文件名与我们希望使用的终端命令同名,命令如下: $ sudo ln -s /home/user/bin/node-echo.js /usr/local/bin/node- echo 七天学会NodeJS http://www.himysql.com 整理精装PDF版 10 2.3.2 Windows 在Windows系统下的做法完全不同,我们得靠 .cmd 文件来解决问题。假设 node-echo.js 存 放在C:\Users\user\bin目录,并且该目录已经添加到PATH环境变量里了。接下来需要在该目录下 新建一个名为 node-echo.cmd 的文件,文件内容如下: @node "C:\User\user\bin\node-echo.js" %* 这样处理后,我们就可以在任何目录下使用node-echo命令了。 2.4 工程目录工程目录 了解了以上知识后,现在我们可以来完整地规划一个工程目录了。以编写一个命令行程序为例, 一般我们会同时提供命令行模式和API模式两种使用方式,并且我们会借助三方包来编写代码。 除了代码外,一个完整的程序也应该有自己的文档和测试用例。因此,一个标准的工程目录都看 起来像下边这样。 - /home/user/workspace/node-echo/ # 工程目录 - bin/ # 存放命令行相关代码 node-echo + doc/ # 存放文档 - lib/ # 存放API相关代码 echo.js - node_modules/ # 存放三方包 + argv/ + tests/ # 存放测试用例 package.json # 元数据文件 README.md # 说明文件 其中部分文件内容如下: /* bin/node-echo */ varvar argv == require('argv'), echo == require('../lib/echo'); console.log(echo(argv.join(' '))); 第 2 章 代码的组织和部署 11 /* lib/echo.js */ module.exports == functionfunction (message) { returnreturn message; }; /* package.json */ { "name": "node-echo", "main": "./lib/echo.js" } 以上例子中分类存放了不同类型的文件,并通过 node_moudles 目录直接使用三方包名加载模 块。此外,定义了 package.json 之后,node-echo 目录也可被当作一个包来使用。 2.5 NPM NPM是随同NodeJS一起安装的包管理工具,能解决NodeJS代码部署上的很多问题,常见的使用 场景有以下几种: • 允许用户从NPM服务器下载别人编写的三方包到本地使用。 • 允许用户从NPM服务器下载并安装别人编写的命令行程序到本地使用。 • 允许用户将自己编写的包或命令行程序上传到NPM服务器供别人使用。 可以看到,NPM建立了一个NodeJS生态圈,NodeJS开发者和用户可以在里边互通有无。以下分 别介绍这三种场景下怎样使用NPM。 2.5.1 下载三方包下载三方包 需要使用三方包时,首先得知道有哪些包可用。虽然 npmjs.org 提供了个搜索框可以根据包名来 搜索,但如果连想使用的三方包的名字都不确定的话,就请百度一下吧。知道了包名后,比如上 边例子中的argv,就可以在工程目录下打开终端,使用以下命令来下载三方包。 $ npm install argv ... argv@0.0.2 node_modules\argv 七天学会NodeJS http://www.himysql.com 整理精装PDF版 12 下载好之后,argv 包就放在了工程目录下的 node_modules 目录中,因此在代码中只需要通 过 require('argv') 的方式就好,无需指定三方包路径。 以上命令默认下载最新版三方包,如果想要下载指定版本的话,可以在包名后边加上 @,例如通过以下命令可下载0.0.1版的 argv。 $ npm install argv@0.0.1 ... argv@0.0.1 node_modules\argv 如果使用到的三方包比较多,在终端下一个包一条命令地安装未免太人肉了。因此NPM对 package.json 的字段做了扩展,允许在其中申明三方包依赖。因此,上边例子中的 package.json 可以改写如下: { "name": "node-echo", "main": "./lib/echo.js", "dependencies": { "argv": "0.0.2" } } 这样处理后,在工程目录下就可以使用 npm install 命令批量安装三方包了。更重要的是, 当以后 node-echo 也上传到了NPM服务器,别人下载这个包时,NPM会根据包中申明的三方 包依赖自动下载进一步依赖的三方包。例如,使用 npm install node-echo 命令时,NPM 会自动创建以下目录结构。 - project/ - node_modules/ - node-echo/ - node_modules/ + argv/ ... ... 如此一来,用户只需关心自己直接使用的三方包,不需要自己去解决所有包的依赖关系。 第 2 章 代码的组织和部署 13 2.5.2 安装命令行程序安装命令行程序 从NPM服务上下载安装一个命令行程序的方法与三方包类似。例如上例中的 node-echo 提供 了命令行使用方式,只要 node-echo 自己配置好了相关的 package.json 字段,对于用户而 言,只需要使用以下命令安装程序。 $ npm install node-echo -g 参数中的 -g 表示全局安装,因此 node-echo 会默认安装到以下位置,并且NPM会自动创建好 *nix系统下需要的软链文件或Windows系统下需要的 .cmd 文件。 - /usr/local/ # *nix系统下 - lib/node_modules/ + node-echo/ ... - bin/ node-echo ... ... - %APPDATA%\npm\ # Windows系统下 - node_modules\ + node-echo\ ... node-echo.cmd ... 2.5.3 发布代码发布代码 第一次使用NPM发布代码前需要注册一个账号。终端下运行 npm adduser,之后按照提示做即 可。账号搞定后,接着我们需要编辑 package.json 文件,加入NPM必需的字段。接着上边 node-echo 的例子,package.json 里必要的字段如下。 { "name": "node-echo", # 包名,在NPM服务器上须要保持唯一 "version": "1.0.0", # 当前版本号 七天学会NodeJS http://www.himysql.com 整理精装PDF版 14 "dependencies": { # 三方包依赖,需要指定包名和版本号 "argv": "0.0.2" }, "main": "./lib/echo.js", # 入口模块位置 "bin" : { "node-echo": "./bin/node-echo" # 命令行程序名和主模块位置 } } 之后,我们就可以在 package.json 所在目录下运行 npm publish 发布代码了。 2.5.4 版本号版本号 使用NPM下载和发布代码时都会接触到版本号。NPM使用语义版本号来管理代码,这里简单介 绍一下。 语义版本号分为 X.Y.Z 三位,分别代表主版本号、次版本号和补丁版本号。当代码变更时,版 本号按以下原则更新。 + 如果只是修复bug,需要更新Z位。 + 如果是新增了功能,但是向下兼容,需要更新Y位。 + 如果有大变动,向下不兼容,需要更新X位。 版本号有了这个保证后,在申明三方包依赖时,除了可依赖于一个固定版本号外,还可依赖于某 个范围的版本号。例如 "argv": "0.0.x" 表示依赖于 0.0.x 系列的最新版 argv。NPM支持 的所有版本号范围指定方式可以查看官方文档。 2.5.5 灵机一点灵机一点 除了本章介绍的部分外,NPM还提供了很多功能,package.json 里也有很多其它有用的字 段。除了可以在npmjs.org/doc/查看官方文档外,这里再介绍一些NPM常用命令。 • NPM提供了很多命令,例如 install 和 publish,使用 npm help 可查看所有命令。 • 使用 npm help 可查看某条命令的详细帮助,例如 npm help install。 • 在 package.json 所在目录下使用 npm install . -g 可先在本地安装当前命令行 程序,可用于发布前的本地测试。 第 2 章 代码的组织和部署 15 • 使用 npm update 可以把当前目录下 node_modules 子目录里边的对应 模块更新至最新版本。 • 使用 npm update -g 可以把全局安装的对应命令行程序更新至最新版。 • 使用 npm cache clear 可以清空NPM本地缓存,用于对付使用相同版本号发布新版本 代码的人。 • 使用 npm unpublish @ 可以撤销发布自己发布过的某个版本 代码。 2.6 小结小结 本章介绍了使用NodeJS编写代码前需要做的准备工作,总结起来有以下几点: • 编写代码前先规划好目录结构,才能做到有条不紊。 • 捎大些的程序可以将代码拆分为多个模块管理,更大些的程序可以使用包来组织模块。 • 合理使用 node_modules 和 NODE_PATH 来解耦包的使用方式和物理路径。 • 使用NPM加入NodeJS生态圈互通有无。 • 想到了心仪的包名时请提前在NPM上抢注。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 16 第第 3 章章 文件操作文件操作 让前端觉得如获神器的不是NodeJS能做网络编程,而是NodeJS能够操作文件。小至文件查找, 大至代码编译,几乎没有一个前端工具不操作文件。换个角度讲,几乎也只需要一些数据处理逻 辑,再加上一些文件操作,就能够编写出大多数前端工具。本章将介绍与之相关的NodeJS内置 模块。 3.1 开门红开门红 NodeJS提供了基本的文件操作API,但是像文件拷贝这种高级功能就没有提供,因此我们先拿文 件拷贝程序练手。与 copy 命令类似,我们的程序需要能接受源文件路径与目标文件路径两个参 数。 3.1.1 小文件拷贝小文件拷贝 我们使用NodeJS内置的 fs 模块简单实现这个程序如下。 varvar fs == require('fs'); functionfunction copy(src, dst) { fs.writeFileSync(dst, fs.readFileSync(src)); } functionfunction main(argv) { copy(argv[0], argv[1]); } main(process.argv.slice(2)); 以上程序使用 fs.readFileSync 从源路径读取文件内容,并使用 fs.writeFileSync 将文 件内容写入目标路径。 第 3 章 文件操作 17 豆知识豆知识 process 是一个全局变量,可通过 process.argv 获得命令行参数。由于 argv[0] 固定 等于NodeJS执行程序的绝对路径,argv[1] 固定等于主模块的绝对路径,因此第一个命令 行参数从 argv[2] 这个位置开始。 3.1.2 大文件拷贝大文件拷贝 上边的程序拷贝一些小文件没啥问题,但这种一次性把所有文件内容都读取到内存中后再一次性 写入磁盘的方式不适合拷贝大文件,内存会爆仓。对于大文件,我们只能读一点写一点,直到完 成拷贝。因此上边的程序需要改造如下。 varvar fs == require('fs'); functionfunction copy(src, dst) { fs.createReadStream(src).pipe(fs.createWriteStream(dst)); } functionfunction main(argv) { copy(argv[0], argv[1]); } main(process.argv.slice(2)); 以上程序使用 fs.createReadStream 创建了一个源文件的只读数据流,并使用 fs.createWriteStream 创建了一个目标文件的只写数据流,并且用 pipe 方法把两个数据 流连接了起来。连接起来后发生的事情,说得抽象点的话,水顺着水管从一个桶流到了另一个 桶。 3.2 API走马观花走马观花 我们先大致看看NodeJS提供了哪些和文件操作有关的API。这里并不逐一介绍每个API的使用方 法,官方文档已经做得很好了。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 18 Buffer(数据块)(数据块)1 JS语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与 String 对 等的全局构造函数 Buffer 来提供对二进制数据的操作。除了可以读取文件得到 Buffer 的实 例外,还能够直接构造,例如: varvar bin == newnew Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]); Buffer 与字符串类似,除了可以用 .length 属性得到字节长度外,还可以用 [index] 方式 读取指定位置的字节,例如: bin[0]; // => 0x68; Buffer 与字符串能够互相转化,例如可以使用指定编码将二进制数据转化为字符串: varvar str == bin.toString('utf-8'); // => "hello" 或者反过来,将字符串转换为指定编码下的二进制数据: varvar bin == newnew Buffer('hello', 'utf-8'); // => Buffer 与字符串有一个重要区别。字符串是只读的,并且对字符串的任何修改得到的都是一个 新字符串,原字符串保持不变。至于 Buffer,更像是可以做指针操作的C语言数组。例如,可 以用 [index] 方式直接修改某个位置的字节。 bin[0] == 0x48; 而 .slice 方法也不是返回一个新的 Buffer,而更像是返回了指向原 Buffer 中间的某个位 置的指针,如下所示。 [ 0x68, 0x65, 0x6c, 0x6c, 0x6f ] ^^ ^^ || || bin bin.slice(2) 1. 官方文档:官方文档:http://nodejs.org/api/buffer.html 第 3 章 文件操作 19 因此对 .slice 方法返回的 Buffer 的修改会作用于原 Buffer,例如: varvar bin == newnew Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]); varvar sub == bin.slice(2); sub[0] == 0x65; console.log(bin); // => 也因此,如果想要拷贝一份 Buffer,得首先创建一个新的 Buffer,并通过 .copy 方法把原 Buffer 中的数据复制过去。这个类似于申请一块新的内存,并把已有内存中的数据复制过去。 以下是一个例子。 varvar bin == newnew Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]); varvar dup == newnew Buffer(bin.length); bin.copy(dup); dup[0] == 0x48; console.log(bin); // => console.log(dup); // => 总之,Buffer 将JS的数据处理能力从字符串扩展到了任意二进制数据。 Stream(数据流)(数据流)2 当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到 数据流。NodeJS中通过各种 Stream 来提供对数据流的操作。 以上边的大文件拷贝程序为例,我们可以为数据来源创建一个只读数据流,示例如下: varvar rs == fs.createReadStream(pathname); rs.on('data', functionfunction (chunk) { doSomething(chunk); }); rs.on('end', functionfunction () { 2. 官方文档:官方文档: http://nodejs.org/api/buffer.html 七天学会NodeJS http://www.himysql.com 整理精装PDF版 20 cleanUp(); }); 豆知识豆知识 Stream 基于事件机制工作,所有 Stream 的实例都继承于NodeJS提供的EventEmitter。 上边的代码中 data 事件会源源不断地被触发,不管 doSomething 函数是否处理得过来。代码 可以继续做如下改造,以解决这个问题。 varvar rs == fs.createReadStream(src); rs.on('data', functionfunction (chunk) { rs.pause(); doSomething(chunk, functionfunction () { rs.resume(); }); }); rs.on('end', functionfunction () { cleanUp(); }); 以上代码给 doSomething 函数加上了回调,因此我们可以在处理数据前暂停数据读取,并在 处理数据后继续读取数据。 此外,我们也可以为数据目标创建一个只写数据流,示例如下: varvar rs == fs.createReadStream(src); varvar ws == fs.createWriteStream(dst); rs.on('data', functionfunction (chunk) { ws.write(chunk); }); rs.on('end', functionfunction () { 第 3 章 文件操作 21 ws.end(); }); 我们把 doSomething 换成了望只写数据流里写入数据后,以上代码看起来就像是一个文件拷 贝程序了。但是以上代码存在上边提到的问题,如果写入速度跟不上读取速度的话,只写数据流 内部的缓存会爆仓。我们可以根据 .write 方法的返回值来判断传入的数据是写入目标了,还 是临时放在了缓存了,并根据 drain 事件来判断什么时候只写数据流已经将缓存中的数据写入 目标,可以传入下一个待写数据了。因此代码可以改造如下: varvar rs == fs.createReadStream(src); varvar ws == fs.createWriteStream(dst); rs.on('data', functionfunction (chunk) { ifif (ws.write(chunk) ====== falsefalse) { rs.pause(); } }); rs.on('end', functionfunction () { ws.end(); }); ws.on('drain', functionfunction () { rs.resume(); }); 以上代码实现了数据从只读数据流到只写数据流的搬运,并包括了防爆仓控制。因为这种使用场 景很多,例如上边的大文件拷贝程序,NodeJS直接提供了 .pipe 方法来做这件事情,其内部实 现方式与上边的代码类似。 File System(文件系统)(文件系统)3 NodeJS通过 fs 内置模块提供对文件的操作。fs 模块提供的API基本上可以分为以下三类: • 文件属性读写。 其中常用的有 fs.stat、fs.chmod、fs.chown 等等。 3. 官方文档:官方文档: http://nodejs.org/api/buffer.html 七天学会NodeJS http://www.himysql.com 整理精装PDF版 22 • 文件内容读写。 其中常用的有 fs.readFile、fs.readdir、fs.writeFile、fs.mkdir 等等。 • 底层文件操作。 其中常用的有 fs.open、fs.read、fs.write、fs.close 等等。 NodeJS最精华的异步IO模型在 fs 模块里有着充分的体现,例如上边提到的这些API都通过回调 函数传递结果。以 fs.readFile 为例: fs.readFile(pathname, functionfunction (err, data) { ifif (err) { // Deal with error. } elseelse { // Deal with data. } }); 如上边代码所示,基本上所有 fs 模块API的回调参数都有两个。第一个参数在有错误发生时等 于异常对象,第二个参数始终用于返回API方法执行结果。 此外,fs 模块的所有异步API都有对应的同步版本,用于无法使用异步操作时,或者同步操作 更方便时的情况。同步API除了方法名的末尾多了一个 Sync 之外,异常对象与执行结果的传递 方式也有相应变化。同样以 fs.readFileSync 为例: trytry { varvar data == fs.readFileSync(pathname); // Deal with data. } catchcatch (err) { // Deal with error. } fs 模块提供的API很多,这里不一一介绍,需要时请自行查阅官方文档。 第 3 章 文件操作 23 Path(路径)(路径)4 操作文件时难免不与文件路径打交道。NodeJS提供了 path 内置模块来简化路径相关操作,并提 升代码可读性。以下分别介绍几个常用的API。 path.normalize 将传入的路径转换为标准路径,具体讲的话,除了解析路径中的 . 与 .. 外,还能去掉多余的斜 杠。如果有程序需要使用路径作为某些数据的索引,但又允许用户随意输入路径时,就需要使用 该方法保证路径的唯一性。以下是一个例子: varvar cache == {}; functionfunction store(key, value) { cache[path.normalize(key)] == value; } store('foo/bar', 1); store('foo//baz//../bar', 2); console.log(cache); // => { "foo/bar": 2 } 坑出没注意坑出没注意 标准化之后的路径里的斜杠在Windows系统下是 \,而在*nix系统下是 /。如果想保证任何 系统下都使用 / 作为路径分隔符的话,需要用 .replace(/\\/g, '/') 再替换一下标准 路径。 path.join 将传入的多个路径拼接为标准路径。该方法可避免手工拼接路径字符串的繁琐,并且能在不同系 统下正确使用相应的路径分隔符。以下是一个例子: path.join('foo/', 'baz/', '../bar'); // => "foo/bar" 4. 官方文档:官方文档: http://nodejs.org/api/path.html 七天学会NodeJS http://www.himysql.com 整理精装PDF版 24 path.extname 当我们需要根据不同文件扩展名做不同操作时,该方法就显得很好用。以下是一个例子: path.extname('foo/bar.js'); // => ".js" path 模块提供的其余方法也不多,稍微看一下官方文档就能全部掌握。 3.3 遍历目录遍历目录 遍历目录是操作文件时的一个常见需求。比如写一个程序,需要找到并处理指定目录下的所有JS 文件时,就需要遍历整个目录。 3.3.1 递归算法递归算法 遍历目录时一般使用递归算法,否则就难以编写出简洁的代码。递归算法与数学归纳法类似,通 过不断缩小问题的规模来解决问题。以下示例说明了这种方法。 functionfunction factorial(n) { ifif (n ====== 1) { returnreturn 1; } elseelse { returnreturn n ** factorial(n -- 1); } } 上边的函数用于计算N的阶乘(N!)。可以看到,当N大于1时,问题简化为计算N乘以N-1的阶 乘。当N等于1时,问题达到最小规模,不需要再简化,因此直接返回1。 陷阱陷阱 使用递归算法编写的代码虽然简洁,但由于每递归一次就产生一次函数调用,在需要优先考 虑性能时,需要把递归算法转换为循环算法,以减少函数调用次数。 第 3 章 文件操作 25 3.3.2 遍历算法遍历算法 目录是一个树状结构,在遍历时一般使用深度优先+先序遍历算法。深度优先,意味着到达一个 节点后,首先接着遍历子节点而不是邻居节点。先序遍历,意味着首次到达了某节点就算遍历完 成,而不是最后一次返回某节点才算数。因此使用这种遍历方式时,下边这棵树的遍历顺序是 A > B > D > E > C > F。 A / \ B C / \ \ D E F 3.3.3 同步遍历同步遍历 了解了必要的算法后,我们可以简单地实现以下目录遍历函数。 functionfunction travel(dir, callback) { fs.readdirSync(dir).forEach(functionfunction (file) { varvar pathname == path.join(dir, file); ifif (fs.statSync(pathname).isDirectory()) { travel(pathname, callback); } elseelse { callback(pathname); } }); } 可以看到,该函数以某个目录作为遍历的起点。遇到一个子目录时,就先接着遍历子目录。遇到 一个文件时,就把文件的绝对路径传给回调函数。回调函数拿到文件路径后,就可以做各种判断 和处理。因此假设有以下目录: - /home/user/ - foo/ x.js - bar/ 七天学会NodeJS http://www.himysql.com 整理精装PDF版 26 y.js z.css 使用以下代码遍历该目录时,得到的输入如下。 travel('/home/user', functionfunction (pathname) { console.log(pathname); }); ------------------------ /home/user/foo/x.js /home/user/bar/y.js /home/user/z.css 3.3.4 异步遍历异步遍历 如果读取目录或读取文件状态时使用的是异步API,目录遍历函数实现起来会有些复杂,但原理 完全相同。travel 函数的异步版本如下。 functionfunction travel(dir, callback, finish) { fs.readdir(dir, functionfunction (err, files) { (functionfunction next(i) { ifif (i << files.length) { varvar pathname == path.join(dir, files[i]); fs.stat(pathname, functionfunction (err, stats) { ifif (stats.isDirectory()) { travel(pathname, callback, functionfunction () { next(i ++ 1); }); } elseelse { callback(pathname, functionfunction () { next(i ++ 1); }); } }); } elseelse { 第 3 章 文件操作 27 finish &&&& finish(); } }(0)); }); } 这里不详细介绍异步遍历函数的编写技巧,在后续章节中会详细介绍这个。总之我们可以看到异 步编程还是蛮复杂的。 3.4 文本编码文本编码 使用NodeJS编写前端工具时,操作得最多的是文本文件,因此也就涉及到了文件编码的处理问 题。我们常用的文本编码有 UTF8 和 GBK 两种,并且 UTF8 文件还可能带有BOM。在读取不同 编码的文本文件时,需要将文件内容转换为JS使用的 UTF8 编码字符串后才能正常处理。 3.4.1 BOM的移除的移除 BOM用于标记一个文本文件使用Unicode编码,其本身是一个Unicode字符(”\uFEFF”),位于文 本文件头部。在不同的Unicode编码下,BOM字符对应的二进制字节如下: Bytes Encoding -------------------------------------------------------- FE FF UTF16BE FF FE UTF16LE EF BB BF UTF8 因此,我们可以根据文本文件头几个字节等于啥来判断文件是否包含BOM,以及使用哪种 Unicode编码。但是,BOM字符虽然起到了标记文件编码的作用,其本身却不属于文件内容的一 部分,如果读取文本文件时不去掉BOM,在某些使用场景下就会有问题。例如我们把几个JS文 件合并成一个文件后,如果文件中间含有BOM字符,就会导致浏览器JS语法错误。因此,使用 NodeJS读取文本文件时,一般需要去掉BOM。例如,以下代码实现了识别和去除UTF8 BOM的 功能。 functionfunction readText(pathname) { varvar bin == fs.readFileSync(pathname); 七天学会NodeJS http://www.himysql.com 整理精装PDF版 28 ifif (bin[0] ====== 0xEF &&&& bin[1] ====== 0xBB &&&& bin[2] ====== 0xBF) { bin == bin.slice(3); } returnreturn bin.toString('utf-8'); } 3.4.2 GBK转转UTF8 NodeJS支持在读取文本文件时,或者在 Buffer 转换为字符串时指定文本编码,但遗憾的是, GBK编码不在NodeJS自身支持范围内。因此,一般我们借助 iconv-lite 这个三方包来转换编 码。使用NPM下载该包后,我们可以按下边方式编写一个读取GBK文本文件的函数。 varvar iconv == require('iconv-lite'); functionfunction readGBKText(pathname) { varvar bin == fs.readFileSync(pathname); returnreturn iconv.decode(bin, 'gbk'); } 3.4.3 单字节编码单字节编码 有时候,我们无法预知需要读取的文件采用哪种编码,因此也就无法指定正确的编码。比如我们 要处理的某些CSS文件中,有的用GBK编码,有的用UTF8编码。虽然可以一定程度可以根据文 件的字节内容猜测出文本编码,但这里要介绍的是有些局限,但是要简单得多的一种技术。 首先我们知道,如果一个文本文件只包含英文字符,比如 Hello World ,那无论用GBK编码 或是UTF8编码读取这个文件都是没问题的。这是因为在这些编码下,ASCII0~128范围内字符都 使用相同的单字节编码。 反过来讲,即使一个文本文件中有中文等字符,如果我们需要处理的字符仅在ASCII0~128范围 内,比如除了注释和字符串以外的JS代码,我们就可以统一使用单字节编码来读取文件,不用关 心文件的实际编码是GBK还是UTF8。以下示例说明了这种方法。 1. GBK编码源文件内容: 第 3 章 文件操作 29 var foo = '中文'; 2. 对应字节: 76 61 72 20 66 6F 6F 20 3D 20 27 D6 D0 CE C4 27 3B 3. 使用单字节编码读取后得到的内容: var foo = '{乱码}{乱码}{乱码}{乱码}'; 4. 替换内容: var bar = '{乱码}{乱码}{乱码}{乱码}'; 5. 使用单字节编码保存后对应字节: 76 61 72 20 62 61 72 20 3D 20 27 D6 D0 CE C4 27 3B 6. 使用GBK编码读取后得到内容: var bar = '中文'; 这里的诀窍在于,不管大于0xEF的单个字节在单字节编码下被解析成什么乱码字符,使用同样 的单字节编码保存这些乱码字符时,背后对应的字节保持不变。 NodeJS中自带了一种 binary 编码可以用来实现这个方法,因此在下例中,我们使用这种编码 来演示上例对应的代码该怎么写。 functionfunction replace(pathname) { varvar str == fs.readFileSync(pathname, 'binary'); str == str.replace('foo', 'bar'); fs.writeFileSync(pathname, str, 'binary'); } 3.5 小结小结 本章介绍了使用NodeJS操作文件时需要的API以及一些技巧,总结起来有以下几点: • 学好文件操作,编写各种程序都不怕。 • 如果不是很在意性能,fs 模块的同步API能让生活更加美好。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 30 • 需要对文件读写做到字节级别的精细控制时,请使用 fs 模块的文件底层操作API。 • 不要使用拼接字符串的方式来处理路径,使用 path 模块。 • 掌握好目录遍历和文件编码处理技巧,很实用。 第 3 章 文件操作 31 第第 4 章章 网络操作网络操作 不了解网络编程的程序员不是好前端,而NodeJS恰好提供了一扇了解网络编程的窗口。通过 NodeJS,除了可以编写一些服务端程序来协助前端开发和测试外,还能够学习一些HTTP协议与 Socket协议的相关知识,这些知识在优化前端性能和排查前端故障时说不定能派上用场。本章将 介绍与之相关的NodeJS内置模块。 4.1 开门红开门红 NodeJS本来的用途是编写高性能Web服务器。我们首先在这里重复一下官方文档里的例子,使用 NodeJS内置的 http 模块简单实现一个HTTP服务器。 varvar http == require('http'); http.createServer(functionfunction (request, response) { response.writeHead(200, { 'Content-Type':: 'text-plain' }); response.end('Hello World\n'); }).listen(8124); 以上程序创建了一个HTTP服务器并监听 8124 端口,打开浏览器访问该端口 http://127.0.0.1:8124/ 就能够看到效果。 豆知识豆知识 在*nix系统下,监听1024以下端口需要root权限。因此,如果想监听80或443端口的话,需要 使用 sudo 命令启动程序。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 32 4.2 API走马观花走马观花 我们先大致看看NodeJS提供了哪些和网络操作有关的API。这里并不逐一介绍每个API的使用方 法,官方文档已经做得很好了。 HTTP 1 http 模块提供两种使用方式: • 作为服务端使用时,创建一个HTTP服务器,监听HTTP客户端请求并返回响应。 • 作为客户端使用时,发起一个HTTP客户端请求,获取服务端响应。 首先我们来看看服务端模式下如何工作。如开门红中的例子所示,首先需要使用 .createServer 方法创建一个服务器,然后调用 .listen 方法监听端口。之后,每当来了一 个客户端请求,创建服务器时传入的回调函数就被调用一次。可以看出,这是一种事件机制。 HTTP请求本质上是一个数据流,由请求头(headers)和请求体(body)组成。例如以下是一个 完整的HTTP请求数据内容。 POST / HTTP/1.1 User-Agent: curl/7.26.0 Host: localhost Accept: */* Content-Length: 11 Content-Type: application/x-www-form-urlencoded Hello World 可以看到,空行之上是请求头,之下是请求体。HTTP请求在发送给服务器时,可以认为是按照 从头到尾的顺序一个字节一个字节地以数据流方式发送的。而 http 模块创建的HTTP服务器在 接收到完整的请求头后,就会调用回调函数。在回调函数中,除了可以使用 request 对象访问 请求头数据外,还能把 request 对象当作一个只读数据流来访问请求体数据。以下是一个例 子。 http.createServer(functionfunction (request, response) { varvar body == []; 1. 官方文档:官方文档: http://nodejs.org/api/http.html 第 4 章 网络操作 33 console.log(request.method); console.log(request.headers); request.on('data', functionfunction (chunk) { body.push(chunk); }); request.on('end', functionfunction () { body == Buffer.concat(body); console.log(body.toString()); }); }).listen(80); ------------------------------------ POST { 'user-agent': 'curl/7.26.0', host: 'localhost', accept: '*/*', 'content-length': '11', 'content-type': 'application/x-www-form-urlencoded' } Hello World HTTP响应本质上也是一个数据流,同样由响应头(headers)和响应体(body)组成。例如以下 是一个完整的HTTP请求数据内容。 HTTP/1.1 200 OK Content-Type: text/plain Content-Length: 11 Date: Tue, 05 Nov 2013 05:31:38 GMT Connection: keep-alive Hello World 在回调函数中,除了可以使用 response 对象来写入响应头数据外,还能把 response 对象当 作一个只写数据流来写入响应体数据。例如在以下例子中,服务端原样将客户端请求的请求体数 据返回给客户端。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 34 http.createServer(functionfunction (request, response) { response.writeHead(200, { 'Content-Type':: 'text/plain' }); request.on('data', functionfunction (chunk) { response.write(chunk); }); request.on('end', functionfunction () { response.end(); }); }).listen(80); 接下来我们看看客户端模式下如何工作。为了发起一个客户端HTTP请求,我们需要指定目标服 务器的位置并发送请求头和请求体,以下示例演示了具体做法。 varvar options == { hostname:: 'www.example.com', port:: 80, path:: '/upload', method:: 'POST', headers:: { 'Content-Type':: 'application/x-www-form-urlencoded' } }; varvar request == http.request(options, functionfunction (response) {}); request.write('Hello World'); request.end(); 可以看到,.request 方法创建了一个客户端,并指定请求目标和请求头数据。之后,就可以 把 request 对象当作一个只写数据流来写入请求体数据和结束请求。另外,由于HTTP请求中 GET 请求是最常见的一种,并且不需要请求体,因此 http 模块也提供了以下便捷API。 http.get('http://www.example.com/', functionfunction (response) {}); 第 4 章 网络操作 35 当客户端发送请求并接收到完整的服务端响应头时,就会调用回调函数。在回调函数中,除了可 以使用 response 对象访问响应头数据外,还能把 response 对象当作一个只读数据流来访问 响应体数据。以下是一个例子。 http.get('http://www.example.com/', functionfunction (response) { varvar body == []; console.log(response.statusCode); console.log(response.headers); response.on('data', functionfunction (chunk) { body.push(chunk); }); response.on('end', functionfunction () { body == Buffer.concat(body); console.log(body.toString()); }); }); ------------------------------------ 200 { 'content-type': 'text/html', server: 'Apache', 'content-length': '801', date: 'Tue, 05 Nov 2013 06:08:41 GMT', connection: 'keep-alive' } ... HTTPS 2 https 模块与 http 模块极为类似,区别在于 https 模块需要额外处理SSL证书。 在服务端模式下,创建一个HTTPS服务器的示例如下。 2. 官方文档:官方文档: http://nodejs.org/api/https.html 七天学会NodeJS http://www.himysql.com 整理精装PDF版 36 varvar options == { key:: fs.readFileSync('./ssl/default.key'), cert:: fs.readFileSync('./ssl/default.cer') }; varvar server == https.createServer(options, functionfunction (request, response) { // ... }); 可以看到,与创建HTTP服务器相比,多了一个 options 对象,通过 key 和 cert 字段指定了 HTTPS服务器使用的私钥和公钥。 另外,NodeJS支持SNI技术,可以根据HTTPS客户端请求使用的域名动态使用不同的证书,因此 同一个HTTPS服务器可以使用多个域名提供服务。接着上例,可以使用以下方法为HTTPS服务 器添加多组证书。 server.addContext('foo.com', { key:: fs.readFileSync('./ssl/foo.com.key'), cert:: fs.readFileSync('./ssl/foo.com.cer') }); server.addContext('bar.com', { key:: fs.readFileSync('./ssl/bar.com.key'), cert:: fs.readFileSync('./ssl/bar.com.cer') }); 在客户端模式下,发起一个HTTPS客户端请求与 http 模块几乎相同,示例如下。 varvar options == { hostname:: 'www.example.com', port:: 443, path:: '/', method:: 'GET' }; varvar request == https.request(options, functionfunction (response) {}); 第 4 章 网络操作 37 request.end(); 但如果目标服务器使用的SSL证书是自制的,不是从颁发机构购买的,默认情况下 https 模块 会拒绝连接,提示说有证书安全问题。在 options 里加入 rejectUnauthorized: false 字段可以禁用对证书有效性的检查,从而允许 https 模块请求开发环境下使用自制证书的 HTTPS服务器。 URL 3 处理HTTP请求时 url 模块使用率超高,因为该模块允许解析URL、生成URL,以及拼接URL。 首先我们来看看一个完整的URL的各组成部分。 href ----------------------------------------------------------------- host path --------------- ---------------------------- http: // user:pass @ host.com : 8080 /p/a/t/h ?query=string #hash ----- --------- -------- ---- -------- ------------- ----- protocol auth hostname port pathname search hash ------------ query 我们可以使用 .parse 方法来将一个URL字符串转换为URL对象,示例如下。 url.parse('http://user:pass@host.com:8080/p/a/t/ h?query=string#hash'); /* => { protocol: 'http:', auth: 'user:pass', host: 'host.com:8080', port: '8080', hostname: 'host.com', hash: '#hash', search: '?query=string', query: 'query=string', 3. 官方文档:官方文档: http://nodejs.org/api/url.html 七天学会NodeJS http://www.himysql.com 整理精装PDF版 38 pathname: '/p/a/t/h', path: '/p/a/t/h?query=string', href: 'http://user:pass@host.com:8080/p/a/t/h?query=string#hash' } */ 传给 .parse 方法的不一定要是一个完整的URL,例如在HTTP服务器回调函数中, request.url 不包含协议头和域名,但同样可以用 .parse 方法解析。 http.createServer(functionfunction (request, response) { varvar tmp == request.url; // => "/foo/bar?a=b" url.parse(tmp); /* => { protocol: null, slashes: null, auth: null, host: null, port: null, hostname: null, hash: null, search: '?a=b', query: 'a=b', pathname: '/foo/bar', path: '/foo/bar?a=b', href: '/foo/bar?a=b' } */ }).listen(80); .parse 方法还支持第二个和第三个布尔类型可选参数。第二个参数等于 true 时,该方法返回 的URL对象中,query 字段不再是一个字符串,而是一个经过 querystring 模块转换后的参 数对象。第三个参数等于 true 时,该方法可以正确解析不带协议头的URL,例如 //www.example.com/foo/bar。 反过来,format 方法允许将一个URL对象转换为URL字符串,示例如下。 url.format({ protocol:: 'http:', host:: 'www.example.com', 第 4 章 网络操作 39 pathname:: '/p/a/t/h', search:: 'query=string' }); /* => 'http://www.example.com/p/a/t/h?query=string' */ 另外,.resolve 方法可以用于拼接URL,示例如下。 url.resolve('http://www.example.com/foo/bar', '../baz'); /* => http://www.example.com/baz */ Query String 4 querystring 模块用于实现URL参数字符串与参数对象的互相转换,示例如下。 querystring.parse('foo=bar&baz=qux&baz=quux&corge'); /* => { foo: 'bar', baz: ['qux', 'quux'], corge: '' } */ querystring.stringify({ foo:: 'bar', baz:: ['qux', 'quux'], corge:: '' }); /* => 'foo=bar&baz=qux&baz=quux&corge=' */ Zlib 5 zlib 模块提供了数据压缩和解压的功能。当我们处理HTTP请求和响应时,可能需要用到这个 模块。 4. 官方文档:官方文档: http://nodejs.org/api/querystring.html 5. 官方文档:官方文档: http://nodejs.org/api/zlib.html 七天学会NodeJS http://www.himysql.com 整理精装PDF版 40 首先我们看一个使用 zlib 模块压缩HTTP响应体数据的例子。这个例子中,判断了客户端是否 支持gzip,并在支持的情况下使用 zlib 模块返回gzip之后的响应体数据。 http.createServer(functionfunction (request, response) { varvar i == 1024, data == ''; whilewhile (i----) { data +=+= '.'; } ifif ((request.headers['accept-encoding'] |||| '').indexOf('gzip') !==!== --1) { zlib.gzip(data, functionfunction (err, data) { response.writeHead(200, { 'Content-Type':: 'text/plain', 'Content-Encoding':: 'gzip' }); response.end(data); }); } elseelse { response.writeHead(200, { 'Content-Type':: 'text/plain' }); response.end(data); } }).listen(80); 接着我们看一个使用 zlib 模块解压HTTP响应体数据的例子。这个例子中,判断了服务端响应 是否使用gzip压缩,并在压缩的情况下使用 zlib 模块解压响应体数据。 ```var options = { hostname: ‘www.example.com’, port: 80, path: ‘/’, method: ‘GET’, headers: { ‘Accept-Encoding’: ‘gzip, deflate’ } }; http.request(options, function (response) { var body = []; response.on('data', function (chunk) { body.push(chunk); 第 4 章 网络操作 41 }); response.on('end', function () { body = Buffer.concat(body); if (response.headers['content-encoding'] === 'gzip') { zlib.gunzip(body, function (err, data) { console.log(data.toString()); }); } else { console.log(data.toString()); } }); }).end(); ``` Net 6 net 模块可用于创建Socket服务器或Socket客户端。由于Socket在前端领域的使用范围还不是很 广,这里先不涉及到WebSocket的介绍,仅仅简单演示一下如何从Socket层面来实现HTTP请求和 响应。 首先我们来看一个使用Socket搭建一个很不严谨的HTTP服务器的例子。这个HTTP服务器不管收 到啥请求,都固定返回相同的响应。 net.createServer(functionfunction (conn) { conn.on('data', functionfunction (data) { conn.write([ 'HTTP/1.1 200 OK', 'Content-Type: text/plain', 'Content-Length: 11', '', 'Hello World' ].join('\n')); }); }).listen(80); 6. 官方文档:官方文档: http://nodejs.org/api/net.html 七天学会NodeJS http://www.himysql.com 整理精装PDF版 42 接着我们来看一个使用Socket发起HTTP客户端请求的例子。这个例子中,Socket客户端在建立连 接后发送了一个HTTP GET请求,并通过 data 事件监听函数来获取服务器响应。 varvar options == { port:: 80, host:: 'www.example.com' }; varvar client == net.connect(options, functionfunction () { client.write([ 'GET / HTTP/1.1', 'User-Agent: curl/7.26.0', 'Host: www.baidu.com', 'Accept: */*', '', '' ].join('\n')); }); client.on('data', functionfunction (data) { console.log(data.toString()); client.end(); }); 4.3 灵机一点灵机一点 使用NodeJS操作网络,特别是操作HTTP请求和响应时会遇到一些惊喜,这里对一些常见问题做 解答。 • 问: 为什么通过 headers 对象访问到的HTTP请求头或响应头字段不是驼峰的? 答: 从规范上讲,HTTP请求头和响应头字段都应该是驼峰的。但现实是残酷的,不是 每个HTTP服务端或客户端程序都严格遵循规范,所以NodeJS在处理从别的客户端或服务 端收到的头字段时,都统一地转换为了小写字母格式,以便开发者能使用统一的方式来 访问头字段,例如 headers['content-length']。 • 问: 为什么 http 模块创建的HTTP服务器返回的响应是 chunked 传输方式的? 第 4 章 网络操作 43 答: 因为默认情况下,使用 .writeHead 方法写入响应头后,允许使用 .write 方法 写入任意长度的响应体数据,并使用 .end 方法结束一个响应。由于响应体数据长度不 确定,因此NodeJS自动在响应头里添加了 Transfer-Encoding: chunked 字段,并 采用 chunked 传输方式。但是当响应体数据长度确定时,可使用 .writeHead 方法在 响应头里加上 Content-Length 字段,这样做之后NodeJS就不会自动添加 Transfer- Encoding 字段和使用 chunked 传输方式。 • 问: 为什么使用 http 模块发起HTTP客户端请求时,有时候会发生 socket hang up 错误? 答: 发起客户端HTTP请求前需要先创建一个客户端。http 模块提供了一个全局客户端 http.globalAgent,可以让我们使用 .request 或 .get 方法时不用手动创建客户 端。但是全局客户端默认只允许5个并发Socket连接,当某一个时刻HTTP客户端请求创 建过多,超过这个数字时,就会发生 socket hang up 错误。解决方法也很简单,通 过 http.globalAgent.maxSockets 属性把这个数字改大些即可。另外,https 模 块遇到这个问题时也一样通过 https.globalAgent.maxSockets 属性来处理。 4.4 小结小结 本章介绍了使用NodeJS操作网络时需要的API以及一些坑回避技巧,总结起来有以下几点: • http 和 https 模块支持服务端模式和客户端模式两种使用方式。 • request 和 response 对象除了用于读写头数据外,都可以当作数据流来操作。 • url.parse 方法加上 request.url 属性是处理HTTP请求时的固定搭配。 • 使用 zlib 模块可以减少使用HTTP协议时的数据传输量。 • 通过 net 模块的Socket服务器与客户端可对HTTP协议做底层操作。 • 小心踩坑。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 44 第第 5 章章 进程管理进程管理 NodeJS可以感知和控制自身进程的运行环境和状态,也可以创建子进程并与其协同工作,这使 得NodeJS可以把多个程序组合在一起共同完成某项工作,并在其中充当胶水和调度器的作用。 本章除了介绍与之相关的NodeJS内置模块外,还会重点介绍典型的使用场景。 5.1 开门红开门红 我们已经知道了NodeJS自带的 fs 模块比较基础,把一个目录里的所有文件和子目录都拷贝到另 一个目录里需要写不少代码。另外我们也知道,终端下的 cp 命令比较好用,一条 cp -r source/* target 命令就能搞定目录拷贝。那我们首先看看如何使用NodeJS调用终端命令来 简化目录拷贝,示例代码如下: varvar child_process == require('child_process'); varvar util == require('util'); functionfunction copy(source, target, callback) { child_process.exec( util.format('cp -r %s/* %s', source, target), callback); } copy('a', 'b', functionfunction (err) { // ... }); 从以上代码中可以看到,子进程是异步运行的,通过回调函数返回执行结果。 5.2 API走马观花走马观花 我们先大致看看NodeJS提供了哪些和进程管理有关的API。这里并不逐一介绍每个API的使用方 法,官方文档已经做得很好了。 第 5 章 进程管理 45 Process 1 任何一个进程都有启动进程时使用的命令行参数,有标准输入标准输出,有运行权限,有运行环 境和运行状态。在NodeJS中,可以通过 process 对象感知和控制NodeJS自身进程的方方面面。 另外需要注意的是,process 不是内置模块,而是一个全局对象,因此在任何地方都可以直接 使用。 Child Process 2 使用 child_process 模块可以创建和控制子进程。该模块提供的API中最核心的是 .spawn, 其余API都是针对特定使用场景对它的进一步封装,算是一种语法糖。 Cluster 3 cluster 模块是对 child_process 模块的进一步封装,专用于解决单进程NodeJS Web服务器 无法充分利用多核CPU的问题。使用该模块可以简化多进程服务器程序的开发,让每个核上运行 一个工作进程,并统一通过主进程监听端口和分发请求。 5.3 应用场景应用场景 和进程管理相关的API单独介绍起来比较枯燥,因此这里从一些典型的应用场景出发,分别介绍 一些重要API的使用方法。 5.3.1 如何获取命令行参数如何获取命令行参数 在NodeJS中可以通过 process.argv 获取命令行参数。但是比较意外的是,node 执行程序路 径和主模块文件路径固定占据了 argv[0] 和 argv[1] 两个位置,而第一个命令行参数从 argv[2] 开始。为了让 argv 使用起来更加自然,可以按照以下方式处理。 functionfunction main(argv) { // ... } main(process.argv.slice(2)); 1. 官方文档:官方文档: http://nodejs.org/api/process.html 2. 官方文档:官方文档: http://nodejs.org/api/child_process.html 3. 官方文档:官方文档: http://nodejs.org/api/cluster.html 七天学会NodeJS http://www.himysql.com 整理精装PDF版 46 5.3.2 如何退出程序如何退出程序 通常一个程序做完所有事情后就正常退出了,这时程序的退出状态码为 0。或者一个程序运行时 发生了异常后就挂了,这时程序的退出状态码不等于 0。如果我们在代码中捕获了某个异常,但 是觉得程序不应该继续运行下去,需要立即退出,并且需要把退出状态码设置为指定数字,比如 1,就可以按照以下方式: trytry { // ... } catchcatch (err) { // ... process.exit(1); } 5.3.3 如何控制输入输出如何控制输入输出 NodeJS程序的标准输入流(stdin)、一个标准输出流(stdout)、一个标准错误流(stderr)分别 对应 process.stdin、process.stdout 和 process.stderr,第一个是只读数据流,后 边两个是只写数据流,对它们的操作按照对数据流的操作方式即可。例如,console.log 可以 按照以下方式实现。 functionfunction log() { process.stdout.write( util.format.apply(util, arguments) ++ '\n'); } 5.3.4 如何降权如何降权 在*nix系统下,我们知道需要使用root权限才能监听1024以下端口。但是一旦完成端口监听后, 继续让程序运行在root权限下存在安全隐患,因此最好能把权限降下来。以下是这样一个例子。 http.createServer(callback).listen(80, functionfunction () { varvar env == process.env, uid == parseInt(env['SUDO_UID'] |||| process.getuid(), 10), gid == parseInt(env['SUDO_GID'] |||| process.getgid(), 10); 第 5 章 进程管理 47 process.setgid(gid); process.setuid(uid); }); 上例中有几点需要注意: 1. 如果是通过 sudo 获取root权限的,运行程序的用户的UID和GID保存在环境变量 SUDO_UID 和 SUDO_GID 里边。如果是通过 chmod +s 方式获取root权限的,运行程序 的用户的UID和GID可直接通过 process.getuid 和 process.getgid 方法获取。 2. process.setuid 和 process.setgid 方法只接受number类型的参数。 3. 降权时必须先降GID再降UID,否则顺序反过来的话就没权限更改程序的GID了。 5.3.5 如何创建子进程如何创建子进程 以下是一个创建NodeJS子进程的例子。 varvar child == child_process.spawn('node', [ 'xxx.js' ]); child.stdout.on('data', functionfunction (data) { console.log('stdout: ' ++ data); }); child.stderr.on('data', functionfunction (data) { console.log('stderr: ' ++ data); }); child.on('close', functionfunction (code) { console.log('child process exited with code ' ++ code); }); 上例中使用了 .spawn(exec, args, options) 方法,该方法支持三个参数。第一个参数是 执行文件路径,可以是执行文件的相对或绝对路径,也可以是根据PATH环境变量能找到的执行 文件名。第二个参数中,数组中的每个成员都按顺序对应一个命令行参数。第三个参数可选,用 于配置子进程的执行环境与行为。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 48 另外,上例中虽然通过子进程对象的 .stdout 和 .stderr 访问子进程的输出,但通过 options.stdio 字段的不同配置,可以将子进程的输入输出重定向到任何数据流上,或者让 子进程共享父进程的标准输入输出流,或者直接忽略子进程的输入输出。 5.3.6 进程间如何通讯进程间如何通讯 在*nix系统下,进程之间可以通过信号互相通信。以下是一个例子。 /* parent.js */ varvar child == child_process.spawn('node', [ 'child.js' ]); child.kill('SIGTERM'); /* child.js */ process.on('SIGTERM', functionfunction () { cleanUp(); process.exit(0); }); 在上例中,父进程通过 .kill 方法向子进程发送 SIGTERM 信号,子进程监听 process 对象的 SIGTERM 事件响应信号。不要被 .kill 方法的名称迷惑了,该方法本质上是用来给进程发送信 号的,进程收到信号后具体要做啥,完全取决于信号的种类和进程自身的代码。 另外,如果父子进程都是NodeJS进程,就可以通过IPC(进程间通讯)双向传递数据。以下是一 个例子。 /* parent.js */ varvar child == child_process.spawn('node', [ 'child.js' ], { stdio:: [ 0, 1, 2, 'ipc' ] }); child.on('message', functionfunction (msg) { console.log(msg); }); child.send({ hello:: 'hello' }); 第 5 章 进程管理 49 /* child.js */ process.on('message', functionfunction (msg) { msg.hello == msg.hello.toUpperCase(); process.send(msg); }); 可以看到,父进程在创建子进程时,在 options.stdio 字段中通过 ipc 开启了一条IPC通 道,之后就可以监听子进程对象的 message 事件接收来自子进程的消息,并通过 .send 方法 给子进程发送消息。在子进程这边,可以在 process 对象上监听 message 事件接收来自父进 程的消息,并通过 .send 方法向父进程发送消息。数据在传递过程中,会先在发送端使用 JSON.stringify 方法序列化,再在接收端使用 JSON.parse 方法反序列化。 5.3.7 如何守护子进程如何守护子进程 守护进程一般用于监控工作进程的运行状态,在工作进程不正常退出时重启工作进程,保障工作 进程不间断运行。以下是一种实现方式。 /* daemon.js */ functionfunction spawn(mainModule) { varvar worker == child_process.spawn('node', [ mainModule ]); worker.on('exit', functionfunction (code) { ifif (code !==!== 0) { spawn(mainModule); } }); } spawn('worker.js'); 可以看到,工作进程非正常退出时,守护进程立即重启工作进程。 5.4 小结小结 本章介绍了使用NodeJS管理进程时需要的API以及主要的应用场景,总结起来有以下几点: • 使用 process 对象管理自身。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 50 • 使用 child_process 模块创建和管理子进程。 第 5 章 进程管理 51 第第 6 章章 异步编程异步编程 NodeJS最大的卖点——事件机制和异步IO,对开发者并不是透明的。开发者需要按异步方式编 写代码才用得上这个卖点,而这一点也遭到了一些NodeJS反对者的抨击。但不管怎样,异步编 程确实是NodeJS最大的特点,没有掌握异步编程就不能说是真正学会了NodeJS。本章将介绍与 异步编程相关的各种知识。 6.1 回调回调 在代码中,异步编程的直接体现就是回调。异步编程依托于回调来实现,但不能说使用了回调后 程序就异步化了。我们首先可以看看以下代码。 functionfunction heavyCompute(n, callback) { varvar count == 0, i, j; forfor (i == n; i >> 0; ----i) { forfor (j == n; j >> 0; ----j) { count +=+= 1; } } callback(count); } heavyCompute(10000, functionfunction (count) { console.log(count); }); console.log('hello'); 七天学会NodeJS http://www.himysql.com 整理精装PDF版 52 100000000 hello 可以看到,以上代码中的回调函数仍然先于后续代码执行。JS本身是单线程运行的,不可能在一 段代码还未结束运行时去运行别的代码,因此也就不存在异步执行的概念。 但是,如果某个函数做的事情是创建一个别的线程或进程,并与JS主线程并行地做一些事情,并 在事情做完后通知JS主线程,那情况又不一样了。我们接着看看以下代码。 setTimeout(functionfunction () { console.log('world'); }, 1000); console.log('hello'); hello world 这次可以看到,回调函数后于后续代码执行了。如同上边所说,JS本身是单线程的,无法异步执 行,因此我们可以认为 setTimeout 这类JS规范之外的由运行环境提供的特殊函数做的事情是 创建一个平行线程后立即返回,让JS主进程可以接着执行后续代码,并在收到平行进程的通知后 再执行回调函数。除了 setTimeout、setInterval 这些常见的,这类函数还包括NodeJS提 供的诸如 fs.readFile 之类的异步API。 另外,我们仍然回到JS是单线程运行的这个事实上,这决定了JS在执行完一段代码之前无法执行 包括回调函数在内的别的代码。也就是说,即使平行线程完成工作了,通知JS主线程执行回调函 数了,回调函数也要等到JS主线程空闲时才能开始执行。以下就是这么一个例子。 functionfunction heavyCompute(n) { varvar count == 0, i, j; forfor (i == n; i >> 0; ----i) { forfor (j == n; j >> 0; ----j) { count +=+= 1; } } 第 6 章 异步编程 53 } varvar t == newnew Date(); setTimeout(functionfunction () { console.log(newnew Date() -- t); }, 1000); heavyCompute(50000); 8520 可以看到,本来应该在1秒后被调用的回调函数因为JS主线程忙于运行其它代码,实际执行时间 被大幅延迟。 6.2 代码设计模式代码设计模式 异步编程有很多特有的代码设计模式,为了实现同样的功能,使用同步方式和异步方式编写的代 码会有很大差异。以下分别介绍一些常见的模式。 6.2.1 函数返回值函数返回值 使用一个函数的输出作为另一个函数的输入是很常见的需求,在同步方式下一般按以下方式编写 代码: varvar output == fn1(fn2('input')); // Do something. 而在异步方式下,由于函数执行结果不是通过返回值,而是通过回调函数传递,因此一般按以下 方式编写代码: fn2('input', functionfunction (output2) { fn1(output2, functionfunction (output1) { // Do something. }); }); 七天学会NodeJS http://www.himysql.com 整理精装PDF版 54 可以看到,这种方式就是一个回调函数套一个回调函多,套得太多了很容易写出 >形状的代码。 6.2.2 遍历数组遍历数组 在遍历数组时,使用某个函数依次对数据成员做一些处理也是常见的需求。如果函数是同步执行 的,一般就会写出以下代码: varvar len == arr.length, i == 0; forfor (; i << len; ++++i) { arr[i] == sync(arr[i]); } // All array items have processed. 如果函数是异步执行的,以上代码就无法保证循环结束后所有数组成员都处理完毕了。如果数组 成员必须一个接一个串行处理,则一般按照以下方式编写异步代码: (functionfunction next(i, len, callback) { ifif (i << len) { async(arr[i], functionfunction (value) { arr[i] == value; next(i ++ 1, len, callback); }); } elseelse { callback(); } }(0, arr.length, functionfunction () { // All array items have processed. })); 可以看到,以上代码在异步函数执行一次并返回执行结果后才传入下一个数组成员并开始下一轮 执行,直到所有数组成员处理完毕后,通过回调的方式触发后续代码的执行。 如果数组成员可以并行处理,但后续代码仍然需要所有数组成员处理完毕后才能执行的话,则异 步代码会调整成以下形式: 第 6 章 异步编程 55 (functionfunction (i, len, count, callback) { forfor (; i << len; ++++i) { (functionfunction (i) { async(arr[i], functionfunction (value) { arr[i] == value; ifif (++++count ====== len) { callback(); } }); }(i)); } }(0, arr.length, 0, functionfunction () { // All array items have processed. })); 可以看到,与异步串行遍历的版本相比,以上代码并行处理所有数组成员,并通过计数器变量来 判断什么时候所有数组成员都处理完毕了。 6.2.3 异常处理异常处理 JS自身提供的异常捕获和处理机制——try..catch..,只能用于同步执行的代码。以下是一 个例子。 functionfunction sync(fn) { returnreturn fn(); } trytry { sync(nullnull); // Do something. } catchcatch (err) { console.log('Error: %s', err.message); } Error: object is not a function 七天学会NodeJS http://www.himysql.com 整理精装PDF版 56 可以看到,异常会沿着代码执行路径一直冒泡,直到遇到第一个 try 语句时被捕获住。但由于 异步函数会打断代码执行路径,异步函数执行过程中以及执行之后产生的异常冒泡到执行路径被 打断的位置时,如果一直没有遇到 try 语句,就作为一个全局异常抛出。以下是一个例子。 functionfunction async(fn, callback) { // Code execution path breaks here. setTimeout(functionfunction () { callback(fn()); }, 0); } trytry { async(nullnull, functionfunction (data) { // Do something. }); } catchcatch (err) { console.log('Error: %s', err.message); } /home/user/test.js:4 callback(fn()); ^ TypeError: object is not a function at null._onTimeout (/home/user/test.js:4:13) at Timer.listOnTimeout [as ontimeout] (timers.js:110:15) 因为代码执行路径被打断了,我们就需要在异常冒泡到断点之前用 try 语句把异常捕获住,并 通过回调函数传递被捕获的异常。于是我们可以像下边这样改造上边的例子。 functionfunction async(fn, callback) { // Code execution path breaks here. setTimeout(functionfunction () { trytry { callback(nullnull, fn()); } catchcatch (err) { callback(err); } 第 6 章 异步编程 57 }, 0); } async(nullnull, functionfunction (err, data) { ifif (err) { console.log('Error: %s', err.message); } elseelse { // Do something. } }); Error: object is not a function 可以看到,异常再次被捕获住了。在NodeJS中,几乎所有异步API都按照以上方式设计,回调函 数中第一个参数都是 err。因此我们在编写自己的异步函数时,也可以按照这种方式来处理异 常,与NodeJS的设计风格保持一致。 有了异常处理方式后,我们接着可以想一想一般我们是怎么写代码的。基本上,我们的代码都是 做一些事情,然后调用一个函数,然后再做一些事情,然后再调用一个函数,如此循环。如果我 们写的是同步代码,只需要在代码入口点写一个 try 语句就能捕获所有冒泡上来的异常,示例 如下。 functionfunction main() { // Do something. syncA(); // Do something. syncB(); // Do something. syncC(); } trytry { main(); } catchcatch (err) { // Deal with exception. } 七天学会NodeJS http://www.himysql.com 整理精装PDF版 58 但是,如果我们写的是异步代码,就只有呵呵了。由于每次异步函数调用都会打断代码执行路 径,只能通过回调函数来传递异常,于是我们就需要在每个回调函数里判断是否有异常发生,于 是只用三次异步函数调用,就会产生下边这种代码。 functionfunction main(callback) { // Do something. asyncA(functionfunction (err, data) { ifif (err) { callback(err); } elseelse { // Do something asyncB(functionfunction (err, data) { ifif (err) { callback(err); } elseelse { // Do something asyncC(functionfunction (err, data) { ifif (err) { callback(err); } elseelse { // Do something callback(nullnull); } }); } }); } }); } main(functionfunction (err) { ifif (err) { // Deal with exception. } }); 第 6 章 异步编程 59 6.3 域域 官方文档:http://nodejs.org/api/domain.html NodeJS提供了 domain 模块,可以简化异步代码的异常处理。在介绍该模块之前,我们需要首 先理解“域”的概念。简单的讲,一个域就是一个JS运行环境,在一个运行环境中,如果一个异常 没有被捕获,将作为一个全局异常被抛出。NodeJS通过 process 对象提供了捕获全局异常的方 法,示例代码如下 process.on('uncaughtException', functionfunction (err) { console.log('Error: %s', err.message); }); setTimeout(functionfunction (fn) { fn(); }); Error: undefined is not a function 虽然全局异常有个地方可以捕获了,但是对于大多数异常,我们希望尽早捕获,并根据结果决定 代码的执行路径。我们用以下HTTP服务器代码作为例子: functionfunction async(request, callback) { // Do something. asyncA(request, functionfunction (err, data) { ifif (err) { callback(err); } elseelse { // Do something asyncB(request, functionfunction (err, data) { ifif (err) { callback(err); } elseelse { // Do something asyncC(request, functionfunction (err, data) { ifif (err) { callback(err); 七天学会NodeJS http://www.himysql.com 整理精装PDF版 60 } elseelse { // Do something callback(nullnull, data); } }); } }); } }); } http.createServer(functionfunction (request, response) { async(request, functionfunction (err, data) { ifif (err) { response.writeHead(500); response.end(); } elseelse { response.writeHead(200); response.end(data); } }); }); 以上代码将请求对象交给异步函数处理后,再根据处理结果返回响应。这里采用了使用回调函数 传递异常的方案,因此 async 函数内部如果再多几个异步函数调用的话,代码就变成上边这副 鬼样子了。为了让代码好看点,我们可以在每处理一个请求时,使用 domain 模块创建一个子 域(JS子运行环境)。在子域内运行的代码可以随意抛出异常,而这些异常可以通过子域对象的 error 事件统一捕获。于是以上代码可以做如下改造: functionfunction async(request, callback) { // Do something. asyncA(request, functionfunction (data) { // Do something asyncB(request, functionfunction (data) { // Do something asyncC(request, functionfunction (data) { // Do something 第 6 章 异步编程 61 callback(data); }); }); }); } http.createServer(functionfunction (request, response) { varvar d == domain.create(); d.on('error', functionfunction () { response.writeHead(500); response.end(); }); d.run(functionfunction () { async(request, functionfunction (data) { response.writeHead(200); response.end(data); }); }); }); 可以看到,我们使用 .create 方法创建了一个子域对象,并通过.run方法进入需要在子域中运 行的代码的入口点。而位于子域中的异步函数回调函数由于不再需要捕获异常,代码一下子瘦身 很多。 6.3.1 陷阱陷阱 无论是通过 process 对象的 uncaughtException 事件捕获到全局异常,还是通过子域对象 的 error 事件捕获到了子域异常,在NodeJS官方文档里都强烈建议处理完异常后立即重启程 序,而不是让程序继续运行。按照官方文档的说法,发生异常后的程序处于一个不确定的运行状 态,如果不立即退出的话,程序可能会发生严重内存泄漏,也可能表现得很奇怪。 但这里需要澄清一些事实。JS本身的 throw..try..catch 异常处理机制并不会导致内存泄 漏,也不会让程序的执行结果出乎意料,但NodeJS并不是存粹的JS。NodeJS里大量的API内部是 用C/C++实现的,因此NodeJS程序的运行过程中,代码执行路径穿梭于JS引擎内部和外部,而JS 七天学会NodeJS http://www.himysql.com 整理精装PDF版 62 的异常抛出机制可能会打断正常的代码执行流程,导致C/C++部分的代码表现异常,进而导致内 存泄漏等问题。 因此,使用 uncaughtException 或 domain 捕获异常,代码执行路径里涉及到了C/C++部分 的代码时,如果不能确定是否会导致内存泄漏等问题,最好在处理完异常后重启程序比较妥当。 而使用 try 语句捕获异常时一般捕获到的都是JS本身的异常,不用担心上诉问题。 6.4 小结小结 本章介绍了JS异步编程相关的知识,总结起来有以下几点: • 不掌握异步编程就不算学会NodeJS。 • 异步编程依托于回调来实现,而使用回调不一定就是异步编程。 • 异步编程下的函数间数据传递、数组遍历和异常处理与同步编程有很大差别。 • 使用 domain 模块简化异步代码的异常处理,并小心陷阱。 第 6 章 异步编程 63 第第 7 章章 大示例大示例 学习讲究的是学以致用和融会贯通。至此我们已经分别介绍了NodeJS的很多知识点,本章作为 最后一章,将完整地介绍一个使用NodeJS开发Web服务器的示例。 7.1 需求需求 我们要开发的是一个简单的静态文件合并服务器,该服务器需要支持类似以下格式的JS或CSS文 件合并请求。 http://assets.example.com/foo/??bar.js,baz.js 在以上URL中,?? 是一个分隔符,之前是需要合并的多个文件的URL的公共部分,之后是使 用,分隔的差异部分。因此服务器处理这个URL时,返回的是以下两个文件按顺序合并后的内 容。 /foo/bar.js /foo/baz.js 另外,服务器也需要能支持类似以下格式的普通的JS或CSS文件请求。 http://assets.example.com/foo/bar.js 以上就是整个需求。 7.2 第一次迭代第一次迭代 快速迭代是一种不错的开发方式,因此我们在第一次迭代时先实现服务器的基本功能。 7.2.1 设计设计 简单分析了需求之后,我们大致会得到以下的设计方案。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 64 +---------+ +-----------+ +----------+ request -->| parse |-->| combine |-->| output |--> response +---------+ +-----------+ +----------+ 也就是说,服务器会首先分析URL,得到请求的文件的路径和类型(MIME)。然后,服务器会 读取请求的文件,并按顺序合并文件内容。最后,服务器返回响应,完成对一次请求的处理。 另外,服务器在读取文件时需要有个根目录,并且服务器监听的HTTP端口最好也不要写死在代 码里,因此服务器需要是可配置的。 7.2.2 实现实现 根据以上设计,我们写出了第一版代码如下。 varvar fs == require('fs'), path == require('path'), http == require('http'); varvar MIME == { '.css':: 'text/css', '.js':: 'application/javascript' }; functionfunction combineFiles(pathnames, callback) { varvar output == []; (functionfunction next(i, len) { ifif (i << len) { fs.readFile(pathnames[i], functionfunction (err, data) { ifif (err) { callback(err); } elseelse { output.push(data); next(i ++ 1, len); } }); } elseelse { 第 7 章 大示例 65 callback(nullnull, Buffer.concat(output)); } }(0, pathnames.length)); } functionfunction main(argv) { varvar config == JSON.parse(fs.readFileSync(argv[0], 'utf-8')), root == config.root |||| '.', port == config.port |||| 80; http.createServer(functionfunction (request, response) { varvar urlInfo == parseURL(root, request.url); combineFiles(urlInfo.pathnames, functionfunction (err, data) { ifif (err) { response.writeHead(404); response.end(err.message); } elseelse { response.writeHead(200, { 'Content-Type':: urlInfo.mime }); response.end(data); } }); }).listen(port); } functionfunction parseURL(root, url) { varvar base, pathnames, parts; ifif (url.indexOf('??') ====== --1) { url == url.replace('/', '/??'); } parts == url.split('??'); base == parts[0]; pathnames == parts[1].split(',').map(functionfunction (value) { 七天学会NodeJS http://www.himysql.com 整理精装PDF版 66 returnreturn path.join(root, base, value); }); returnreturn { mime:: MIME[path.extname(pathnames[0])] |||| 'text/plain', pathnames:: pathnames }; } main(process.argv.slice(2)); 以上代码完整实现了服务器所需的功能,并且有以下几点值得注意: 1. 使用命令行参数传递JSON配置文件路径,入口函数负责读取配置并创建服务器。 2. 入口函数完整描述了程序的运行逻辑,其中解析URL和合并文件的具体实现封装在其它 两个函数里。 3. 解析URL时先将普通URL转换为了文件合并URL,使得两种URL的处理方式可以一致。 4. 合并文件时使用异步API读取文件,避免服务器因等待磁盘IO而发生阻塞。 我们可以把以上代码保存为 server.js,之后就可以通过 node server.js config.json 命令启动程序,于是我们的第一版静态文件合并服务器就顺利完工了。 另外,以上代码存在一个不那么明显的逻辑缺陷。例如,使用以下URL请求服务器时会有惊喜。 http://assets.example.com/foo/bar.js,foo/baz.js 经过分析之后我们会发现问题出在 / 被自动替换 /?? 这个行为上,而这个问题我们可以到第二 次迭代时再解决。 7.3 第二次迭代第二次迭代 在第一次迭代之后,我们已经有了一个可工作的版本,满足了功能需求。接下来我们需要从性能 的角度出发,看看代码还有哪些改进余地。 第 7 章 大示例 67 7.3.1 设计设计 把 map 方法换成 for 循环或许会更快一些,但第一版代码最大的性能问题存在于从读取文件到 输出响应的过程当中。我们以处理 /??a.js,b.js,c.js 这个请求为例,看看整个处理过程中 耗时在哪儿。 发送请求 等待服务端响应 接收响应 ---------+----------------------+-------------> -- 解析请求 ------ 读取a.js ------ 读取b.js ------ 读取c.js -- 合并数据 -- 输出响应 可以看到,第一版代码依次把请求的文件读取到内存中之后,再合并数据和输出响应。这会导致 以下两个问题: 1. 当请求的文件比较多比较大时,串行读取文件会比较耗时,从而拉长了服务端响应等待 时间。 2. 由于每次响应输出的数据都需要先完整地缓存在内存里,当服务器请求并发数较大时, 会有较大的内存开销。 对于第一个问题,很容易想到把读取文件的方式从串行改为并行。但是别这样做,因为对于机械 磁盘而言,因为只有一个磁头,尝试并行读取文件只会造成磁头频繁抖动,反而降低IO效率。 而对于固态硬盘,虽然的确存在多个并行IO通道,但是对于服务器并行处理的多个请求而言, 硬盘已经在做并行IO了,对单个请求采用并行IO无异于拆东墙补西墙。因此,正确的做法不是 改用并行IO,而是一边读取文件一边输出响应,把响应输出时机提前至读取第一个文件的时 刻。这样调整后,整个请求处理过程变成下边这样。 发送请求 等待服务端响应 接收响应 ---------+----+-------------------------------> -- 解析请求 -- 检查文件是否存在 -- 输出响应头 ------ 读取和输出a.js 七天学会NodeJS http://www.himysql.com 整理精装PDF版 68 ------ 读取和输出b.js ------ 读取和输出c.js 按上述方式解决第一个问题后,因为服务器不需要完整地缓存每个请求的输出数据了,第二个问 题也迎刃而解。 7.3.2 实现实现 根据以上设计,第二版代码按以下方式调整了部分函数。 function main(argv) { var config = JSON.parse(fs.readFileSync(argv[0], 'utf-8')), root = config.root || '.', port = config.port || 80; http.createServer(function (request, response) { var urlInfo = parseURL(root, request.url); validateFiles(urlInfo.pathnames, function (err, pathnames) { if (err) { response.writeHead(404); response.end(err.message); } else { response.writeHead(200, { 'Content-Type': urlInfo.mime }); outputFiles(pathnames, response); } }); }).listen(port); } function outputFiles(pathnames, writer) { (function next(i, len) { if (i < len) { var reader = fs.createReadStream(pathnames[i]); 第 7 章 大示例 69 reader.pipe(writer, { end: false }); reader.on('end', function() { next(i + 1, len); }); } else { writer.end(); } }(0, pathnames.length)); } function validateFiles(pathnames, callback) { (function next(i, len) { if (i < len) { fs.stat(pathnames[i], function (err, stats) { if (err) { callback(err); } else if (!stats.isFile()) { callback(new Error()); } else { next(i + 1, len); } }); } else { callback(null, pathnames); } }(0, pathnames.length)); } 可以看到,第二版代码在检查了请求的所有文件是否有效之后,立即就输出了响应头,并接着一 边按顺序读取文件一边输出响应内容。并且,在读取文件时,第二版代码直接使用了只读数据流 来简化代码。 7.4 第三次迭代第三次迭代 第二次迭代之后,服务器本身的功能和性能已经得到了初步满足。接下来我们需要从稳定性的角 度重新审视一下代码,看看还需要做些什么。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 70 7.4.1 设计设计 从工程角度上讲,没有绝对可靠的系统。即使第二次迭代的代码经过反复检查后能确保没有 bug,也很难说是否会因为NodeJS本身,或者是操作系统本身,甚至是硬件本身导致我们的服务 器程序在某一天挂掉。因此一般生产环境下的服务器程序都配有一个守护进程,在服务挂掉的时 候立即重启服务。一般守护进程的代码会远比服务进程的代码简单,从概率上可以保证守护进程 更难挂掉。如果再做得严谨一些,甚至守护进程自身可以在自己挂掉时重启自己,从而实现双保 险。 因此在本次迭代时,我们先利用NodeJS的进程管理机制,将守护进程作为父进程,将服务器程 序作为子进程,并让父进程监控子进程的运行状态,在其异常退出时重启子进程。 7.4.2 实现实现 根据以上设计,我们编写了守护进程需要的代码。 varvar cp == require('child_process'); varvar worker; functionfunction spawn(server, config) { worker == cp.spawn('node', [ server, config ]); worker.on('exit', functionfunction (code) { ifif (code !==!== 0) { spawn(server, config); } }); } functionfunction main(argv) { spawn('server.js', argv[0]); process.on('SIGTERM', functionfunction () { worker.kill(); process.exit(0); }); } 第 7 章 大示例 71 main(process.argv.slice(2)); 此外,服务器代码本身的入口函数也要做以下调整。 functionfunction main(argv) { varvar config == JSON.parse(fs.readFileSync(argv[0], 'utf-8')), root == config.root |||| '.', port == config.port |||| 80, server; server == http.createServer(functionfunction (request, response) { ... }).listen(port); process.on('SIGTERM', functionfunction () { server.close(functionfunction () { process.exit(0); }); }); } 我们可以把守护进程的代码保存为 daemon.js,之后我们可以通过 node daemon.js config.json 启动服务,而守护进程会进一步启动和监控服务器进程。此外,为了能够正常终 止服务,我们让守护进程在接收到 SIGTERM 信号时终止服务器进程。而在服务器进程这一端, 同样在收到 SIGTERM 信号时先停掉HTTP服务再正常退出。至此,我们的服务器程序就靠谱很 多了。 7.5 第四次迭代第四次迭代 在我们解决了服务器本身的功能、性能和可靠性的问题后,接着我们需要考虑一下代码部署的问 题,以及服务器控制的问题。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 72 7.5.1 设计设计 一般而言,程序在服务器上有一个固定的部署目录,每次程序有更新后,都重新发布到部署目录 里。而一旦完成部署后,一般也可以通过固定的服务控制脚本启动和停止服务。因此我们的服务 器程序部署目录可以做如下设计。 - deploy/ - bin/ startws.sh killws.sh + conf/ config.json + lib/ daemon.js server.js 在以上目录结构中,我们分类存放了服务控制脚本、配置文件和服务器代码。 7.5.2 实现实现 按以上目录结构分别存放对应的文件之后,接下来我们看看控制脚本怎么写。首先是 start.sh。 #!/bin/sh ifif [[ ! -f "pid" ]] thenthen node ../lib/daemon.js ../conf/config.json & echo $! > pid fifi 然后是 killws.sh。 #!/bin/sh ifif [[ -f "pid" ]] thenthen kill $($(tr -d '\r\n' < pid)) 第 7 章 大示例 73 rm pid fifi 于是这样我们就有了一个简单的代码部署目录和服务控制脚本,我们的服务器程序就可以上线工 作了。 7.6 后续迭代后续迭代 我们的服务器程序正式上线工作后,我们接下来或许会发现还有很多可以改进的点。比如服务器 程序在合并JS文件时可以自动在JS文件之间插入一个 ; 来避免一些语法问题,比如服务器程序需 要提供日志来统计访问量,比如服务器程序需要能充分利用多核CPU,等等。而此时的你,在学 习了这么久NodeJS之后,应该已经知道该怎么做了。 7.7 小结小结 本章将之前零散介绍的知识点串了起来,完整地演示了一个使用NodeJS开发程序的例子,至此 我们的课程就全部结束了。以下是对新诞生的NodeJSer的一些建议。 • 要熟悉官方API文档。并不是说要熟悉到能记住每个API的名称和用法,而是要熟悉 NodeJS提供了哪些功能,一旦需要时知道查询API文档的哪块地方。 • 要先设计再实现。在开发一个程序前首先要有一个全局的设计,不一定要很周全,但要 足够能写出一些代码。 • 要实现后再设计。在写了一些代码,有了一些具体的东西后,一定会发现一些之前忽略 掉的细节。这时再反过来改进之前的设计,为第二轮迭代做准备。 • 要充分利用三方包。NodeJS有一个庞大的生态圈,在写代码之前先看看有没有现成的三 方包能节省不少时间。 • 不要迷信三方包。任何事情做过头了就不好了,三方包也是一样。三方包是一个黑盒, 每多使用一个三方包,就为程序增加了一份潜在风险。并且三方包很难恰好只提供程序 需要的功能,每多使用一个三方包,就让程序更加臃肿一些。因此在决定使用某个三方 包之前,最好三思而后行。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 74 第第 8 章章 关于我们关于我们 我们是阿里巴巴国际站前端技术部。 我们喜爱NodeJS,用NodeJS做了不少好东西,并 写下了这篇文章。 我们在杭州市滨江园区,俗称大内,相比西溪园区: 这吃饭不排 队,停车不收费,通勤不累。 如果你也愿意加入我们,趁着还有一些股票可以发,赶紧 发送简历至nanqiao.dengnq@alibaba-inc.com,通过后我们会尽快与你联系,谢谢。 8.1 工作机会工作机会 8.1.1 招聘职位招聘职位 高级前端开发工程师。 8.1.2 为什么选择加入我们?为什么选择加入我们? 1. 工资优异:优秀的公司赢利能力,保障你的付出有回报,工资及年终奖肯定让你满意。 2. 福利丰厚:健全的福利体系,全额公积金、医疗保险、股票期权奖励、iHome无息贷款 等。 3. 升职空间大:有完善的晋升体系,P1-P14,只要你敢想敢做,晋升空间永远放在那儿。 4. 学习空间大:大牛多,技术氛围浓,技术上可以得到很大提升。 5. 内部转岗:阿里系下BU多,不同职位,不同BU,只要愿意,随时可以换个发展环境, 公司非常鼓励内部转岗。 8.1.3 能做些什么事情?能做些什么事情? 1. 负责网站应用前端开发,与后台工程师协作,完成数据交互、动态信息展现。 2. 编写封装良好的前端交互组件,维护及优化网站前端页面性能。 3. 研究和探索创新的开发思路和最新的前端技术。 4. 参与新人培训和前端技术布道。 第 8 章 关于我们 75 8.1.4 需要具备什么能力?需要具备什么能力? 1. 精通Web前端技术,包括HTML/CSS/JavaScript等,能够解决各种浏览器兼容性问题。 2. 持续关注业界的新话题和新技术,研究过JQuery、Ext、YUI等框架中的一种或以上。 3. 重视团队协作,愿意探寻和改进目前还不成熟的前端开发流程,了解各种常用工具。 4. 热爱前端技术,个性乐观开朗,逻辑性强,善于和各种背景的人沟通合作。 5. 如具备其它语言的开发经验,比如Java或PHP或Actionscript3/Flex会更受欢迎。 七天学会NodeJS http://www.himysql.com 整理精装PDF版 76

下载文档,方便阅读与编辑

文档的实际排版效果,会与网站的显示效果略有不同!!

需要 10 金币 [ 分享文档获得金币 ] 3 人已下载

下载文档

相关文档