一:NodeJS基础

1:什么是NodeJS

  1. JS是脚本语言,脚本语言都需要一个解析器才能运行。对于写在HTML页面里的JS,浏览器充当了解析器的角色。而**对于需要独立运行的JSNodeJS就是一个解析器**。<br /> 每一种解析器都是一个运行环境,不但允许JS定义各种数据结构,进行各种计算,还允许JS使用运行环境提供的内置对象和方法做一些事情。例如运行在浏览器中的JS的用途是操作DOM,浏览器就提供了document之类的内置对象。而**运行在NodeJS中的JS的用途是操作磁盘文件或搭建HTTP服务器**,NodeJS就相应提供了fshttp等内置对象。

2:NodeJS能做什么

  1. NodeJS的作者说,他创造NodeJS的目的是为了实现高性能Web服务器,他首先看重的是事件机制和异步IO模型的优越性,而不是JS。但是他需要选择一种编程语言实现他的想法,这种编程语言不能自带IO功能,并且需要能良好支持事件机制。JS没有自带IO功能,天生就用于处理浏览器中的DOM事件,并且拥有一大群程序员,因此就成为了天然的选择。 <br /> 如他所愿,NodeJS在服务端活跃起来,出现了大批基于NodeJSWeb服务。而另一方面,NodeJS让前端众如获神器,终于可以让自己的能力覆盖范围跳出浏览器窗口,更大批的前端工具如雨后春笋。<br /> 因此,对于前端而言,虽然不是人人都要拿NodeJS写一个服务器程序,但**简单可至使用命令交互模式调试JS代码片段,复杂可至编写工具提升工作效率。**

3:从命令行运行 Node.js

打开终端,键入node进入命令交互模式,可以输入一条代码语句后立即执行并显示结果,例如:
image.png
如果要运行一大段代码的话,可以先写一个JS文件再运行。
比如主 Node.js 应用程序文件是 app.js,则可以通过键入以下命令调用它:
node app.js
注: 当运行命令时,请确保位于包含 app.js 文件的目录中。

4:从 Node.js 程序退出

  1. 有很多种方法可以终止 Node.js 应用程序。 当在控制台中运行程序时,可以使用 ctrl-C 将其关闭,但是这里要讨论的是以编程的方式退出。 <br />** process **核心模块提供了一种便利的方法,可以以编程的方式退出 Node.js 程序:process.exit()。当 Node.js 运行此行代码时,进程会被立即强制终止。这意味着任何待处理的回调、仍在发送中的任何网络请求、任何文件系统访问、或正在写入 stdout stderr 的进程,所有这些都会被立即非正常地终止。<br />可以传入一个整数,向操作系统发送退出码:
  1. process.exit(1)
  1. 默认情况下,退出码为 0,表示成功。 不同的退出码具有不同的含义,可以在系统中用于程序与其他程序的通信。

5:模块

  1. 编写稍大一点的程序时一般都会将代码模块化。在NodeJS中,一般将代码合理拆分到不同的JS文件中,每一个文件就是一个模块,而文件路径就是模块名。<br /> 在编写每个模块时,都有requireexportsmodule三个预先定义好的变量可供使用。

require

  1. require函数用于在当前模块中加载和使用别的模块,传入一个模块名,返回一个模块导出对象。模块名可使用相对路径(以./开头),或者是绝对路径(以/或C:之类的盘符开头)。另外,模块名中的.js扩展名可以省略。以下是一个例子。
  1. var foo1 = require('./foo');
  2. var foo2 = require('./foo.js');
  3. var foo3 = require('/home/user/foo');
  4. var foo4 = require('/home/user/foo.js');
  5. // foo1至foo4中保存的是同一个模块的导出对象。

另外,可以使用以下方式加载和使用一个JSON文件。

  1. var data = require('./data.json');

exports

  1. exports对象是当前模块的导出对象,用于导出模块公有方法和属性。别的模块通过require函数使用当前模块时得到的就是当前模块的exports对象。以下例子中导出了一个公有方法。
  1. exports.hello = function () {
  2. console.log('Hello World!');
  3. };

以上代码中,模块默认导出对象被替换为一个函数。

模块初始化

  1. 一个模块中的JS代码仅在模块第一次被使用时执行一次,并在执行过程中初始化模块的导出对象。之后,缓存起来的导出对象被重复利用。

