详解Node模块加载机制
source link: http://mp.weixin.qq.com/s?__biz=MzIwMTM5MTM1NA%3D%3D&%3Bmid=2649473537&%3Bidx=1&%3Bsn=7605d0c02400afe7f18d0ddbcd1ec3c9
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
关注「 前端向后 」微信公众号,你将收获一系列「用 心 原创」的高质量技术文章,主题包括但不限于前端、Node.js以及服务端技术
一.require()时发生了什么?
Node.js 中,模块加载过程分为 5 步:
-
路径解析(Resolution):根据模块标识找出对应模块(入口)文件的绝对路径
-
加载(Loading):如果是 JSON 或 JS 文件,就把文件内容读入内存。如果是内置的原生模块,将其共享库动态链接到当前 Node.js 进程
-
包装(Wrapping):将文件内容(JS 代码)包进一个函数,建立模块作用域,
exports, require, module
等作为参数注入 -
执行(Evaluation):传入参数,执行包装得到的函数
-
缓存(Caching):函数执行完毕后,将
module
缓存起来,并把module.exports
作为require()
的返回值返回
其中,模块标识(Module Identifiers)就是传入 require(id)
的第一个字符串参数 id
,例如 require('./myModule')
中的 './myModule'
, 无需指定后缀名 (但带上也无碍)
对于 .
、 ..
、 /
开头的文件路径,尝试当做文件、目录来匹配,具体过程如下:
-
若路径存在并且是个文件,就当做 JS 代码来加载(无论文件后缀名是什么,
require(./myModule.abcd)
完全正确) -
若不存在,依次尝试拼上
.js
、.json
、.node
(Node.js 支持的二进制扩展)后缀名 -
如果路径存在并且是个文件夹,就在该目录下找
package.json
,取其main
字段,并加载指定的模块(相当于一次重定向) -
如果没有
package.json
,就依次尝试index.js
、index.json
、index.node
对于模块标识不是文件路径的,先看是不是 Node.js 原生模块( fs
、 path
等)。如果不是,就从当前目录开始,逐级向上在各个 node_modules
下找,一直找到顶层的 /node_modules
,以及一些全局目录:
-
NODE_PATH
环境变量中指定的位置 -
默认的全局目录:
$HOME/.node_modules
、$HOME/.node_libraries
和$PREFIX/lib/node
P.S.关于全局目录的更多信息,见Loading from the global folders
找到模块文件后,读取内容,并包一层函数:
(function(exports, require, module, __filename, __dirname) { // Module code actually lives in here });
(摘自The module wrapper)
执行时从外部注入这些模块变量( exports, require, module, __filename, __dirname
),模块导出的东西通过 module.exports
带出来,并将整个 module
对象缓存起来,最后返回 require()
结果
循环依赖
特殊的,模块之间可能会出现循环依赖,对此,Node.js 的处理策略非常简单:
// module1.js exports.a = 1; require('./module2'); exports.b = 2; exports.c = 3; // module2.js const module1 = require('./module1'); console.log('module1 is partially loaded here', module1);
module1.js
执行中引用了 module2.js
, module2
又引了 module1
,此时 module1
尚未加载完( exports.b = 2; exports.c = 3;
还没执行)。而 在 Node.js 里,只加载了一部分的模块也可以正常引用 :
When there are circular require() calls, a module might not have finished executing when it is returned.
所以 module1.js
执行结果是:
module1 is partially loaded here { a: 1 }
P.S.关于循环引用的更多信息,见Cycles
二.Node.js 内部是怎么实现的?
实现上,模块加载的绝大多数工作都是由 module
模块来完成的:
const Module = require('module'); console.log(Module);
Module
是个函数/类:
function Module(id = '', parent) { this.id = id; this.path = path.dirname(id); // 即module.exports this.exports = {}; this.parent = parent; updateChildren(parent, this, false); this.filename = null; this.loaded = false; this.children = []; }
每加载一个模块都创建一个 Module
实例,模块文件执行完后,该实例仍然保留,模块导出的东西依附于 Module
实例存在
模块加载的所有工作都是由 module
原生模块来完成的,包括 Module._load
、 Module.prototype._compile
Module._load
Module._load()
负责加载新模块、管理缓存,具体如下:
Module._load = function(request, parent, isMain) { // 0.解析模块路径 const filename = Module._resolveFilename(request, parent, isMain); // 1.优先找缓存 Module._cache const cachedModule = Module._cache[filename]; // 2.尝试匹配原生模块 const mod = loadNativeModule(filename, request, experimentalModules); // 3.未命中缓存,也没匹配到原生模块,就创建一个新的 Module 实例 const module = new Module(filename, parent); // 4.把新实例缓存起来 Module._cache[filename] = module; // 5.加载模块 module.load(filename); // 6.如果加载/执行出错了,就删掉缓存 if (threw) { delete Module._cache[filename]; } // 7.返回 module.exports return module.exports; }; Module.prototype.load = function(filename) { // 0.判定模块类型 const extension = findLongestRegisteredExtension(filename); // 1.按类型加载模块内容 Module._extensions[extension](this, filename); };
支持的类型有 .js
、 .json
、 .node
3 种:
// Native extension for .js Module._extensions['.js'] = function(module, filename) { // 1.读取JS文件内容 const content = fs.readFileSync(filename, 'utf8'); // 2.包装、执行 module._compile(content, filename); }; // Native extension for .json Module._extensions['.json'] = function(module, filename) { // 1.读取JSON文件内容 const content = fs.readFileSync(filename, 'utf8'); // 2.直接JSON.parse()完事 module.exports = JSONParse(stripBOM(content)); }; // Native extension for .node Module._extensions['.node'] = function(module, filename) { // 动态加载共享库 return process.dlopen(module, path.toNamespacedPath(filename)); };
P.S. process.dlopen
具体见process.dlopen(module, filename[, flags])
Module.prototype._compile
Module.prototype._compile = function(content, filename) { // 1.包一层函数 const compiledWrapper = wrapSafe(filename, content, this); // 2.把要注入的参数准备好 const dirname = path.dirname(filename); const require = makeRequireFunction(this, redirects); const exports = this.exports; const thisValue = exports; const module = this; // 3.注入参数、执行 compiledWrapper.call(thisValue, exports, require, module, filename, dirname); };
包装部分的实现如下:
function wrapSafe(filename, content, cjsModuleInstance) { let compiled = compileFunction( content, filename, 0, 0, undefined, false, undefined, [], [ 'exports', 'require', 'module', '__filename', '__dirname', ] ); return compiled.function; }
P.S.模块加载的完整实现见node/lib/internal/modules/cjs/loader.js
三.知道这些有什么用?
知道了模块的加载机制,在一些需要 扩展 篡改加载逻辑的场景很有用,比如用来实现虚拟模块、模块别名等
虚拟模块
比如,VS Code 插件通过 require('vscode')
来访问插件 API:
// The module 'vscode' contains the VS Code extensibility API import * as vscode from 'vscode';
而 vscode
模块实际上是不存在的,是个运行时扩展出来的虚拟模块:
// ref: src/vs/workbench/api/node/extHost.api.impl.ts function defineAPI() { const node_module = <any>require.__$__nodeRequire('module'); const original = node_module._load; // 1.劫持 Module._load node_module._load = function load(request, parent, isMain) { if (request !== 'vscode') { return original.apply(this, arguments); } // 2.注入虚拟模块 vscode // get extension id from filename and api for extension const ext = extensionPaths.findSubstr(parent.filename); let apiImpl = extApiImpl.get(ext.id); if (!apiImpl) { apiImpl = factory(ext); extApiImpl.set(ext.id, apiImpl); } return apiImpl; }; }
具体见 API 注入机制及插件启动流程_VSCode 插件开发笔记 2 ,这里不再赘述
模块别名
类似的,可以通过重写 Module._resolveFilename
来实现模块别名,比如把 proj/src
中的 @lib/my-module
模块引用映射到 proj/lib/my-module
:
// src/index.js require('./patchModule'); const myModule = require('@lib/my-module'); console.log(myModule);
patchModule
具体实现如下:
const Module = require('module'); const path = require('path'); const _resolveFilename = Module._resolveFilename; Module._resolveFilename = function(request) { const args = Array.from(arguments); // 别名映射 const LIB_PREFIX = '@lib/'; if (request.startsWith(LIB_PREFIX)) { console.log(request); request = path.resolve(__dirname, '../' + request.slice(1)); args[0] = request; console.log(` => ${request}`); } return _resolveFilename.apply(null, args); }
P.S.当然,一般不需要这样做,可以通过Webpack等构建工具来完成
清掉缓存
默认 Node.js 模块加载过就有缓存,而有些时候可能想要禁掉缓存,强制重新加载一个模块,比如想要读取能被用户频繁修改的 JS 文件(如 webpack.config.js
)
此时可以手动删掉挂在 require.cache
身上的 module.exports
缓存:
delete require.cache[require.resolve('./b.js')]
然而,如果 b.js
还引用了其它外部(非原生)模块, 也需要一并删除 :
const mod = require.cache[require.resolve('./b.js')]; // 把引用树上所有模块缓存全都删掉 (function traverse(mod) { mod.children.forEach((child) => { traverse(child); }); console.log('decache ' + mod.id); delete require.cache[mod.id]; }(mod));
P.S.或者采用decache模块
参考资料
-
Node.js, TC-39, and Modules:以及译文
-
The Node.js Way – How
require()
Actually Works -
Requiring modules in Node.js: Everything you need to know
-
Deep Dive Into Node.js Module Architecture
-
node.js require() cache – possible to invalidate?
联系我
如果心中仍有疑问,请查看原文并留下评论噢。( 特别要紧的问题,可以直接微信联系 ayqywx )
Recommend
About Joyk
Aggregate valuable and interesting links.
Joyk means Joy of geeK