前言
CommonJS 的模块规范指出模块主要分为三部分: 模块引用, 模块定义, 模块标识
模块引用
模块引用的示例代码如下:
const math = require('math')
在 CommonJS 规范中, 存在 require() 方法, 这个方法接受的参数为模块标识, 以此引入一个模块的 API 到当前的上下文中.
模块定义
在模块中, 上下文提供 require() 方法来引入外部模块. 对应引入共能, 上下文还提供了 exports 对象用于导出当前模块的方法或者变量. 模块中还有一 module 对象, 代表模块自身, exports 是 module 对象的属性. 在 Node 中, 一个文件就是一个模块, 将方法挂载在 exports 对象上作为属性即可定义导出的方式:
- // math
- exports.add = function add(...args) {
- let sum = 0
- args.forEach(i => {
- sum += i
- })
- return sum
- }
在另外一个文件中, 通过 require() 方法引入该模块, 就可以调用其方法:
- // program.JS
- const math = require('math')
- exports.increment = function(val) {
- return math.add(val, 1)
- }
模块标识
模块标识其实就是传递给 require() 方法的参数, 它必须是符合小驼峰命名的字符串, 或者以. , .. 开头的相对路径, 或者绝对路径. 它可以没有文件后缀 .JS .
Node 的模块加载过程
在 Node 中引入模块, 需要经历 3 个步骤:
路径分析
文件定位
编译执行
在 Node 中模块分为两类: 一类是 Node 提供的模块, 称为核心模块; 另一类是用户编写的, 称为文件模块.
优先从缓存中加载
Node 会对引用过的模块进行缓存, 以减少再次引用时的开销. Node 缓存的是编译和执行之后的对象.
不论是核心模块还是文件模块, require() 方法对相同模块的二次加载一律采用缓存优先的策略, 是 第一优先级的 . 不同之处在于核心模块的缓存检查要先于文件模块的缓存检查.
路径分析和文件定位
对于不同的标识符, 模块的查找和定位会有不同程度上的差异.
模块标识符分析
模块标识符分为如下几类:
核心模块, 如 http , fs , path 等;
. 或者 .. 开始的相对路径的文件模块;
以 / 开始的绝对路径的文件模块;
非路径形式的文件模块, 如 express 模块.
核心模块
核心模块的优先级仅次于缓存加载, 加载速度最快, 如果想要加载一个和核心模块同名的自定义模块, 不会成功
路径形式的文件模块
以 . , .. 开头的标识符, 在分析路径是会将其转换为绝对路径并将绝对路径作为索引, 将编译执行后的结果存在缓存中.
文件模块给出了文件所在的确切位置, 查找可以节约大量时间, 加载速度慢于核心模块.
自定义模块
自定义模块指的是非核心模块, 也不是路径形式的标识符. 它是特殊的一种文件模块, 可能是一个文件或者包的形式, 这类模块查找最费时间, 所以加载最慢.
模块路径 是 Node 在定位文件模块的具体文件时制定的查找策略, 具体表现是一个路径的数组, 这个路径的生成规则, 可以动手尝试一番:
创建一个 path_test.JS 的文件, 其内容为 console.log(module.paths)
将其放在任意一个目录中并通过 node 执行 node path_test.JS .
可能会得到如下输出:
- [ 'E:\\workspace\\myBlog\\node_modules',
- 'E:\\workspace\\node_modules',
- 'E:\\node_modules' ]
可以看出, 模块路径的生成规则如下:
- node_modules
- node_modules
- node_modules
- node_modules
可以看出, 当前文件的路径越深, 模块查找的耗时越多, 这是自定义模块加载速度最慢的原因.
文件定位
文件定位还包括一些细节: 文件拓展名的分析, 目录和包的处理.
文件拓展名的分析
因为模块的标识符也就是 require() 方法的参数是可以不含有文件拓展名的, 这种情况下, Node 会按照 .JS , .JSON , .node 的次序补足拓展名, 依次尝试.
目录分析和包
通过分析文件拓展名之后可能没有得到一个文件, 但是得到一个目录此时 Node 会将目录当作一个包处理.
Node 会在当前目录下查找 package.JSON 文件, 通过 JSON.parse() 解析出包描述对象, 取出 main 属性来对文件定位. 如果 main 指向的文件没有, 或者没有 package.JSON 文件, 则依次查找 index.JS , index.JSON , index.node .
目录分析的过程中如果没有成功定位任何文件, 则进入下一个模块路径进行定位. 所有的模块路径遍历完还没找到则抛出查找失败的异常.
模块编译
注: 这里提到的模块编译都是指文件模块
在 Node 中, 每个文件模块都是一个 Module 对象, 可以写一个测试文件 console.log(module), 并运行得到如下结果:
- (function (exports, require, module, __filename, __dirname) {
- // JavaScript 文件的内容
- })
一个正常的 JavaScript 文件可能会被包装成这样:
- (function (exports, require, module, __filename, __dirname) {
- var math = require('math')
- exports.area= function (radius) {
- return Math.PI * radius * radius
- }
- })
具体编译的方式视文件的拓展名而定
JavaScript 模块的编译
每个模块文件中存在着 require , exports , module 这三个变量, 通过阅读 Node 的文档, 还有 __filename , __dirname 这两个变量, 他们从何而来?
实际上, 在编译的过程中, Node 对获取的 JavaScript 文件的内容进行了包装. 将文件包裹在
- (function (exports, require, module, __filename, __dirname) {
- // JavaScript 文件的内容
- })
一个正常的 JavaScript 文件可能会被包装成这样:
- (function (exports, require, module, __filename, __dirname) {
- var math = require('math')
- exports.area= function (radius) {
- return Math.PI * radius * radius
- }
- })
这样每个模块文件之间都进行了作用域的隔离, 在执行之后, 模块的 exports 属性返回给了调用方, 模块的 exports 属性上的方法以及属性都可以被外部调用的到, 其他变量方法不可直接被调用.
另外不可直接对 exports 赋值, 原因在于, exports 对象是通过形参的方式传入的, 直接赋值形参会改变形参的引用, 但不能改变作用域外的值. 解决方法是赋值给 module.exports 对象
C/C++ 模块的编译
Node 调用 process.dlopen() 方法进行加载和执行, 执行的过程中, 模块的 exports 对象与. node 模块产生联系, 然后返回给调用者.
JSON 文件的编译
Node 利用 fs 模块同步读取 JSON 文件的内容, 并将内容通过 JSON.parse() 得到对象赋给 exports 对象, 供外部调用.
来源: http://www.qdfuns.com/article/51116/2153072053f866fddda6bcceb71bfff8.html