I believe everyone knows how to load a module in Node:
const fs = require('fs'); const express = require('express'); const anotherModule = require('./another-module');
Yes, require
It is the API for loading cjs modules, but V8 itself does not have a cjs module system, so how does node find the module and load it through require
? [Related tutorial recommendations: nodejs video tutorial]
Today we will explore the Node.js source code and gain an in-depth understanding of the loading process of the cjs module. The node code version we are reading is v17.
For working logic, we need to first understand how built-in modules are loaded into node (such as 'fs', 'path', 'child_process', which also include some internal modules that cannot be referenced by users). After preparing the code, we First, start reading from node startup. The main function of node is in [src/node_main.cc](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node_main.cc#L105), by calling the method[node::Start](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc#L1134) To start a node instance:
int Start(int argc, char** argv) { InitializationResult result = InitializeOncePerProcess(argc, argv); if (result.early_return) { return result.exit_code; } { Isolate::CreateParams params; const std::vector<size_t>* indices = nullptr; const EnvSerializeInfo* 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(); return result.exit_code; }
Create here The event loop is opened, and an instance of NodeMainInstance
is created main_instance
and its Run
method is called:
int NodeMainInstance::Run(const EnvSerializeInfo* env_info) { Locker locker(isolate_); Isolate::Scope isolate_scope(isolate_); HandleScope handle_scope(isolate_); int exit_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()); return exit_code; }
Run
Call [CreateMainEnvironment](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node_main_instance.cc#L170)
to create and initialize the environment: Environment* CreateEnvironment(
IsolateData* isolate_data,
Local<Context> context,
const std::vector<std::string>& args,
const std::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 = new Environment(
isolate_data, context, args, exec_args, nullptr, flags, thread_id);
#if HAVE_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);
return nullptr;
}
return env;
}
object env
and call its [RunBootstrapping](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc# L398)
Method:
MaybeLocal<Value> Environment::RunBootstrapping() { EscapableHandleScope scope(isolate_); CHECK(!has_run_bootstrapping_code()); if (BootstrapInternalLoaders().IsEmpty()) { return MaybeLocal<Value>(); } Local<Value> result; if (!BootstrapNode().ToLocal(&result)) { return MaybeLocal<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(); return scope.Escape(result); }
Here[BootstrapInternalLoaders](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc#L298)
Implemented a very important step in the node module loading process:
By packaging and executing [internal/bootstrap/loaders.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.js#L326)
Get the built-in The module's [nativeModulerequire](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.js#L332)
function is used to load the built-in js module and obtain
Used to load built-in C modules, [ NativeModule](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.js#L191)
is a small module system specially used for built-in modules. <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">function nativeModuleRequire(id) {
if (id === loaderId) {
return loaderExports;
}
const mod = 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) throw new TypeError(`Missing internal module '${id}'`);
return mod.compileForInternalLoader();
}
const loaderExports = {
internalBinding,
NativeModule,
require: nativeModuleRequire
};
return loaderExports;</pre><div class="contentsignin">Copy after login</div></div>
It should be noted that this require
function will only be used to load built-in modules, and it will not be used to load user modules. (This is also the reason why we can see all user modules by printing require('module')._cache
, but cannot see built-in modules such as fs
, because the loading of both Not the same as cache maintenance).
User Module
Next let us move our attention back to
Function: int NodeMainInstance::Run(const EnvSerializeInfo* env_info) {
Locker locker(isolate_);
Isolate::Scope isolate_scope(isolate_);
HandleScope handle_scope(isolate_);
int exit_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());
return exit_code;
}
object, this Environment
instance already has a module system
used to maintain built-in modules.
The code will then run to another overloaded version of the
Run function
: <div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">void NodeMainInstance::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.
#if HAVE_INSPECTOR && defined(__POSIX__) && !defined(NODE_SHARED_MODE)
struct sigaction act;
memset(&act, 0, sizeof(act));
for (unsigned nr = 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
#if defined(LEAK_SANITIZER)
__lsan_do_leak_check();
#endif
}</pre><div class="contentsignin">Copy after login</div></div>
Call here[LoadEnvironment](https://github.com /nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/api/environment.cc#L403)
:<div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:php;toolbar:false">MaybeLocal<Value> LoadEnvironment(
Environment* env,
StartExecutionCallback cb) {
env->InitializeLibuv();
env->InitializeDiagnostics();
return StartExecution(env, cb);
}</pre><div class="contentsignin">Copy after login</div></div>
Then execute [StartExecution](https://github.com/nodejs/node /blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc#L455):
MaybeLocal<Value> StartExecution(Environment* env, StartExecutionCallback cb) { // 已省略其他运行方式,我们只看 `node index.js` 这种情况,不影响我们理解模块系统 if (!first_argv.empty() && first_argv != "-") { return StartExecution(env, "internal/main/run_main_module"); } }
在 StartExecution(env, "internal/main/run_main_module")
这个调用中,我们会包装一个 function,并传入刚刚从 loaders 中导出的 require
函数,并运行 [lib/internal/main/run_main_module.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/main/run_main_module.js)
内的代码:
'use strict'; const { prepareMainThreadExecution } = require('internal/bootstrap/pre_execution'); prepareMainThreadExecution(true); markBootstrapComplete(); // Note: this loads the module through the ESM loader if the module is // determined to be an ES module. This hangs from the CJS module loader // because we currently allow monkey-patching of the module loaders // in the preloaded scripts through require('module'). // runMain here might be monkey-patched by users in --require. // XXX: the monkey-patchability here should probably be deprecated. require('internal/modules/cjs/loader').Module.runMain(process.argv[1]);
所谓的包装 function 并传入 require
,伪代码如下:
(function(require, /* 其他入参 */) { // 这里是 internal/main/run_main_module.js 的文件内容 })();
所以这里是通过内置模块的 require
函数加载了 [lib/internal/modules/cjs/loader.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/modules/cjs/loader.js#L172)
导出的 Module 对象上的 runMain
方法,不过我们在 loader.js
中并没有发现 runMain
函数,其实这个函数是在 [lib/internal/bootstrap/pre_execution.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/pre_execution.js#L428)
中被定义到 Module
对象上的:
function initializeCJSLoader() { const CJSLoader = 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](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/modules/run_main.js#L74)
中找到 executeUserEntryPoint
方法:
function executeUserEntryPoint(main = process.argv[1]) { const resolvedMain = resolveMainPath(main); const useESMLoader = shouldUseESMLoader(resolvedMain); if (useESMLoader) { runMainESM(resolvedMain || main); } else { // Module._load is the monkey-patchable CJS module loader. Module._load(main, null, true); } }
参数 main
即为我们传入的入口文件 index.js
。可以看到,index.js
作为一个 cjs 模块应该被 Module._load
加载,那么 _load
干了些什么呢?这个函数是 cjs 模块加载过程中最重要的一个函数,值得仔细阅读:
// `_load` 函数检查请求文件的缓存 // 1. 如果模块已经存在,返回已缓存的 exports 对象 // 2. 如果模块是内置模块,通过调用 `NativeModule.prototype.compileForPublicLoader()` // 获取内置模块的 exports 对象,compileForPublicLoader 函数是有白名单的,只能获取公开 // 内置模块的 exports。 // 3. 以上两者皆为否,创建新的 Module 对象并保存到缓存中,然后通过它加载文件并返回其 exports。 // request:请求的模块,比如 `fs`,`./another-module`,'@pipcook/core' 等 // parent:父模块,如在 `a.js` 中 `require('b.js')`,那么这里的 request 为 'b.js', parent 为 `a.js` 对应的 Module 对象 // isMain: 除入口文件为 `true` 外,其他模块都为 `false` Module._load = function(request, parent, isMain) { let relResolveCacheIdentifier; if (parent) { debug('Module._load REQUEST %s parent: %s', request, parent.id); // relativeResolveCache 是模块路径缓存, // 用于加速父模块所在目录下的所有模块请求当前模块时 // 可以直接查询到实际路径,而不需要通过 _resolveFilename 查找文件 relResolveCacheIdentifier = `${parent.path}\x00${request}`; const filename = relativeResolveCache[relResolveCacheIdentifier]; if (filename !== undefined) { const cachedModule = Module._cache[filename]; if (cachedModule !== undefined) { updateChildren(parent, cachedModule, true); if (!cachedModule.loaded) return getExportsForCircularRequire(cachedModule); return cachedModule.exports; } delete relativeResolveCache[relResolveCacheIdentifier]; } } // 尝试查找模块文件路径,找不到模块抛出异常 const filename = Module._resolveFilename(request, parent, isMain); // 如果是内置模块,从 `NativeModule` 加载 if (StringPrototypeStartsWith(filename, 'node:')) { // Slice 'node:' prefix const id = StringPrototypeSlice(filename, 5); const module = loadNativeModule(id, request); if (!module?.canBeRequiredByUsers) { throw new ERR_UNKNOWN_BUILTIN_MODULE(filename); } return module.exports; } // 如果缓存中已存在,将当前模块 push 到父模块的 children 字段 const cachedModule = Module._cache[filename]; if (cachedModule !== undefined) { updateChildren(parent, cachedModule, true); // 处理循环引用 if (!cachedModule.loaded) { const parseCachedModule = cjsParseCache.get(cachedModule); if (!parseCachedModule || parseCachedModule.loaded) return getExportsForCircularRequire(cachedModule); parseCachedModule.loaded = true; } else { return cachedModule.exports; } } // 尝试从内置模块加载 const mod = loadNativeModule(filename, request); if (mod?.canBeRequiredByUsers) return mod.exports; // Don't call updateChildren(), Module constructor already does. const module = cachedModule || new Module(filename, parent); if (isMain) { process.mainModule = module; module.id = '.'; } // 将 module 对象加入缓存 Module._cache[filename] = module; if (parent !== undefined) { relativeResolveCache[relResolveCacheIdentifier] = filename; } // 尝试加载模块,如果加载失败则删除缓存中的 module 对象, // 同时删除父模块的 children 内的 module 对象。 let threw = true; try { module.load(filename); threw = false; } finally { if (threw) { delete Module._cache[filename]; if (parent !== undefined) { delete relativeResolveCache[relResolveCacheIdentifier]; const children = parent?.children; if (ArrayIsArray(children)) { const index = ArrayPrototypeIndexOf(children, module); if (index !== -1) { ArrayPrototypeSplice(children, index, 1); } } } } else if (module.exports && !isProxy(module.exports) && ObjectGetPrototypeOf(module.exports) === CircularRequirePrototypeWarningProxy) { ObjectSetPrototypeOf(module.exports, ObjectPrototype); } } // 返回 exports 对象 return module.exports; };
module
对象上的 [load](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/modules/cjs/loader.js#L963)
函数用于执行一个模块的加载:
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)); const extension = findLongestRegisteredExtension(filename); // allow .mjs to be overridden if (StringPrototypeEndsWith(filename, '.mjs') && !Module._extensions['.mjs']) throw new ERR_REQUIRE_ESM(filename, true); Module._extensions[extension](this, filename); this.loaded = true; const esmLoader = asyncESM.esmLoader; // Create module entry at load time to snapshot exports correctly const exports = 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);
中进行的,根据扩展名的不同,会有不同的加载策略:
fs.readFileSync
读取文件内容,将文件内容包在 wrapper 中,需要注意的是,这里的 require
是 Module.prototype.require
而非内置模块的 require
方法。const wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});', ];
而 Module.prototype.require
函数也是调用了静态方法 Module._load
实现模块加载的:
Module.prototype.require = function(id) { validateString(id, 'id'); if (id === '') { throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string'); } requireDepth++; try { return Module._load(id, this, /* isMain */ false); } finally { requireDepth--; } };
看到这里,cjs 模块的加载过程已经基本清晰了:
初始化 node,加载 NativeModule,用于加载所有的内置的 js 和 c++ 模块
运行内置模块 run_main
在 run_main
中引入用户模块系统 module
通过 module
的 _load
方法加载入口文件,在加载时通过传入 module.require
和 module.exports
等让入口文件可以正常 require
其他依赖模块并递归让整个依赖树被完整加载。
在清楚了 cjs 模块加载的完整流程之后,我们还可以顺着这条链路阅读其他代码,比如 global
变量的初始化,esModule 的管理方式等,更深入地理解 node 内的各种实现。
更多node相关知识,请访问:nodejs 教程!
The above is the detailed content of Explore the Node.js source code and explain the loading process of the cjs module in detail. For more information, please follow other related articles on the PHP Chinese website!