基本概念
包是实现了某些功能模块的集合,它将摸个独立的功能封装起来, 用于发布、更新、依赖管理和版本控制.
模块和文件是一一对应的, 一个Node.js文件就是一个模块(类似于python的模块),这个文件可能是JavaScript代码 (*.js), JSON(*.json)或者编译过的C/C++扩展(*.node).
创建模块
创建模块非常简单,因为一个文件就是一个模块, 我们需要关注的是如何在其他文件中获取这个模块.
Node.js 提供了exports
和require
两个对象,其中exports是模块公开的接口, require
用于从外部获取一个模块的接口,即获取外部模块的exports
对象
示例:
1. 覆盖exports
// hello.js |
2. 为exports
增加对象属性
// hello.js |
创建包
Node.js根据CommonJS规范实现了包机制, 开发npm来解决包的发布和获取需求.
CommonJS规范的包具有以下特征:
package.json
必须在包的顶层目录下.- 二进制文件应该在
bin
目录下. - JavaScript 代码应该在
lib
目录下. - 文档应该在
doc
目录下. - 单元测试应该在
test
目录下.
Node.js对包的要求并没有这么严格,只要顶层目录下有package.json,并符合一些规范即可. 不过,为了提高兼容性,在制作包的时候,尽量遵守CommonJS规范.
package.json
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 : 包的依赖, 一个关联数组, 由包的名称和版本号组成.
npm
npm是Node.js官方提供的包管理工具,已经成为Node.js包标准发布平台.
npm install/i [package_name]
本地模式,将包安装到当前目录npm install/i [package_name] -g
全局模式.
注意: 使用全局模式安装的包并不能直接在JavaScript文件中用require
获得, 因为requier
不会搜索/usr/local/lib/node_modules/
Node的模块实现
在Node中引入模块,需要经历以下3个过程:
- 路径分析
- 文件定位
- 编译执行
在Node中,模块氛围两类: 一类是Node自身提供的模块,称为核心模块, 另一类是用户编写的模块, 称为文件模块.
核心模块在Node源代码编译过程中(指Node安装过程中),被编译进二进制执行文件中, 在执行时, 部分核心模块就被直接加载进内存中, 所以这部分核心模块引入时, 文件定位和编译执行两个步骤可以省略掉, 并在路径分析中优先判断, 所以加载速度是最快的.
文件模块是在运行时动态加载的, 需要完整的路径分析、文件定位、编译执行过程.
Node对引入过的模块都会进行缓存, 以减少二次引入时开销, 它缓存的是编译和执行之后的对象。 不论是核心模块,还是文件模块,require方法对相同模块的二次加载, 都会采用缓存优先的方式,具有最高优先级. 核心模块的缓存检查优先于文件模块的缓存检查.
路径分析
- 核心模块的优先级仅次于缓存加载, 如果想引入用户自己编写的一个http用户模块, 如果想加载成功,必须换用其他标识符, 或者换用路径的方式(如下)
- 路径形式的文件模块:以
.
,..
和/
开始的标识符, 都被当做文件模块处理, 由于指定了文件的确切位置, 查找过程中可以节省一些时间. - 自定义模块: 会在模块路径( module.paths )中查找, 查找最为费时. 模块路径的生成规则是
- 当前文件目录下的 node_modules目录
- 父目录的node_modules目录
- 父目录的父目录的node_modules目录
- … 路径向上逐级递归,直到根目录下的node_modules目录
文件定位
文件扩展名分析
CommonJS规范允许标识符中不包含文件扩展名, Node会按照.js
, .json
, .node
的次序补足扩展名, 依次尝试, 这里有个小建议, 如果是.json
,.node
文件,在传递给require()的标识符中带上扩展名,会加快一点速度.
目录和包的处理
通过分析文件扩展名后,可能没有找到对应的文件,但却得到一个目录,此时Node会将目录当成一个包来处理.
在这个过程中, 首先,Node会在目录下查找package.json
, 通过JSON.parse()
取出main属性指定的文件名进行定位,如果缺省扩展名,将会进入扩展名分析步骤, 如果main属性指定的文件名错误,或者没有package.json, node会默认将index作为文件名,依次查找index.js
,index.json
, index.node
.
如果没能成功定位任何文件,会进入下一个路径查找, 如果遍历所有路径也没能定位文件,则抛出异常.
注意,目录和包处理是在文件扩展名分析失败后进行的
模块编译
定位到具体文件之后, Node会新建一个模块对象,然后根据路径载入并编译。 对于不同的文件扩展名, 载入方式有所不同, 具体如下:
- .js 文件 : 通过fs模块同步读取文件后编译执行
- .node文件 : 通过dlopen()方法加载最后编译生成的文件
- .json文件 : 同步读取后,用JSON.parse()解析并返回
- 其余扩展名文件都被当做是
.js
文件载入
下面仅介绍JavaScript模块的编译
根据CommonJS模块规范, 每个模块文件中都存在require
、exports
、module
这3个变量,另外, 在Node的API文档中,我们知道每个模块还有__filename\, __dirname 这两个变量, 它们从何而来?
实际上, 编译过程中, Node对JavaScript内容进行了头尾包装, 在头部添加(function (exports, require, module, __filename, __dirname) {\n, 尾部添加了\n}); 一个正常的JavaScript文件会被包装成如下的样子:
(function (exports, require, module, __filename, __dirname) { |
这样每个模块文件之间进行了作用域隔离, 包装之后的代码通过vm原生模块的runInThisContext()方法执行(类似于eval,只是具有明确的上下文,不会污染全局), 返回一个具体的function对象。 最后, 将当前模块对象的exports属性、require()方法, module(模块对象自身), 以及在文件定位中得到的完整的文件路径和文件目录作为参数传给这个function()执行。
执行后, 模块的exports属性被返回调用方, eports属性上的任何方法和属性都能被外部调用, 但是模块中的其余变量或属性不可直接被调用.
看看下面这行简单的语句,梳理一下模块导入的整个流程
var area = require('./area'); |
参考资料:
- 深入浅出Node.js, 朴灵 著
- Node.js开发指南, BYVoid 著