主模块

  1. 通过命令行参数传递给NodeJS以启动程序的模块被称为主模块。主模块负责调度组成整个程序的其它模块完成工作。例如通过以下命令启动程序时,main.js就是主模块。
  1. node main.js

完整示例

例如有以下目录。

  1. - /home/user/hello/
  2. - util/
  3. counter.js
  4. main.js

其中counter.js内容如下:

  1. var i = 0;
  2. function count() {
  3. return ++i;
  4. }
  5. exports.count = count;

该模块内部定义了一个私有变量i,并在exports对象导出了一个公有方法count。
主模块main.js内容如下:

  1. var counter1 = require('./util/counter');
  2. var counter2 = require('./util/counter');
  3. console.log(counter1.count());
  4. console.log(counter2.count());
  5. console.log(counter2.count());

运行该程序的结果如下:

  1. $ node main.js
  2. 1
  3. 2
  4. 3

可以看到,counter.js并没有因为被require了两次而初始化两次。

小结:

  • NodeJS是一个JS脚本解析器,任何操作系统下安装NodeJS本质上做的事情都是把NodeJS执行程序复制到一个目录,然后保证这个目录在系统PATH环境变量下,以便终端下可以使用node命令。
  • 终端下直接输入node命令可进入命令交互模式,很适合用来测试一些JS代码片段,比如正则表达式。
  • NodeJS使用CMD模块系统,主模块作为程序入口点,所有模块在执行过程中只初始化一次

二:代码的组织和部署

1:模块路径解析规则

  1. 我们已经知道,require函数支持斜杠(/)或盘符(C:)开头的绝对路径,也支持./开头的相对路径。但这两种路径在模块之间建立了强耦合关系,一旦某个模块文件的存放位置需要变更,使用该模块的其它模块的代码也需要跟着调整,变得牵一发动全身。因此,**require函数支持第三种形式的路径**,写法类似于foo/bar,并依次按照以下规则解析路径,直到找到模块位置。

内置模块

  1. 如果传递给require函数的是NodeJS内置模块名称,不做路径解析,直接返回内部模块的导出对象,例如require('fs')。

node_modules目录

  1. NodeJS定义了一个特殊的node_modules目录用于存放模块。例如某个模块的绝对路径是/home/user/hello.js,在该模块中使用require('foo/bar')方式加载模块时,则NodeJS依次尝试使用以下路径。
  1. /home/user/node_modules/foo/bar
  2. /home/node_modules/foo/bar
  3. /node_modules/foo/bar

NODE_PATH环境变量

  1. PATH环境变量类似,NodeJS允许通过NODE_PATH环境变量来**指定额外的模块搜索路径**。NODE_PATH环境变量中包含一到多个目录路径,**路径之间在Linux下使用:分隔,在Windows下使用;分隔**。<br /> 例如定义了以下NODE_PATH环境变量:
  1. NODE_PATH=/home/user/lib:/home/lib

当使用require(‘foo/bar’)的方式加载模块时,则NodeJS依次尝试以下路径。

  1. /home/user/lib/foo/bar
  2. /home/lib/foo/bar

2:包

  1. 我们已经知道了JS模块的基本单位是单个JS文件,但复杂些的模块往往由多个子模块组成。为了便于管理和使用,我们可以把**由多个子模块组成的大模块称做包**,并**把所有子模块放在同一个目录里**.<br /> 在组成一个包的所有子模块中,需要有一个入口模块,入口模块的导出对象被作为包的导出对象。例如有以下目录结构。
  1. - /home/user/lib/
  2. - cat/
  3. head.js
  4. body.js
  5. main.js

其中cat目录定义了一个包,其中包含了3个子模块。main.js作为入口模块,其内容如下:

  1. var head = require('./head');
  2. var body = require('./body');
  3. exports.create = function (name) {
  4. return {
  5. name: name,
  6. head: head.create(),
  7. body: body.create()
  8. };
  9. };
  1. 在其它模块里使用包的时候,需要加载包的入口模块。接着上例,使用require('/home/user/lib/cat/main')能达到目的,但是入口模块名称出现在路径里看上去不是个好主意。因此我们需要做点额外的工作,让包使用起来更像是单个模块。

