Saya percaya semua orang tahu cara memuatkan modul dalam Nod:
const fs = require('fs'); const express = require('express'); const anotherModule = require('./another-module');
Ya, require
sedang memuatkan cjs API modul, tetapi V8 sendiri tidak mempunyai sistem modul cjs, jadi bagaimana nod mencari modul melalui require
dan memuatkannya? [Tutorial berkaitan yang disyorkan: tutorial video nodejs]
Hari ini kita akan meneroka kod sumber Node.js dan mendapat pemahaman yang mendalam tentang proses pemuatan modul cjs. Versi kod nod yang kami baca ialah v17.x:
github.>Modul terbina dalam
require
[src/node_main.cc](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node_main.cc#L105)
Untuk mengetahui logik kerja daripada [node::Start](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc#L1134)
, kami Anda perlu terlebih dahulu memahami cara modul terbina dalam dimuatkan ke dalam nod (seperti 'fs', 'path', 'child_process', yang juga termasuk beberapa modul dalaman yang tidak boleh dirujuk oleh pengguna menyediakan kod, kita perlu bermula dari nod Mula membaca.
Fungsi utama nod adalah dalam
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; }
NodeMainInstance
main_instance
Gelung peristiwa dicipta di sini dan tika dicipta dan Kaedah
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
[CreateMainEnvironment](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node_main_instance.cc#L170)
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; }
Environment
env
Buat[RunBootstrapping](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc#L398)
Objek
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); }
[BootstrapInternalLoaders](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc#L298)
[internal/bootstrap/loaders.js](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.js#L326)
[nativeModulerequire](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.js#L332)
di sini melaksanakan langkah yang sangat penting dalam proses pemuatan modul nod:
Dengan membungkus dan melaksanakan [internalBinding](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.js#L164)
, fungsi [NativeModule](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/lib/internal/bootstrap/loaders.js#L191)
modul terbina dalam digunakan untuk memuatkan modul js terbina dalam, dan
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;
require
Perlu diambil perhatian bahawa fungsi require('module')._cache
ini hanya akan digunakan untuk memuatkan modul terbina dalam dan ia tidak akan digunakan untuk memuatkan modul pengguna. (Ini juga sebab kita boleh melihat semua modul pengguna dengan mencetak fs
, tetapi bukan modul terbina dalam seperti
Modul Pengguna
[NodeMainInstance::Run](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node_main_instance.cc#L127)
Seterusnya mari kita kembalikan perhatian kepada fungsi
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; }
CreateMainEnvironment
Kami Objek env
telah dibuat melalui fungsi Environment
Contoh NativeModule
ini sudah mempunyai sistem modul Run
untuk mengekalkan modul terbina dalam.
Kod itu kemudiannya akan dijalankan ke versi terlampau beban fungsi yang lain
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 }
[LoadEnvironment](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/api/environment.cc#L403)
memanggil
MaybeLocal<Value> LoadEnvironment( Environment* env, StartExecutionCallback cb) { env->InitializeLibuv(); env->InitializeDiagnostics(); return StartExecution(env, cb); }
[StartExecution](https://github.com/nodejs/node/blob/881174e016d6c27b20c70111e6eae2296b6c6293/src/node.cc#L455)
dan kemudian laksanakan
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 教程!
Atas ialah kandungan terperinci Terokai kod sumber Node.js dan terangkan proses pemuatan modul cjs secara terperinci. Untuk maklumat lanjut, sila ikut artikel berkaitan lain di laman web China PHP!