模块(Module)和包(Package)是 Node.js 重要的支柱。开发一个具有一定规模的程 序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实 现这种方式而诞生的。在浏览器 JavaScript 中,脚本模块的拆分和组合通常使用 HTML 的 script 标签来实现。Node.js 提供了 require 函数来调用其他模块,而且模块都是基于 文件的,机制十分简单。
Node.js 的模块和包机制的实现参照了 CommonJS 的标准,但并未完全遵循。不过 两者的区别并不大,一般来说你大可不必担心,只有当你试图制作一个除了支持 Node.js 之外还要支持其他平台的模块或包的时候才需要仔细研究。通常,两者没有直接冲突的地方。
我们经常把 Node.js 的模块和包相提并论,因为模块和包是没有本质区别的,两个概念 也时常混用。如果要辨析,那么可以把包理解成是实现了某个功能模块的集合,用于发布 和维护。对使用者来说,模块和包的区别是透明的,因此经常不作区分。
模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是 JavaScript 代码、JSON 或者编译过的 C/C++ 扩展。 在前面章节的例子中,我们曾经用到了 var http = require('http')
,其中 http 是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 JavaScript 封装。我们通过 require 函数获取了这个模块,然后才能使用其中的对象。
在 Node.js 中,创建一个模块非常简单,因为一个文件就是一个模块,我们要关注的问 题仅仅在于如何在其他文件中获取这个模块。Node.js 提供了 exports 和 require 两个对 象,其中 exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即所获 取模块的 exports 对象。 让我们以一个例子来了解模块。创建一个 module.js 的文件,内容是:
var name;
exports.setName = function(thyName){
name = thyName;
};
exports.sayHello = function(){
console.log('Hello ' + name);
};
在同一目录下创建 getmodule.js,内容是:
var myModule = require('./module');
myModule.setName('Cai');
myModule.sayHello();
在以上示例中,module.js 通过 exports 对象把 setName 和 sayHello 作为模块的访 问接口,在 getmodule.js 中通过 require(‘./module’) 加载这个模块,然后就可以直接访 问 module.js 中 exports 对象的成员函数了。 这种接口封装方式比许多语言要简洁得多,同时也不失优雅,未引入违反语义的特性, 符合传统的编程逻辑。在这个基础上,我们可以构建大型的应用程序,npm 提供的上万个模 块都是通过这种简单的方式搭建起来的。
上面这个例子有点类似于创建一个对象,但实际上和对象又有本质的区别,因为 require 不会重复加载模块,也就是说无论调用多少次 require,获得的模块都是同一个。 我们在 getmodule.js 的基础上稍作修改,创建一个loadmodule.js :
var hello1 = require('./module');
hello1.setName('Cai');
var hello2 = require('./module');
hello2.setName("Kevin");
hello1.sayHello();
运行后发现输出结果是 Hello Kevin,这是因为变量 hello1 和 hello2 指向的是 同一个实例,因此 hello1.setName 的结果被 hello2.setName 覆盖,终输出结果是 由后者决定的。
有时候我们只是想把一个对象封装到模块中,例如:
//singleobject.js
function Hello(){
var name;
this.setName = function(thyName){
name = thyName;
};
this.sayHello = function(){
console.log('Hello ' + name);
};
};
exports.Hello = Hello;
此时我们在其他文件中需要通过 require('./singleobject').Hello
来获取 Hello 对象,这略显冗余,可以用下面方法稍微简化。
//gethello.js
function Hello(){
var name;
this.setName = function(thyName){
name = thyName;
};
this.sayHello = function(){
console.log('Hello ' + name);
};
};
module.exports = Hello;
这样就可以直接获得这个对象了:
var Hello = require('./hello');
hello = new Hello();
hello.setName('Kevin');
hello.sayHello();
注意,模块接口的唯一变化是使用 module.exports = Hello 代替了 exports.Hello= Hello。在外部引用该模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的 exports。
事实上,exports 本身仅仅是一个普通的空对象,即 {},它专门用来声明接口,本 质上是通过它为模块闭包的内部建立了一个有限的访问接口。因为它没有任何特殊的地方, 所以可以用其他东西来代替,譬如我们上面例子中的 Hello 对象。
包是在模块基础上更深一步的抽象,Node.js 的包类似于 C/C++ 的函数库或者 Java/.Net 的类库。它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。Node.js 根 据 CommonJS 规范实现了包机制,开发了 npm来解决包的发布和获取需求。
Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符 合 CommonJS 规范的包应该具备以下特征:
Node.js 对包的要求并没有这么严格,只要顶层目录下有 package.json,并符合一些规范 即可。当然为了提高兼容性,我们还是建议你在制作包的时候,严格遵守 CommonJS 规范。
模块与文件是一一对应的。文件不仅可以是 JavaScript 代码或二进制代码,还可以是一 个文件夹。简单的包,就是一个作为文件夹的模块。下面我们来看一个例子,建立一个叫 做 somepackage 的文件夹,在其中创建 index.js,内容如下:
exports.hello = function(){
console.log('Hello.');
};
然后在 somepackage 之外建立 getpackage.js,内容如下:
var somePackage = require('./somepackage');
somePackage.hello();
运行 node getpackage.js,控制台将输出结果
我们使用这种方法可以把文件夹封装为一个模块,即所谓的包。包通常是一些模块的集 合,在模块的基础上提供了更高层的抽象,相当于提供了一些固定接口的函数库。通过定制 package.json,我们可以创建更复杂、更完善、更符合规范的包用于发布。
在前面例子中的 somepackage 文件夹下,我们创建一个叫做 package.json 的文件,内容如 下所示:
{
"main" : "./lib/interface.js"
}
然后将 index.js 重命名为 interface.js 并放入 lib 子文件夹下。以同样的方式再次调用这个包,依然可以正常使用。
Node.js 在调用某个包时,会首先检查包中 package.json 文件的 main 字段,将其作为 包的接口模块,如果 package.json 或 main 字段不存在,会尝试寻找 index.js 或 index.node 作 为包的接口。
package.json 是 CommonJS 规定的用来描述包的文件,完全符合规范的 package.json 文 件应该含有以下字段。
Node.js包管理器,即npm是 Node.js 官方提供的包管理工具,它已经成了 Node.js 包的 标准发布平台,用于 Node.js 包的发布、传播、依赖控制。npm 提供了命令行工具,使你可 以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包
使用 npm 安装包的命令格式为:
npm [install/i] [package_name]
例如你要安装 express,可以在命令行运行:
$ npm install express
或者
$ npm i express
此时 express 就安装成功了,并且放置在当前目录的 node_modules 子目录下。npm 在获取 express 的时候还将自动解析其依赖,并获取 express 依赖的 mime、mkdirp、 qs 和 connect
npm在默认情况下会从http://npmjs.org搜索或下载包,将包安装到当前目录的node_modules 子目录下。
在使用 npm 安装包的时候,有两种模式:本地模式和全局模式。默认情况下我们使用 npm install命令就是采用本地模式,即把包安装到当前目录的 node_modules 子目录下。Node.js 的 require 在加载模块时会尝试搜寻 node_modules 子目录,因此使用 npm 本地模式安装 的包可以直接被引用。 npm 还有另一种不同的安装模式被成为全局模式,使用方法为:
npm [install/i] -g [package_name
与本地模式的不同之处就在于多了一个参数 -g
。我们在 介绍 supervisor那个小节中使用 了 npm install -g supervisor
命令,就是以全局模式安装 supervisor。
为什么要使用全局模式呢?多数时候并不是因为许多程序都有可能用到它,为了减少多 重副本而使用全局模式,而是因为本地模式不会注册 PATH 环境变量。举例说明,我们安装 supervisor 是为了在命令行中运行它,譬如直接运行 supervisor script.js,这时就需要在 PATH 环境变量中注册 supervisor。npm 本地模式仅仅是把包安装到 node_modules 子目录下,其中 的 bin 目录没有包含在 PATH 环境变量中,不能直接在命令行中调用。而当我们使用全局模 式安装时,npm 会将包安装到系统目录,譬如 /usr/local/lib/node_modules/,同时 package.json 文 件中 bin 字段包含的文件会被链接到 /usr/local/bin/。/usr/local/bin/ 是在PATH 环境变量中默认 定义的,因此就可以直接在命令行中运行 supervisor script.js命令了。
总而言之,当我们要把某个包作为工程运行时的一部分时,通过本地模式获取,如果要 在命令行下使用,则使用全局模式安装。
npm 提供了一个有趣的命令 npm link
,它的功能是在本地包和全局包之间创建符号链 接。我们说过使用全局模式安装的包不能直接通过 require 使用,但通过 npm link
命令 可以打破这一限制。举个例子,我们已经通过 npm install -g express
安装了 express, 这时在工程的目录下运行命令:
$ npm link express ./node_modules/express -> /usr/local/lib/node_modules/express
我们可以在 node_modules 子目录中发现一个指向安装到全局的包的符号链接。通过这 种方法,我们就可以把全局包当本地包来使用了。
注意:npm link 命令不支持 Windows。
除了将全局的包链接到本地以外,使用 npm link命令还可以将本地的包链接到全局。 使用方法是在包目录( package.json 所在目录)中运行 npm link 命令。如果我们要开发 一个包,利用这种方法可以非常方便地在不同的工程间进行测试。
npm 可以非常方便地发布一个包,比 pip、gem、pear 要简单得多。在发布之前,首先 需要让我们的包符合 npm 的规范,npm 有一套以 CommonJS 为基础包规范,但与 CommonJS 并不完全一致,其主要差别在于必填字段的不同。通过使用 npm init 可以根据交互式问答 产生一个符合标准的 package.json,例如创建一个名为 byvoidmodule 的目录,然后在这个 目录中运行npm init
。
这样就在 byvoidmodule 目录中生成一个符合 npm 规范的 package.json 文件。创建一个 index.js 作为包的接口,一个简单的包就制作完成了。
在发布前,我们还需要获得一个账号用于今后维护自己的包,使用 npm adduser
根据 提示输入用户名、密码、邮箱,等待账号创建完成。完成后可以使用 npm whoami 测验是 否已经取得了账号。 接下来,在 package.json 所在目录下运行 npm publish
,稍等片刻就可以完成发布了。 打开浏览器,访问 http://search.npmjs.org/ 就可以找到自己刚刚发布的包了。现在我们可以在 世界的任意一台计算机上使用 npm install byvoidmodule
命令来安装它。
如果你的包将来有更新,只需要在 package.json 文件中修改 version 字段,然后重新 使用 npm publish
命令就行了。如果你对已发布的包不满意(比如我们发布的这个毫无意 义的包),可以使用 npm unpublish
命令来取消发布。
写程序时免不了遇到 bug,而当 bug 发生以后,除了抓耳挠腮之外,一个常用的技术是 单步调试。在写 C/C++ 程序的时候,我们有 Visual Studio、gdb 这样顺手的调试器,而脚本 语言开发者就没有这么好的待遇了。多年以来,像 JavaScript 语言一直缺乏有效的调试手段, “攻城师”只能依靠“眼观六路,耳听八方”的方式进行静态查错,或者在代码之间添加冗 长的输出语句来分析可能出错的地方。直到有了 FireBug、Chrome 开发者工具,JavaScript 才 算有了基本的调试工具。在没有编译器或解译器的支持下,为缺乏内省机制的语言实现一个 调试器是几乎不可能的。Node.js 的调试功能正是由 V8 提供的,保持了一贯的高效和方便的 特性。尽管你也许已经对原始的调试方式十分适应,而且有了一套高效的调试技巧,但我们 还是想介绍一下如何使用 Node.js 内置的工具和第三方模块来进行单步调试。
Node.js 支持命令行下的单步调试。下面是一个简单的程序:
var a = 1;
var b = "world";
var c = function(x){
console.log('hello ' + x + a);
};
c(b);
在命令行下执行 node debug debug.js
,将会启动调试工具:
这样就打开了一个 Node.js 的调试终端,我们可以用一些基本的命令进行单步跟踪调试。
V8 提供的调试功能是基于 TCP 协议的,因此 Node.js 可以轻松地实现远程调试。在命 令行下使用以下两个语句之一可以打开调试服务器:
node --debug[=port] script.js
node --debug-brk[=port] script.js
node –debug 命令选项可以启动调试服务器,默认情况下调试端口是 5858,也可以 使用 –debug=1234 指定调试端口为 1234。使用 –debug 选项运行脚本时,脚本会正常 执行,但不会暂停,在执行过程中调试客户端可以连接到调试服务器。如果要求脚本暂停执 行等待客户端连接,则应该使用 –debug-brk 选项。这时调试服务器在启动后会立刻暂停 执行脚本,等待调试客户端连接。
当调试服务器启动以后,可以用命令行调试工具作为调试客户端连接,例如:
在一个终端中:
在另一个终端:
事实上,当使用 node debug debug.js 命令调试时,只不过是用 Node.js 命令行工 具将以上两步工作自动完成而已。