index.js

  1. 当模块的文件名是index.js,加载模块时可以使用模块**所在目录的路径代替模块文件路径**,因此接着上例,以下两条语句等价。
  1. var cat = require('/home/user/lib/cat');
  2. var cat = require('/home/user/lib/cat/index');

package.json

  1. 如果想自定义入口模块的文件名和存放位置,就需要在包目录下包含一个package.json文件,并在其中指定入口模块的路径。上例中的cat模块可以重构如下。
  1. - /home/user/lib/
  2. - cat/
  3. + doc/
  4. - lib/
  5. head.js
  6. body.js
  7. main.js
  8. + tests/
  9. package.json
  1. 其中package.json内容如下。
  1. {
  2. "name": "cat",
  3. "main": "./lib/main.js"
  4. }
  1. 如此一来,就同样可以使用require('/home/user/lib/cat')的方式加载模块。NodeJS会根据包目录下的package.json找到入口模块所在位置。

3:命令行程序

  1. 使用NodeJS编写的东西,**要么是一个包,要么是一个命令行程序**,而前者最终也会用于开发后者。因此我们在部署代码时需要一些技巧,让用户觉得自己是在使用一个命令行程序。 <br /> 例如我们用NodeJS写了个程序,可以把命令行参数原样打印出来。该程序很简单,在主模块内实现了所有功能。并且写好后,我们把该程序部署在/home/user/bin/node-echo.js这个位置。**为了在任何目录下都能运行该程序**,我们需要使用以下终端命令。
  1. $ node /home/user/bin/node-echo.js Hello World
  2. Hello World

这种使用方式看起来不怎么像是一个命令行程序,下边的才是我们期望的方式。

  1. $ node-echo Hello World
  1. Windows系统下,我们得靠.cmd文件来解决问题。假设node-echo.js存放在C:\Users\user\bin目录,并且该目录已经添加到PATH环境变量里了。接下来需要在该目录下新建一个名为node-echo.cmd的文件,文件内容如下: <br />@node "C:\User\user\bin\node-echo.js" %*<br /> 这样处理后,我们就可以在任何目录下使用node-echo命令了。

4:工程目录

  1. 了解了以上知识后,现在我们可以来完整地规划一个工程目录了。以编写一个命令行程序为例,一般我们会同时提供命令行模式和API模式两种使用方式,并且我们会借助三方包来编写代码。除了代码外,一个完整的程序也应该有自己的文档和测试用例。因此,一个标准的工程目录都看起来像下边这样。
  1. - /home/user/workspace/node-echo/ # 工程目录
  2. - bin/ # 存放命令行相关代码
  3. node-echo
  4. + doc/ # 存放文档
  5. - lib/ # 存放API相关代码
  6. echo.js
  7. - node_modules/ # 存放三方包
  8. + argv/
  9. + tests/ # 存放测试用例
  10. package.json # 元数据文件
  11. README.md # 说明文件

其中部分文件内容如下:

  1. /* bin/node-echo */
  2. var argv = require('argv'),
  3. echo = require('../lib/echo');
  4. console.log(echo(argv.join(' ')));
  5. /* lib/echo.js */
  6. module.exports = function (message) {
  7. return message;
  8. };
  9. /* package.json */
  10. {
  11. "name": "node-echo",
  12. "main": "./lib/echo.js"
  13. }
  1. 以上例子中分类存放了不同类型的文件,并通过node_moudles目录直接使用三方包名加载模块。此外,定义了package.json之后,node-echo目录也可被当作一个包来使用。

三:文件操作

  1. 让前端觉得如获神器的不是NodeJS能做网络编程,而是NodeJS**能够操作文件。**<br /> NodeJS提供了基本的文件操作API,但是像文件拷贝这种高级功能就没有提供,因此我们先拿文件拷贝程序练手。与copy命令类似,我们的程序需要能接受源文件路径与目标文件路径两个参数。

小文件拷贝

