模块解析
相关代码大致位于 node/lib/internal/modules/cjs/loader.js
默认有 3 种 (v13 更新了第四种,还在实验),分别是 js,json,node。
> require('module')._extensions
[Object: null prototype] {
'.js': [Function (anonymous)],
'.json': [Function (anonymous)],
'.node': [Function (anonymous)]
}
Node 在没有解析到后缀时,默认使用 CommonJS 模式,而非 JSON ,也是这里只涉及到的这种解析模式。(一直被各种配置文件的常用格式误导了。。)
具体步骤大致如下:
-
查询对应文件的绝对路径
-
读取文件的类型
-
通过封装(套函数外壳)创建私有作用域。
-
通过 VM 读取,执行代码
-
完成加载后,对应绝对路径缓存代码
关键模块
Node 在模块解析上有两个关键全局模块,不需要加载来完成模块引用和解析的操作。
-
Require
-
Module
可以理解成这样, require 作为交互用的指令, module 作为全部引用模块的管理者。 依赖的引用就是通过两者的合作完成的。
加载顺序
-
优先使用内置模块,如果有同名的会优先使用内置模块(像
require和module)。 -
查询缓存
-
查询路径
-
查询文件
-
查询文件夹
-
-
查询
node_modules -
报错
第三部中的查询地址顺序可以通过打印 module 查看。 这是 Node
模块解析时查询 node_modules 的顺序和对应的路径。
// console.log(module)
Module {
id: '.',
path: '/Users/arius/Lib/node-test',
exports: {},
parent: null,
filename: '/Users/arius/Lib/node-test/modules.js',
loaded: false,
children: [],
paths: [
'/Users/arius/Lib/node-test/node_modules',
'/Users/arius/Lib/node_modules',
'/Users/arius/node_modules',
'/Users/node_modules',
'/node_modules'
]
}
文件夹处理
在对应路径找不到文件的时候,会在该路径查询文件夹。
Module._resolveFilename 会判断是否是相对路径。
如果是 . 开头,就是相对路径,这个时候查询文件或者目录 index ;
如果不是 相对路径,则会查询 node_modules ,路径和顺序如上。
只有这个时候会触发 package.json 检索。
加载的 package.json 以 {key: path, value: configJSON} , 的格式存到全局的 packageJSONCache 中。
查询的实现在 lib/internal/modules/esm/resolve.js::packageMainResolve 中
所以当文件不存在时,执行顺序如下:
-
查询缓存
-
判读是否是相对路径
-
如果是,查询路径下的
index -
如果否,遍历
paths查询package.json,并缓存-
如果有,先查询是否有
exports(2020-04 的更新) -
再查询是否有
main -
再查询该路径是否存在
index
-
-
-
报错
模块解析
实现在 lib/internal/modules/cjs/loader.js::Module._compile 。
当查询到后缀为 c?js 或者默认处理时,走js解析。
可以把每个js文件简单理解成一个函数。
因为这个原因,每次引用最外层的代码会被执行,但只输出对应的 module 。
Node解释器就是执行这个函数的工具,将对应的参数传给它,保证执行的作用域。
> node
Welcome to Node.js v14.9.0.
Type ".help" for more information.
> require('module').wrapper
Proxy [
[
'(function (exports, require, module, __filename, __dirname) { ',
'\n});'
],
{ set: [Function: set], defineProperty: [Function: defineProperty] }
]
>
其中
function(
exports, // 当前模块 exports 的引用,和 module.exports 是一个东西
require,// 一般就是 Module.prototype.require 即 _Module.prototype.require._load
module, // 当前模块, 也是 this
__filename, // 文件绝对路径
__dirname // 文件相对路径
) {}
通过前后增加函数执行的外壳,和 Module 统一管理输入的文件访问参数,
生成一个匿名函数,抛给 VM 执行这串代码。
由于node使用函数方式来执行整个文件,所以可以直接打印看到整个模块的相关信息。
// args.js
console.log(arguments)
// run it
node args.js
[Arguments] {
'0': {},
'1': [Function: require] {
resolve: [Function: resolve] { paths: [Function: paths] },
main: Module {
id: '.',
path: '/Users/arius/Lib/node-test/require-test',
exports: {},
parent: null,
filename: '/Users/arius/Lib/node-test/require-test/args.js',
loaded: false,
children: [],
paths: [Array]
},
extensions: [Object: null prototype] {
'.js': [Function (anonymous)],
'.json': [Function (anonymous)],
'.node': [Function (anonymous)]
},
cache: [Object: null prototype] {
'/Users/arius/Lib/node-test/require-test/args.js': [Module]
}
},
'2': Module {
id: '.',
path: '/Users/arius/Lib/node-test/require-test',
exports: {},
parent: null,
filename: '/Users/arius/Lib/node-test/require-test/args.js',
loaded: false,
children: [],
paths: [
'/Users/arius/Lib/node-test/require-test/node_modules',
'/Users/arius/Lib/node-test/node_modules',
'/Users/arius/Lib/node_modules',
'/Users/arius/node_modules',
'/Users/node_modules',
'/node_modules'
]
},
'3': '/Users/arius/Lib/node-test/require-test/args.js',
'4': '/Users/arius/Lib/node-test/require-test'
}
因为是引用的关系 module.exports 一般和 exports 指向同一个东西,
但需要注意,执行 require 导出永远的 module 是不会变的, exports 只是引用。
这也是为啥 exports=xxx 不会生效的原因。
module.exports = {
a: '123', // 最终依然是 123
}
exports.a = '321';
依赖循环
而当 require 一个包或者文件的时候,执行(或者说依赖)的并不直接是这个包。
对应整个对象的 module, 会被缓存到全局对象 Module._cache[filename] 中。
假如执行 a.js 。
// a.js
const b = require('./b')
console.log(b) // 'b'
module.exports = 'a'
// b.js
const a = require('./a')
console.log(a) // {}, Module._cache中 a依然是 空的 Module {} 对象
module.exports = 'b'
依赖处理中, CJS 和 ES 运行顺序是一致的 (其中 default , 子模块都为 undefined ),不同的是 ES 可以动态替换引用。
只要引用是在加载完成之后被调用, 就不会有问题。
所以需要注意需要最外层执行的函数的顺序。
// a.js
import { b } from '../';
console.log(b)
export const a = 'a'
// b.js
import { a } from '../';
console.log(a) // undefined
export const callA = () => {
console.log(a) // 'a'
}
export const b = 'b'
// index
export * from './a';
export * from './b';
如果是 mjs 的话,遇到循环依赖会报错,可能还没支持或者是新的规范。。
ReferenceError: Cannot access 'a' before initialization
那么 mjs 是啥?
ECMAScript 模块
官方实验 ES6 规范,以 mjs 后缀结尾。
cjs 和 esm 的主要的区别在于, require + module.exports 对应 import 和 export 。
// 使用的时候会有一个 实验的 warning
(node:55066) ExperimentalWarning: The ESM module loader is experimental.
// 如果直接 import 调用对应不是 mjs 的模块
(node:54924) Warning: To load an ES module, set "type": "module" in the package.json or use the .mjs extension.
Node 要求 esm 模块都使用 .mjs 后缀,且 默认启动严格模式 。
或者在 package.json 中制定为 type 为 module ,这样执行目录内 js 脚本都会解析成 ES6 模块。这个解析仅限于 js 后缀, 其他(非 js )使用 cjs 或者 mjs 后缀文件会使用对应的解析方式。
与 Babel 时混用处理的方式不同, require 不能加载 mjs , mjs 中不能使用 require 。
相互调用
cjs 调用 mjs 时,需要用异步。
异步调用可能是想继承 esm 动态引用的机制,但是目前还不允许循环依赖。
而 require 不支持 ES6 模块的一个原因是,它是同步加载,而 ES6 模块内部可以使用顶层 await 命令,导致无法被同步加载。
(async () => {
await import('./my-app.mjs');
})();
而 mjs 调用 cjs 的时候只能整体调用,无法解析,作 shaking 啥的。
支持多个模式
如果是 esm 模式则需要给一个整体的接口 export default 。
如果是 CommonJS 模式则需要封装一层,拆分一个单独的文件。
import cjsModule from '../index.cjs';
export const {...} = cjsModule;
或者使用 package.json 的方式修改, 其中 exports 除了相对路径替换外,还可以挑选调用方式。
具体如下:
// ./index.js
require('find-me');// Found you
// ./index.mjs
import 'find-me'; // Found me!
// ./node_modules/find-me/package.json
{
"exports": {
"import": "./me.mjs"
"require": "./you.js"
}
}
// ./node_modules/find-me/you.js
console.log('Found you')
// ./node_modules/find-me/me.mjs
console.log('Found me!')
FAQ
1. Node 中的错误行数问题
Node 本身并没有对错误函数做处理,一切错误处理都来自 VM,历史上 Node 上报堆栈失败的原因是那时候 Node 4 没有配置 displayErrors 参数。
wrapper 本身第一行没有换行,而且代码没有压缩,直接拼接,所以不会有行数问题。