模块(Module)和包(Package)是 Node.js 最重要的支柱。开发一个具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实现这种方式而诞生的。在浏览器 Javascript 中,脚本模块的拆分和组合通常使用 HTML 的 script 标签来实现。Node.js 提供了 require 函数来调用其它模块,而且模块都是基于文件的。
创新互联建站专注于企业全网整合营销推广、网站重做改版、江安网站定制设计、自适应品牌网站建设、H5网站设计、商城建设、集团公司官网建设、外贸网站制作、高端网站制作、响应式网页设计等建站业务,价格优惠性价比高,为江安等各大城市提供网站开发制作服务。
Node.js 的模块和包经常被相提并论,因为模块和包是没有本质区别的,两个概念也时常混用。如果要辨析,那么可以把包理解成是实现某个功能模块的集合,用于发布和维护。对使用者来说,模块和包的区别是透明的,因此经常不做区分。
1、什么是模块
模块是 Node.js 应用程序的基本组成部分,文件和模块是一一对应的。换言之,一个 Node.js 文件就是一个模块,这个文件可能是 Javascript 代码、JSON 或者编译过的 C/C++ 扩展。
例如 var http = require(' http '),其中,http 是 Node.js 的一个核心模块,其内部是用 C++ 实现的,外部用 Javascript 封装。我们通过 require 函数获取了这个模块,然后才能使用其中的对象。
2、创建及加载模块
2-1、创建模块
在 Node.js 中,创建一个模块非常简单,因为一个文件就是一个模块,例如:
var hello = require('./hello'); hello.world();
上例中,代码 require(' ./hello ') 引入了当前目录下的 hello.js 文件(./ 表示当前目录,Node.js 默认后缀为 js)
Node.js 提供了两个 exports 和 require 两个对象,其中,exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即获取模块的 exports 对象。
通过一个例子简单的了解下模块,创建一个 module.js 文件,代码如下:
var name; exports.setName = function( uname ){ name = uname; }; exports.sayHello = function(){ console.log('Hello' +name); };
在同一目录下创建 getmoudle.js 文件,代码如下:
//引用当前目录下的 module.js 模块 var myModule = require('./module'); myModule.setName('Roger'); myModule.sayHello();
运行 node getmodule.js,控制台输出结果为:
在这个例子中,module.js 通过 exports 对象 setName 和 sayHello 作为模块的访问接口,在 getmodule.js 中通过 require(' ./module ') 加载这个模块,然后就可以直接访问 module.js 中 exports 对象的成员函数了。
这种接口封装方式比许多语言要简洁的多,同时也不失优雅,未引入违反语义的特性,符合传统的编程逻辑。在这个基础上,我们可以构建大型的应用程序,npm 提供的上万个模块都是通过这种方式搭建起来的。
2-2、单次加载
上面这个例子有点类似于创建一个对象,但实际上和对象又有本质的区别,因为 require 不会重复加载模块,也就是说无论调用多少次 require,获得的模块都是同一个。
新建一个 loadmoudle.js 文件,代码如下:
var hello1 = require('./module'); hello1.setName('Roger'); var hello2 = require('./module'); hello2.setName('Sarahling'); hello1.sayHello();
运行 node loadmodule.js,控制台输出结果为:
为什么不是“Hello Roger”呢?这是因为变量 hello1 和 hello2 指向的是同一个实例,因此 hello1.setName 的结果被 hello2.setName 覆盖,最终输出结果是由后者决定的。
2-3、覆盖 exports
有时候我们只是想把一个对象封装到模块中,例如,新建一个 singleObject.js 文件,代码如下:
function Hello(){ var name; this.setName = function(uname){ name = uname; }; this.sayHello = function(){ console.log('Hello '+name); } } module.exports = Hello;
新建一个 getHello.js 文件,获得这个对象:
var Hello = require('./singleObject'); hello = new Hello(); hello.setName('Bob'); hello.sayHello();
运行 node getHello.js,控制台输出结果为:
注意,模块接口的唯一变化是使用 module.exports = Hello 代替了 exports.Hello = Hello。在外部引用模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的 exports。
事实上,exports 本身仅仅是一个普通的空对象,即{},它专门用来声明接口,本质上是通过它为模块闭包的内部建立一个有限的访问接口。因为它没有任何特殊的地方,所以可用其它东西来代替。
注意:不可以通过对 exports 直接赋值代替对 module.exports 赋值。exports 实际上只是一个和 module.exports 指向同一个对象的变量,它本身会在模块执行结束后释放,但 module 不会,因此只能通过指定 module.exports 来改变访问接口。
3、服务端的模块放在哪里
在 Node.js 初识 HTTP 模块 介绍中,我们就已经见到了模块的使用,像这样
//第一步: 引入 http 模块 var http = require('http'); //第二步:创建一个服务器(requestListener 是一个函数,里面有2个参数,一个请求消息,一个响应消息) var server = http.createServer(function(req, res){ }); //第三步:服务器监听本地的82端口 server.listen(8082, '127.0.0.1');
Node.js 中自带了一个叫做 http 的模块,我们在代码中请求它并把返回值赋值给一个本地变量,这把我们的本地变量变成了一个拥有所有 http 模块所提供的公共方法的对象。
Node.js 的 require 方法中文件查找策略如下:
由于 Node.js 中存在 4 类模块(原生模块和 3种文件模块),尽管 require 方法及其简单,但是内部的加载却是十分复杂的,其加载优先级也各自不同
3-1、从文件模块缓存中加载
尽管原生模块与文件模块的优先级不同,但是都不会优先于从文件模块的缓存中加载已经存在的模块。
3-2、从原生模块加载
原生模块的优先级仅次于文件模块缓存的优先级。require方法在解析文件名之后,优先检查模块是否在原生模块列表中。以http模块为例,尽管在目录下存在一个http/http.js/http.node/http.json文件,require("http")都不会从这些文件中加载,而是从原生模块中加载。
原生模块也有一个缓存区,同样也是优先从缓存区加载。如果缓存区没有被加载过,则调用原生模块的加载方式进行加载和执行。
3-3、从文件加载
当文件模块缓存中不存在,而且不是原生模块的时候,Node.js会解析require方法传入的参数,并从文件系统中加载实际的文件,加载过程中的包装和编译细节在前一节中已经介绍过,这里我们将详细描述查找文件模块的过程,其中,也有一些细节值得知晓。
require方法接受以下几种参数的传递:
http、fs、path等,原生模块。
./mod或../mod,相对路径的文件模块。
/pathtomodule/mod,绝对路径的文件模块。
mod,非原生模块的文件模块。
4、创建包
包是在模块基础上更深一步的抽象,Node.js 的包类似于 C/C++ 的函数库或者 Java/.Net的类库。它将某个独立的功能封装起来,用于发布、更新、依赖管理和版本控制。Node.js 根据 CommonJS 规范实现了包机制,开发了 npm来解决包的发布和获取需求。
Node.js 的包是一个目录,其中包含一个 JSON 格式的包说明文件 package.json。严格符合CommonJS 规范的包应该具备以下特征:
package.json 必须在包的顶层目录下;
二进制文件应该在 bin 目录下;
JavaScript 代码应该在 lib 目录下;
文档应该在 doc 目录下;
单元测试应该在 test 目录下。
Node.js 对包的要求并没有这么严格,只要顶层目录下有 package.json,并符合一些规范即可。当然为了提高兼容性,我们还是建议你在制作包的时候,严格遵守 CommonJS 规范。
4-1、作为文件夹的模块
模块与文件是一一对应的。文件不仅可以是 JavaScript 代码或二进制代码,还可以是一个文件夹。最简单的包,就是一个作为文件夹的模块。下面我们来看一个例子,建立一个叫做 somepackage 的文件夹,在其中创建 index.js,内容如下:
exports.hello = function(){ console.log('Hello World!'); }
然后在 somepackage 之外建立 getpackage.js,内容如下:
var somePackage = require('./somepackage'); somePackage.hello();
运行 getpackage.js,控制输出结果如下:
我们使用这种方法可以把文件夹封装为一个模块,即所谓的包。包通常是一些模块的集合,在模块的基础上提供了更高层的抽象,相当于提供了一些固定接口的函数库。通过定制package.json,我们可以创建更复杂、更完善、更符合规范的包用于发布。
4-2、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 文件应该含有以下字段。
name:包的名称,必须是唯一的,由小写英文字母、数字和下划线组成,不能包含空格。
description:包的简要说明。
version:符合语义化版本识别规范的版本字符串。
keywords:关键字数组,通常用于搜索。
maintainers:维护者数组,每个元素要包含 name、email (可选)、web (可选)字段。
contributors:贡献者数组,格式与maintainers相同。包的作者应该是贡献者数组的第一个元素。
bugs:提交bug的地址,可以是网址或者电子邮件地址。
licenses:许可证数组,每个元素要包含 type (许可证的名称)和 url (链接到许可证文本的地址)字段。
repositories:仓库托管地址数组,每个元素要包含 type (仓库的类型,如 git )、url (仓库的地址)和 path (相对于仓库的路径,可选)字段。
dependencies:包的依赖,一个关联数组,由包名称和版本号组成。
下面是一个完全符合 CommonJS 规范的 package.json 示例:
{ "name": "mypackage", "description": "Sample package for CommonJS. This package demonstrates the required elements of a CommonJS package.", "version": "0.7.0", "keywords": [ "package", "example" ], "maintainers": [ { "name": "Bill Smith", "email": "bills@example.com", } ], "contributors": [ { "name": "BYVoid", "web": "http://www.byvoid.com/" } ], "bugs": { "mail": "dev@example.com", "web": "http://www.example.com/bugs" }, "licenses": [ { "type": "GPLv2", "url": "http://www.example.org/licenses/gpl.html" } ], "repositories": [ { "type": "git", "url": "http://github.com/BYVoid/mypackage.git" } ], "dependencies": { "webkit": "1.2", "ssl": { "gnutls": ["1.0", "2.0"], "openssl": "0.9.8" } } }
4-3、Node.js 包管理器
Node.js包管理器,即npm是 Node.js 官方提供的包管理工具,它已经成了 Node.js 包的标准发布平台,用于 Node.js 包的发布、传播、依赖控制。npm 提供了命令行工具,使你可以方便地下载、安装、升级、删除包,也可以让你作为开发者发布并维护包。
(1)、获取一个包
使用 npm 安装包的命令格式为:
npm [install/i] [package_name]
比如你要安装 express,可以在命令行运行:
$ npm install express
或
$ npm i express
可以看到安装信息如下:
此时 express 就安装成功了,并且放置在当前目录的 node_moudles 子目录下。npm 在获取 express 的时候还将自动解析其依赖,并获取 express 依赖的 mime、mkdirp、qs 和 connect。
(2)、本地模式 和 全局模式
npm 在默认情况下会从 https://www.npmjs.com/ 搜索或下载包,将包安装到当前目录的 node_moudles 子目录下。
在使用 npm 安装包的时候,有两种模式:本地模式和全局模式。默认情况下我们使用 npm install 命令就是采用本地模式,即把包安装到当前目录的 node_modules 子目录下。Node.js 的 require 在加载模块时会尝试搜寻 node_modules 子目录,因此使用 npm 本地模式安装的包可以直接被引用。
npm 还有另一种不同的安装模式被成为全局模式,使用方法为:
npm [install/i] -g [package_name]
与本地模式的不同之处就在于多了一个参数 -g。
为什么要使用全局模式呢?多数时候并不是因为许多程序都有可能用到它,为了减少多重副本而使用全局模式,而是因为本地模式不会注册 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命令了。
本地模式和全局模式的特点如下表所示:
模式 | 可通过 require 使用 | 注册PATH |
本地模式 | 是 | 否 |
全局模式 | 否 | 是 |
总而言之,当我们要把某个包作为工程运行时的一部分时,通过本地模式获取,如果要 在命令行下使用,则使用全局模式安装。
(3)、创建全局链接
npm 提供了一个有趣的命令 npm link(不支持 Windows),它的功能是在本地包和全局包之间创建符号链接。我们说过使用全局模式安装的包不能直接通过 require 使用,但通过 npm link命令可以打破这一限制。举个例子,我们已经通过 npm install -g express 安装了 express,这时在工程的目录下运行命令:
$ npm link express ./node_modules/express -> /usr/local/lib/node_modules/express
我们可以在 node_modules 子目录中发现一个指向安装到全局的包的符号链接。通过这 种方法,我们就可以把全局包当本地包来使用了。
除了将全局的包链接到本地以外,使用 npm link命令还可以将本地的包链接到全局。使用方法是在包目录( package.json 所在目录)中运行 npm link 命令。如果我们要开发一个包,利用这种方法可以非常方便地在不同的工程间进行测试。
(4)、包的发布
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 命令来安装它。下图是npmjs.org上包的描述页面。
如果你的包将来有更新,只需要在 package.json 文件中修改 version 字段,然后重新使用 npm publish 命令就行了。如果你对已发布的包不满意(比如我们发布的这个毫无意义的包),可以使用 npm unpublish 命令来取消发布。