内容简介:关注「Node.js 中,模块加载过程分为 5 步:
关注「 前端向后 」微信公众号,你将收获一系列「用 心 原创」的高质量技术文章,主题包括但不限于前端、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 )
以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网
猜你喜欢:- Python模块及详解(2)
- APP「登录注册模块」详解
- Burpsuite Collaborato模块详解
- nginx事件模块结构体详解
- Python的模块及详解(1)
- Python模块文件结构代码详解
本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们。
程序员的职业素养
Robert C.Martin / 章显洲、余晟 / 人民邮电出版社 / 2012-9-1 / 49.00元
本书是编程大师Bob 大叔40 余年编程生涯的心得体会, 讲解成为真正专业的程序员需要什么样的态度、原则,需要采取什么样的行动。作者以自己以及身边的同事走过的弯路、犯过的错误为例,意在为后来人引路,助其职业生涯迈上更高台阶。 本书适合所有程序员,也可供所有想成为具备职业素养的职场人士参考。一起来看看 《程序员的职业素养》 这本书的介绍吧!