热门IT资讯网

Node.js 模块和包(Modules)

发表于:2024-11-24 作者:热门IT资讯网编辑
编辑最后更新 2024年11月24日,模块(Module)和包(Package)是 Node.js 最重要的支柱。开发一个具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实现这种方式而诞生的。

模块(Module)和包(Package)是 Node.js 最重要的支柱。开发一个具有一定规模的程序不可能只用一个文件,通常需要把各个功能拆分、封装,然后组合起来,模块正是为了实现这种方式而诞生的。在浏览器 Javascript 中,脚本模块的拆分和组合通常使用 HTML 的 script 标签来实现。Node.js 提供了 require 函数来调用其它模块,而且模块都是基于文件的。


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": "[email protected]",    }  ],  "contributors": [    {      "name": "BYVoid",      "web": "http://www.byvoid.com/"    }  ],  "bugs": {    "mail": "[email protected]",    "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 命令来取消发布。



0