CommonJS 规范
Node 的发展使得 npm
模块库的内容日益丰富,这些 npm
的生成也遵循着 CommonJs
的规范。如今对于前端开发者已经离不开 npm
,所以 CommonJs
的规范及 require()
加载机制对于开发者来说也就极其重要。
CommonJs
对于模块的定义分为 模块引用、模块定义、模块标识 三个部分
模块引用
const sum = require('sum')
模块定义
在 CommonJs
规范中,使用 require
方法来引入模块。在模块中的上下文提供了一个 exports
对象和 module.exports
对象用于导出当前模块的变量或方法。
module.exports 导出模块:
module.exports = {
a: 2
}
exports 导出模块:
exports.a = 2
关于 exports、module.exports 的区别:require
方法加载到的只有 module.exports
这个对象,而我们在编写模块时用到的 exports
对象实际上只是对module.exports
的引用
模块标识
模块标识就是 require()
的时候传入的参数。
标识符代表着包名,也可以是以 .
、 ..
开头的相对路径或者绝对路径。
模块实现
在 Node 中模块分为两类:
- 一类是 Node 内置模块,又称为核心模块(
http
、fs
等) - 一类是用户自定义模块,又称为文件模块
当我们在当前上下文中引入模块时,需要经历以下四个步骤:
- 缓存加载
- 路径分析
- 文件定位
- 文件编译执行
模块缓存加载
不论是核心模块还是文件模块,Node 对引入过的模块都会进行缓存,以减少二次引入时的开销。Node 缓存的是编译过的对象。缓存加载是第一优先级,核心模块的缓存加载会优先于文件模块的缓存。
路径分析
const sum = require('sum')
const sum = require('../sum')
const sum = require('/sum')
require方法接受一个模块标识符作为参数,sum
、 ../sum
、/sum
即为模块标识符。Node 正是基于这样的标识符进行模块查找。
标识符分类:
- 核心模块,
http
,fs
,path
等 - 以
.
或..
开头的相对路径模块 - 以
/
开头的绝对路径模块 - 自定义模块,比如众多 npm 包
核心模块的加载:
核心模块的加载仅此于缓存加载,在 Node 进程启动时,核心模块就被编译为二进制代码加载进内存中。
PS: 如果想加载一个标识符与核心模块相同的模块,我们必须换一个标识符或者换用路径的方式加载。
这里测试一下, 新建 test.js
和 http.js
在同一目录下:
新建一个 test.js 文件:
const a = require('http')
console.log(a)
新建一个 http.js 文件
module.exports = {
a : 1
}
打印如下:
由此可见,打印内容为 http
模块内容。如果我们试图加载一个与核心模块标识符相同的模块,读取的还是核心模块。那如何加载与核心模块相同标识符的模块,可以采用相对路径的方式或修改标识符。
修改一下以相对路径方式引入:
const a = require('./http')
console.log(a)
效果如下:
路径形式的模块
以 .
和 ..
开头的标识符,在引入时会将相对路径转换为绝对路径,并以绝对路径为索引将文件编译执行后的对象添加到缓存,在二级加载时更快。
文件模块的加载指明了文件的位置,所以在查找的过程中很快,加载速度慢于核心模块。
自定义模块
自定义模块通常是指开发者自行封装的模块,比如丰富的 npm 社区,Node 在加载自定义模块的时候会遵循一套路径规则,该规则表现为一套路径数组。
关于数组的详细规则,我们可以自行测试:
创建一个测试文件,find_path.js
console.log(module.paths)
Mac 系统下,输出路径数组如下
[
'/Users/zjg/study/demo/node_modules',
'/Users/zjg/study/node_modules',
'/Users/zjg/node_modules',
'/Users/node_modules',
'/node_modules'
]
可以看出,模块路径查找的规则是这样:
- 查找当前文件目录下的
node_modules
目录 - 查找父目录下的
node_modules
目录 - 父目录的父目录下的
node_modules
目录 - ...
- 根目录下的
node_modules
目录
这种查询目录的方式和 JavaScript
的原型链或作用域的查询方式很类似。
文件定位
当我们使用 require()
的时候,如果标识符中不包含文件扩展名,比如
require('mytest')
这种情况下,Node 会按照 .js
、.json
、.node
的次序补充扩展名尝试加载模块。
如果 mytest 是一个 npm
包文件,Node 通过补充文件扩展名没有找到对应文件,找到的是一个目录,这时 Node 会把这个目录当作一个包来处理。
Node 对 CommpnJs
包规范做了一定支持,首先,Node 会在当前包目录下找到 packsge.json
文件,通过 JSON.parse()
解析出包描述对象,找到 main
属性指定的入口文件位置。如果没有文件扩展名,则先进行扩展名分析。如果 main
属性指定的文件不存在,或者当前包目录没有 package.josn
文件, Node 则会尝试查找 index
文件名,一次查找 index.js
、 index.josn
、 inndex.node
。
如果当前目录分析没有找模块,Node 则按照模块路径数组下一个目录开始查找,依次查找完成,如果最终没有找到模块,则会抛出错误,以上便是 Node 加载一个模块时所做的全部处理。