Node.js 模块和包(Modules)

简介:

模块(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 中,创建一个模块非常简单,因为一个文件就是一个模块,例如:

1
2
var  hello = require( './hello' );
hello.world();


上例中,代码 require(' ./hello ') 引入了当前目录下的 hello.js 文件(./ 表示当前目录,Node.js 默认后缀为 js)


Node.js 提供了两个 exports 和 require 两个对象,其中,exports 是模块公开的接口,require 用于从外部获取一个模块的接口,即获取模块的 exports 对象。


通过一个例子简单的了解下模块,创建一个 module.js 文件,代码如下:

1
2
3
4
5
6
7
8
9
var  name;
 
exports.setName =  function ( uname ){
     name = uname;
};
 
exports.sayHello =  function (){
     console.log( 'Hello'  +name);
};


在同一目录下创建 getmoudle.js 文件,代码如下:

1
2
3
4
5
//引用当前目录下的 module.js 模块
var  myModule = require( './module' );
 
myModule.setName( 'Roger' );
myModule.sayHello();


运行 node getmodule.js,控制台输出结果为:

wKiom1h1mUjRXUbJAAAIGe_mq3Y049.png


在这个例子中,module.js 通过 exports 对象 setName 和 sayHello 作为模块的访问接口,在 getmodule.js 中通过 require(' ./module ') 加载这个模块,然后就可以直接访问 module.js 中 exports 对象的成员函数了。


这种接口封装方式比许多语言要简洁的多,同时也不失优雅,未引入违反语义的特性,符合传统的编程逻辑。在这个基础上,我们可以构建大型的应用程序,npm 提供的上万个模块都是通过这种方式搭建起来的。


2-2、单次加载

上面这个例子有点类似于创建一个对象,但实际上和对象又有本质的区别,因为 require 不会重复加载模块,也就是说无论调用多少次 require,获得的模块都是同一个。


新建一个 loadmoudle.js 文件,代码如下:

1
2
3
4
5
6
7
var  hello1 = require( './module' );
hello1.setName( 'Roger' );
 
var  hello2 = require( './module' );
hello2.setName( 'Sarahling' );
 
hello1.sayHello();


运行 node loadmodule.js,控制台输出结果为:

wKiom1h1nTvCxY5bAAAIX4PGt00812.png


为什么不是“Hello Roger”呢?这是因为变量 hello1 和 hello2 指向的是同一个实例,因此 hello1.setName 的结果被 hello2.setName 覆盖,最终输出结果是由后者决定的。


2-3、覆盖 exports

有时候我们只是想把一个对象封装到模块中,例如,新建一个 singleObject.js 文件,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
function  Hello(){
     var  name;
     this .setName =  function (uname){
         name = uname;
     };
 
     this .sayHello =  function (){
         console.log( 'Hello ' +name);
     }
}
 
module.exports = Hello;


新建一个 getHello.js 文件,获得这个对象:

1
2
3
4
5
var  Hello = require( './singleObject' );
 
hello =  new  Hello();
hello.setName( 'Bob' );
hello.sayHello();


运行 node getHello.js,控制台输出结果为:

wKioL1h1ovqDCl5tAAAHmhFLbQU352.png

注意,模块接口的唯一变化是使用 module.exports = Hello 代替了 exports.Hello = Hello。在外部引用模块时,其接口对象就是要输出的 Hello 对象本身,而不是原先的 exports。


事实上,exports 本身仅仅是一个普通的空对象,即{},它专门用来声明接口,本质上是通过它为模块闭包的内部建立一个有限的访问接口。因为它没有任何特殊的地方,所以可用其它东西来代替。


注意:不可以通过对 exports 直接赋值代替对 module.exports 赋值。exports 实际上只是一个和 module.exports 指向同一个对象的变量,它本身会在模块执行结束后释放,但 module 不会,因此只能通过指定 module.exports 来改变访问接口。



3、服务端的模块放在哪里


在 Node.js 初识 HTTP 模块 介绍中,我们就已经见到了模块的使用,像这样

1
2
3
4
5
6
7
8
9
10
//第一步: 引入 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 方法及其简单,但是内部的加载却是十分复杂的,其加载优先级也各自不同

wKioL1h1qJeRYNAfAAEd0EGAyCs470.png


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,内容如下:

1
2
3
exports.hello =  function (){
     console.log( 'Hello World!' );
}


然后在 somepackage 之外建立 getpackage.js,内容如下:

1
2
3
var  somePackage = require( './somepackage' );
 
somePackage.hello();


运行 getpackage.js,控制输出结果如下:

wKiom1h1q1zyJvtRAACBpoebTbw745.png

我们使用这种方法可以把文件夹封装为一个模块,即所谓的包。包通常是一些模块的集合,在模块的基础上提供了更高层的抽象,相当于提供了一些固定接口的函数库。通过定制package.json,我们可以创建更复杂、更完善、更符合规范的包用于发布。



4-2、package.json

在 somepackage 文件夹下,创建一个叫做 package.json 的文件,内容如下所示:

1
2
3
{
   "main" : "./lib/interface.js"
}


然后将 index.js 重命名为 interface.js 并放入 lib 子文件夹下。以同样的方式再次调用这个包,依然可以正常使用。

wKiom1h1rP6QIrxWAAB_HJPABs4737.png


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 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
{
   "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 安装包的命令格式为:

1
npm [install/i]  [package_name]


比如你要安装 express,可以在命令行运行:

1
$ npm install express

1
$ npm i express


可以看到安装信息如下:

wKiom1h1wsWDHNThAAA1wp9nzG4572.png

wKioL1h1w2SgZKyeAABd3MoQzh8671.png


此时 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 还有另一种不同的安装模式被成为全局模式,使用方法为:

1
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,这时在工程的目录下运行命令:

1
2
$ 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:


wKiom1h1yJjhtEIzAABWVNPd3fg872.png

这样就在 byvoidmodule 目录中生成一个符合 npm 规范的 package.json 文件。创建一个index.js 作为包的接口,一个简单的包就制作完成了。


在发布前,我们还需要获得一个账号用于今后维护自己的包,使用 npm adduser 根据提示输入用户名、密码、邮箱,等待账号创建完成。完成后可以使用 npm whoami 测验是否已经取得了账号。


接下来,在 package.json 所在目录下运行 npm publish,稍等片刻就可以完成发布了。


打开浏览器,访问 http://search.npmjs.org/ 就可以找到自己刚刚发布的包了。现在我们可以在世界的任意一台计算机上使用 npm install byvoidmodule 命令来安装它。下图是npmjs.org上包的描述页面。

wKioL1h1yarTnddAAAEwRhMWehI250.png


如果你的包将来有更新,只需要在 package.json 文件中修改 version 字段,然后重新使用 npm publish 命令就行了。如果你对已发布的包不满意(比如我们发布的这个毫无意义的包),可以使用 npm unpublish 命令来取消发布。


本文转自   frwupeng517   51CTO博客,原文链接:http://blog.51cto.com/dapengtalk/1891051


相关文章
|
2月前
|
小程序 JavaScript 前端开发
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
uni-app开发微信小程序:四大解决方案,轻松应对主包与vendor.js过大打包难题
761 1
|
2月前
|
缓存 JavaScript 安全
nodejs里面的http模块介绍和使用
综上所述,Node.js的http模块是构建Web服务的基础,其灵活性和强大功能,结合Node.js异步非阻塞的特点,为现代Web应用开发提供了坚实的基础。
126 62
|
3月前
|
JavaScript 前端开发
Vue、ElementUI配合Node、multiparty模块实现图片上传并反显_小demo
如何使用Vue和Element UI配合Node.js及multiparty模块实现图片上传并反显的功能,包括前端的Element UI组件配置和后端的Node.js服务端代码实现。
64 1
|
2月前
|
缓存 JSON JavaScript
Node.js模块系统
10月更文挑战第4天
48 2
|
2月前
|
JavaScript 应用服务中间件 Apache
Node.js Web 模块
10月更文挑战第7天
34 0
|
2月前
|
JavaScript 网络协议
Node.js 工具模块
10月更文挑战第7天
26 0
|
3月前
|
存储 JavaScript 前端开发
[JS] ES Modules的运作原理
【9月更文挑战第16天】ES Modules(ECMAScript Modules)是 JavaScript 中的一种模块化开发规范,适用于浏览器和 Node.js 环境。它通过 `export` 和 `import` 关键字实现模块的导出与导入。模块定义清晰,便于维护和测试。JavaScript 引擎会在执行前进行静态分析,确保模块按需加载,并处理循环依赖。ES Modules 支持静态类型检查,现代浏览器已原生支持,还提供动态导入功能,增强了代码的灵活性和性能。这一规范显著提升了代码的组织和管理效率。
|
2月前
|
JavaScript 前端开发 应用服务中间件
Node.js Web 模块
Node.js Web 模块
|
3月前
Nest.js 实战 (十二):优雅地使用事件发布/订阅模块 Event Emitter
这篇文章介绍了在Nest.js构建应用时,如何通过事件/发布-订阅模式使应用程序更健壮、灵活、易于扩展,并简化服务间通信。文章主要围绕@nestjs/event-emitter模块展开,这是一个基于eventemitter2库的社区模块,提供了事件发布/订阅功能,使得实现事件驱动架构变得简单。文章还介绍了如何使用该模块,包括安装依赖、初始化模块、注册EventEmitterModule、使用装饰器简化监听等。最后总结,集成@nestjs/event-emitter模块可以提升应用程序的事件驱动能力,构建出更为松耦合、易扩展且高度灵活的系统架构,是构建现代、响应迅速且具有高度解耦特性的Nest.
|
3月前
|
缓存 JavaScript 前端开发
JavaScript模块化开发:ES6模块与CommonJs的对比与应用
JavaScript模块化开发:ES6模块与CommonJs的对比与应用
37 2