我们使用NodeJS内置的fs模块简单实现这个程序如下。

  1. var fs = require('fs');
  2. function copy(src, dst) {
  3. fs.writeFileSync(dst, fs.readFileSync(src));
  4. }
  5. function main(argv) {
  6. copy(argv[0], argv[1]);
  7. }
  8. main(process.argv.slice(2));
  1. 以上程序使用fs.readFileSync从源路径读取文件内容,并使用fs.writeFileSync将文件内容写入目标路径。 <br /> 注:process是一个全局变量,可通过process.argv获得命令行参数。由于argv[0]固定等于NodeJS执行程序的绝对路径,argv[1]固定等于主模块的绝对路径,因此第一个命令行参数从argv[2]这个位置开始。

大文件拷贝

  1. 上边的程序拷贝一些小文件没啥问题,但这种一次性把所有文件内容都读取到内存中后再一次性写入磁盘的方式不适合拷贝大文件,内存会爆仓。对于大文件,我们只能**读一点写一点,直到完成拷贝**。因此上边的程序需要改造如下。
  1. var fs = require('fs');
  2. function copy(src, dst) {
  3. fs.createReadStream(src).pipe(fs.createWriteStream(dst));
  4. }
  5. function main(argv) {
  6. copy(argv[0], argv[1]);
  7. }
  8. main(process.argv.slice(2));
  1. 以上程序使用fs.createReadStream创建了一个源文件的只读数据流,并使用fs.createWriteStream创建了一个目标文件的只写数据流,并且用pipe方法把两个数据流连接了起来。连接起来后发生的事情,说得抽象点的话,就像水顺着水管从一个桶流到了另一个桶。

有关文件操作的API

Buffer(数据块):

官方文档: http://nodejs.org/api/buffer.html
JS语言自身只有字符串数据类型,没有二进制数据类型,因此NodeJS提供了一个与String对等的全局构造函数Buffer来提供对二进制数据的操作。除了可以读取文件得到Buffer的实例外,还能够直接构造,例如:

  1. var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);

Buffer与字符串类似,除了可以用.length属性得到字节长度外,还可以用[index]方式读取指定位置的字节 ,如:

  1. bin[0]; // => 0x68;

或者反过来,将字符串转换为指定编码下的二进制数据:

  1. var bin = new Buffer('hello', 'utf-8'); // => <Buffer 68 65 6c 6c 6f>
  1. Buffer与字符串有一个重要区别。字符串是只读的,并且对字符串的任何修改得到的都是一个新字符串,原字符串保持不变。至于Buffer,更像是可以做指针操作的C语言数组。例如,可以用[index]方式直接修改某个位置的字节。
  1. bin[0] = 0x48;

而.slice方法也不是返回一个新的Buffer,而更像是返回了指向原Buffer中间的某个位置的指针,如下所示。

  1. [ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]
  2. ^ ^
  3. | |
  4. bin bin.slice(2)

因此对.slice方法返回的Buffer的修改会作用于原Buffer,例如:

  1. var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
  2. var sub = bin.slice(2);
  3. sub[0] = 0x65;
  4. console.log(bin); // => <Buffer 68 65 65 6c 6f>
  1. 也因此,如果想要拷贝一份Buffer,**得首先创建一个新的Buffer,并通过.copy方法把原Buffer中的数据复制过去**。这个类似于申请一块新的内存,并把已有内存中的数据复制过去。以下是一个例子。
  1. var bin = new Buffer([ 0x68, 0x65, 0x6c, 0x6c, 0x6f ]);
  2. var dup = new Buffer(bin.length);
  3. bin.copy(dup);
  4. dup[0] = 0x48;
  5. console.log(bin); // => <Buffer 68 65 6c 6c 6f>
  6. console.log(dup); // => <Buffer 48 65 65 6c 6f>

总之,Buffer将JS的数据处理能力从字符串扩展到了任意二进制数据。

Stream(数据流)

官方文档: http://nodejs.org/api/stream.html
当内存中无法一次装下需要处理的数据时,或者一边读取一边处理更加高效时,我们就需要用到数据流。NodeJS中通过各种Stream来提供对数据流的操作。
以上边的大文件拷贝程序为例,我们可以为数据来源创建一个只读数据流,示例如下:

  1. var rs = fs.createReadStream(pathname);
  2. rs.on('data', function (chunk) {
  3. doSomething(chunk);
  4. });
  5. rs.on('end', function () {
  6. cleanUp();
  7. });

注:Stream基于事件机制工作,所有Stream的实例都继承于NodeJS提供的EventEmitter