读 Node.js 源码深入理解 cjs 模块系统
作 者 | 周飞宇(牟牟)
来 源 | 大淘宝技术团队
本文将对 Node.js 源码进行探索,深入理解 cjs 模块的加载过程Huobi Global。
相信大家都知道如何在 Node.js 中加载一个模块:
constfs = require( 'fs');
constexpress = require( 'express');
constanotherModule = require( './another-module');
没错,require 就是加载 cjs 模块的 API,但 V8 本身是没有 cjs 模块系统的,所以 node 是怎么通过 require找到模块并且加载的呢?我们今天将对 Node.js 源码进行探索,深入理解 cjs 模块的加载过程Huobi Global。
Huobi Global我们阅读的 node 代码版本为 v17.x:
内置模块
为了知道 require 的工作逻辑,我们需要先了解内置模块是如何被加载到 node 中的(诸如 'fs','path','child_process',也包括无法被用户引用的内部模块),准备好代码之后,我们首先要从 node 启动开始阅读Huobi Global。
node 的 main 函数在 src/node_main.cc内Huobi Global,通过调用 API node::Start来启动一个 node 实例:
int Start(int argc, char** argv) {
InitializationResult result = InitializeOncePerProcess(argc, argv);
展开全文
if(result.early_return) {
returnresult.exit_code;
Isolate::CreateParams params;
conststd::vector<size_t>* indices = nullptr;
constEnvSerializeInfo* env_info = nullptr;
bool use_node_snapshot =
per_process::cli_options->per_isolate->node_snapshot;
if(use_node_snapshot) {
v8::StartupData* blob = NodeMainInstance::GetEmbeddedSnapshotBlob;
if(blob != nullptr) {
params.snapshot_blob = blob;
indices = NodeMainInstance::GetIsolateDataIndices;
env_info = NodeMainInstance::GetEnvSerializeInfo;
uv_loop_configure(uv_default_loop, UV_METRICS_IDLE_TIME);
NodeMainInstance main_instance(¶ms,
uv_default_loop,
per_process::v8_platform.Platform,
result.args,
result.exec_args,
indices);
result.exit_code = main_instance.Run(env_info);
TearDownOncePerProcess;
returnresult.exit_code;
这里创建了事件循环Huobi Global,且创建了一个 NodeMainInstance 的实例 main_instance 并调用了它的 Run方法:
intNodeMainInstance::Run( constEnvSerializeInfo* env_info) {
Locker locker(isolate_);
Isolate:: Scope isolate_scope(isolate_);
HandleScope handle_scope(isolate_);
intexit_code = 0;
DeleteFnPtr<Environment, FreeEnvironment> env =
CreateMainEnvironment(&exit_code, env_info);
CHECK_NOT_NULL(env);
Context:: Scope context_scope(env->context) ;
Run(&exit_code, env.get);
returnexit_code;
Run 方法中调用 CreateMainEnvironment来创建并初始化环境:
Environment* CreateEnvironment(
IsolateData* isolate_data,
Local<Context> context,
conststd:: vector< std:: string>& args,
conststd:: vector< std:: string>& exec_args,
EnvironmentFlags::Flags flags,
ThreadId thread_id,
std:: unique_ptr<InspectorParentHandle> inspector_parent_handle) {
Isolate* isolate = context->GetIsolate;
HandleScope handle_scope(isolate);
Context:: Scope context_scope(context);
// TODO(addaleax): This is a much better place for parsing per-Environment
// options than the global parse call.
Environment* env = newEnvironment(
isolate_data, context, args, exec_args, nullptr, flags, thread_id);
# ifHAVE_INSPECTOR
if(inspector_parent_handle) {
env->InitializeInspector(
std::move( static_cast<InspectorParentHandleImpl*>(
inspector_parent_handle.get)->impl));
} else{
env->InitializeInspector({});
# endif
if(env->RunBootstrapping.IsEmpty) {
FreeEnvironment(env);
returnnullptr;
returnenv;
通过创建 Environment 对象 env 并调用其 RunBootstrapping方法:
MaybeLocal<Value> Environment::RunBootstrapping {
EscapableHandleScope scope(isolate_);
CHECK(!has_run_bootstrapping_code);
if(BootstrapInternalLoaders.IsEmpty) {
returnMaybeLocal<Value>;
Local<Value> result;
if(!BootstrapNode.ToLocal(&result)) {
returnMaybeLocal<Value>;
// Make sure that no request or handle is created during bootstrap -
// if necessary those should be done in pre-execution.
// Usually, doing so would trigger the checks present in the ReqWrap and
// HandleWrap classes, so this is only a consistency check.
CHECK(req_wrap_queue->IsEmpty);
CHECK(handle_wrap_queue->IsEmpty);
DoneBootstrapping;
returnscope.Escape(result);
这里的 BootstrapInternalLoaders实现Huobi Global了 node 模块加载过程中非常重要的一步:
通过包装并执行 internal/bootstrap/loaders.js获取内置模块的 nativeModulerequire函数用于加载内置的 js 模块,获取 internalBinding用于加载内置的 C++ 模块,NativeModule则是专门用于内置模块的小型模块系统Huobi Global。
functionnativeModuleRequire( id) {
if(id === loaderId) {
returnloaderExports;
constmod = NativeModule.map.get(id);
// Can't load the internal errors module from here, have to use a raw error.
// eslint-disable-next-line no-restricted-syntax
if(!mod) thrownewTypeError( `Missing internal module ' ${id}'` );
returnmod.compileForInternalLoader;
constloaderExports = {
internalBinding,
NativeModule,
require: nativeModuleRequire
returnloaderExports;
需要注意的是,这个 require 函数只会被用于内置模块的加载,用户模块的加载并不会用到它Huobi Global。(这也是为什么我们通过打印 require('module')._cache 可以看到所有用户模块,却看不到 fs 等内置模块的原因,因为两者的加载和缓存维护方式并不一样)。
用户模块
接下来让Huobi Global我们把目光移回到 NodeMainInstance::Run函数:
intNodeMainInstance::Run( constEnvSerializeInfo* env_info) {
Locker locker(isolate_);
Isolate:: Scope isolate_scope(isolate_);
HandleScope handle_scope(isolate_);
intexit_code = 0;
DeleteFnPtr<Environment, FreeEnvironment> env =
CreateMainEnvironment(&exit_code, env_info);
CHECK_NOT_NULL(env);
Context:: Scope context_scope(env->context) ;
Run(&exit_code, env.get);
returnexit_code;
我们已经通过 CreateMainEnvironment 函数创建好了一个 env 对象,这个 Environment 实例已经有了一个模块系统 NativeModule 用于维护内置模块Huobi Global。
然后代码会运行到 Run 函数的另一个重载版本:
voidNodeMainInstance::Run( int* exit_code, Environment* env) {
if(*exit_code == 0) {
LoadEnvironment(env, StartExecutionCallback{});
*exit_code = SpinEventLoop(env).FromMaybe( 1);
ResetStdio;
// TODO(addaleax): Neither NODE_SHARED_MODE nor HAVE_INSPECTOR really
// make sense here.
# ifHAVE_INSPECTOR && defined(__POSIX__) && !defined(NODE_SHARED_MODE)
structsigactionact;
memset(&act, 0, sizeof(act));
for( unsignednr = 1; nr < kMaxSignal; nr += 1) {
if(nr == SIGKILL || nr == SIGSTOP || nr == SIGPROF)
continue;
act.sa_handler = (nr == SIGPIPE) ? SIG_IGN : SIG_DFL;
CHECK_EQ( 0, sigaction(nr, &act, nullptr));
# endif
# ifdefined(LEAK_SANITIZER)
__lsan_do_leak_check;
# endif
在这里调用 LoadEnvironment:
MaybeLocal<Value> LoadEnvironment(
Environment* env,
StartExecutionCallback cb) {
env->InitializeLibuv;
env->InitializeDiagnostics;
returnStartExecution(env, cb);
然后执行 StartExecution:
MaybeLocal<Value> StartExecution( Environment* env, StartExecutionCallback cb) {
// 已省略其他运行方式Huobi Global,我们只看 `node index.js` 这种情况,不影响我们理解模块系统
if(!first_argv.empty && first_argv != "-") {
returnStartExecution(env, "internal/main/run_main_module");
在 StartExecution(env, "internal/main/run_main_module")这个调用中Huobi Global,我们会包装一个 function,并传入刚刚从 loaders 中导出的 require 函数,并运行 lib/internal/main/run_main_module.js内的代码:
'use strict';
const {
prepareMainThreadExecution
} = require( 'internal/bootstrap/pre_execution');
prepareMainThreadExecution( true);
markBootstrapComplete;
//Note: thisloads the modulethrough the ESM loader ifthe moduleis
//determined to be an ES module. This hangs fromthe CJS moduleloader
//because we currently allow monkey-patching ofthe moduleloaders
//inthe preloaded s through require( 'module').
//runMain here might be monkey-patched byusers in-- require.
//XXX: the monkey-patchability here should probably be deprecated.
require( 'internal/modules/cjs/loader').Module.runMain(process.argv[ 1]);
所谓的包装 function 并传入 requireHuobi Global,伪代码如下:
( function( require, /* 其Huobi Global他入参 */) {
// 这里是 internal/main/run_main_module.js 的文件内容
所以这里是通过内置模块的 require 函数加载了 lib/internal/modules/cjs/loader.js导出的 Module 对象上的 `runMain` 方法Huobi Global,不过我们在 loader.js 中并没有发现 runMain 函数,其实这个函数是在 lib/internal/bootstrap/pre_execution.js中被定义到 Module 对象上的:
lib/internal/modules/cjs/loader.js地址:
lib/internal/bootstrap/pre_execution.js地址:
lib/internal/modules/cjs/loader.js地址:
lib/internal/bootstrap/pre_execution.js地址:
functioninitializeCJSLoader{
constCJSLoader = require( 'internal/modules/cjs/loader');
if(!noGlobalSearchPaths) {
CJSLoader.Module._initPaths;
// TODO(joyeecheung): deprecate this in favor of a proper hook?
CJSLoader.Module.runMain =
require( 'internal/modules/run_main').executeUserEntryPoint;
在 lib/internal/modules/run_main.js中找到 executeUserEntryPoint 方法:
lib/internal/modules/run_main.js地址:
lib/internal/modules/run_main.js地址:
functionexecuteUserEntryPoint( main = process.argv[ 1] ) {
constresolvedMain = resolveMainPath(main);
constuseESMLoader = shouldUseESMLoader(resolvedMain);
if(useESMLoader) {
runMainESM(resolvedMain || main);
} else{
// Module._load is the monkey-patchable CJS module loader.
Module._load(main, null, true);
参数 main 即为我们传入的入口文件 index.jsHuobi Global。可以看到,index.js 作为一个 cjs 模块应该被 Module._load 加载,那么 _load干了些什么呢?这个函数是 cjs 模块加载过程中最重要的一个函数,值得仔细阅读:
// `_load` 函数检查请求文件的缓存
// 1. 如果模块已经存在Huobi Global,返回已缓存的 exports 对象
// 2. 如果模块是内置模块Huobi Global,通过调用 `NativeModule.prototype.compileForPublicLoader`
// 获取内置模块的 exports 对象Huobi Global,compileForPublicLoader 函数是有白名单的,只能获取公开
// 内置模块的 exportsHuobi Global。
// 3. 以上两者皆为否,创建新的 Module 对象并保存到缓存中,然后通过它加载文件并返回其 exportsHuobi Global。
// request:请求的模块Huobi Global,比如 `fs`,`./another-module`,'@pipcook/core' 等
// parent:父模块Huobi Global,如在 `a.js` 中 `require('b.js')`,那么这里的 request 为 'b.js',
parent 为 `a.js`对应的 Module 对象
// isMain: 除入口文件为 `true` 外Huobi Global,其他模块都为 `false`
Module._load = function( request, parent, isMain) {
letrelResolveCacheIdentifier;
if(parent) {
debug( 'Module._load REQUEST %s parent: %s', request, parent.id);
// relativeResolveCache 是模块路径缓存Huobi Global,
// 用于加速父模块所在目录下的所有模块请求当前模块时
// 可以直接查询到实际路径Huobi Global,而不需要通过 _resolveFilename 查找文件
relResolveCacheIdentifier = ` ${parent.path}\x00 ${request}` ;
constfilename = relativeResolveCache[relResolveCacheIdentifier];
if(filename !== undefined) {
constcachedModule = Module._cache[filename];
if(cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
if(!cachedModule.loaded)
returngetExportsForCircularRequire(cachedModule);
returncachedModule.exports;
deleterelativeResolveCache[relResolveCacheIdentifier];
// 尝试查找模块文件路径Huobi Global,找不到模块抛出异常
constfilename = Module._resolveFilename(request, parent, isMain);
// 如果是内置模块Huobi Global,从 `NativeModule` 加载
if(StringPrototypeStartsWith(filename, 'node:')) {
// Slice 'node:' prefix
constid = StringPrototypeSlice(filename, 5);
constmodule= loadNativeModule(id, request);
if(! module?.canBeRequiredByUsers) {
thrownewERR_UNKNOWN_BUILTIN_MODULE(filename);
returnmodule.exports;
// 如果缓存中已存在Huobi Global,将当前模块 push 到父模块的 children 字段
constcachedModule = Module._cache[filename];
if(cachedModule !== undefined) {
updateChildren(parent, cachedModule, true);
// 处理循环引用
if(!cachedModule.loaded) {
constparseCachedModule = cjsParseCache.get(cachedModule);
if(!parseCachedModule || parseCachedModule.loaded)
returngetExportsForCircularRequire(cachedModule);
parseCachedModule.loaded = true;
} else{
returncachedModule.exports;
// 尝试从内置模块加载
constmod = loadNativeModule(filename, request);
if(mod?.canBeRequiredByUsers) returnmod.exports;
// Don't call updateChildren, Module constructor already does.
constmodule= cachedModule || newModule(filename, parent);
if(isMain) {
process.mainModule = module;
module.id = '.';
// 将 module 对象加入缓存
Module._cache[filename] = module;
if(parent !== undefined) {
relativeResolveCache[relResolveCacheIdentifier] = filename;
// 尝试加载模块Huobi Global,如果加载失败则删除缓存中的 module 对象,
// 同时删除父模块的 children 内的 module 对象Huobi Global。
letthrew = true;
try{
module.load(filename);
threw = false;
} finally{
if(threw) {
deleteModule._cache[filename];
if(parent !== undefined) {
deleterelativeResolveCache[relResolveCacheIdentifier];
constchildren = parent?.children;
if(ArrayIsArray(children)) {
constindex = ArrayPrototypeIndexOf(children, module);
if(index !== -1) {
ArrayPrototypeSplice(children, index, 1);
} elseif( module.exports &&
!isProxy( module.exports) &&
ObjectGetPrototypeOf( module.exports) ===
CircularRequirePrototypeWarningProxy) {
ObjectSetPrototypeOf( module.exports, ObjectPrototype);
// 返回 exports 对象
returnmodule.exports;
module 对象上的 load函数用于执行一个模块的加载:
load地址:
load地址:
Module.prototype.load = function(filename) {
debug( 'load %j for module %j', filename, this.id);
assert(! this.loaded);
this.filename = filename;
this.paths = Module._nodeModulePaths(path.dirname(filename));
constextension = findLongestRegisteredExtension(filename);
// allow .mjs to be overridden
if(StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions[ '.mjs'])
thrownewERR_REQUIRE_ESM(filename, true);
Module._extensions[extension]( this, filename);
this.loaded = true;
constesmLoader = asyncESM.esmLoader;
// Create module entry at load time to snapshot exports correctly
constexports= this. exports;
// Preemptively cache
if(( module?. module=== undefined ||
module. module.getStatus < kEvaluated) &&
!esmLoader.cjsCache.has( this))
esmLoader.cjsCache.set( this, exports);
实际的加载动作是在 Module._extensions[extension](this, filename); 中进行的Huobi Global,根据扩展名的不同,会有不同的加载策略:
1..js:调用 fs.readFileSync 读取文件内容,将文件内容包在 wrapper 中,需要注意的是,这里的 require 是 Module.prototype.require 而非内置模块的 require 方法Huobi Global。
constwrapper = [
'(function (exports, require, module, __filename, __dirname) { ',
'\n});',
2..json:调用 fs.readFileSync 读取文件内容,并转换为对象Huobi Global。
3..node:调用 dlopen 打开 node 扩展Huobi Global。
.js地址:
.json地址:
.node地址:
.js地址:
.json地址:
.node地址:
而 Module.prototype.require 函数也是调用Huobi Global了静态方法 Module._load实现模块加载的:
Module.prototype.require = function( id) {
validateString(id, 'id');
if(id === '') {
thrownewERR_INVALID_ARG_VALUE( 'id', id,
'must be a non-empty string');
requireDepth++;
try{
returnModule._load(id, this, /* isMain */false);
} finally{
requireDepth--;
看到这里Huobi Global,cjs 模块的加载过程已经基本清晰了:
1.初始化 nodeHuobi Global,加载 NativeModule,用于加载所有的内置的 js 和 c++ 模块
2.运行内置模块 run_main
3.在 run_main 中引入用户模块系统 module
4.通过 module 的 _load 方法加载入口文件,在加载时通过传入 module.require 和 module.exports 等让入口文件可以正常 require 其他依赖模块并递归让整个依赖树被完整加载Huobi Global。
在清楚了 cjs 模块加载的完整流程之后,我们还可以顺着这条链路阅读其他代码,比如 global 变量的初始化,esModule 的管理方式等,更深入地理解 node 内的各种实现Huobi Global。
评论