本文共 10901 字,大约阅读时间需要 36 分钟。
在 ES2015 标准之前,JavaScript 语言没有原生的组织代码的方式。Node.js 用 CommonJS 模块规范填补了这个空白。我想通过这篇文章和大家分享一下当下的 CommonJS 模块系统的一些机制和细节。
在写这篇文章的时阅读代码 Node.js 版本是 v10.0.0
全文共由三个部分组成:
模块是代码结构的基本组成部分。通过模块系统,我们可以用模块化的方式来组织应用代码。模块可以通过 module.exports
自由地隐藏内部实现、对外暴露接口 。我们只需要通过 require
,就能实现模块加载引入。
当下我们在 Node.js 中最常使用的到的模块系统 commonjs
,是 JavaScript
// add.jsfunction add (a, b) { return a + b}module.exports = { add}
当我们需要在其他地方使用 add
方法时,比如:
// app.jsconst { add } = require('./add')console.log(add(1, 2))// 3
我们只需要调用 require('./add')
就能实现对模块的引入。
require()
可能很多同学在开始学 Node.js 的时候,都曾经遇到这样的问题:
Cannot find module '../../add.js'
? 找不到模块的错误。
loader.js:573 Uncaught Error: Cannot find module '../../add.js' at Function.Module._resolveFilename (internal/modules/cjs/loader.js:571:15) at Function.Module._load (internal/modules/cjs/loader.js:497:25) at Module.require (internal/modules/cjs/loader.js:626:17) at require (internal/modules/cjs/helpers.js:20:18) at:1:1
一般找不到模块的原因:
我们可以在这异常错误堆栈看到,当我们用 require()
时,它内部调用了那些方法。
require
Module.require
Module._load
Module._resolveFilename
我们可以通过这里的调用堆栈一步步探索 require()
的内部机制
require()
的流程为了使之后代码细节显得不那么TLDR,先通过这个图,对整体流程有一定印象:
虽然在 Node.js 8 之后开始做 Node.js 支持 ES Module 的工作,CommonJs 模块系统的实现还是经过了不小的变化。但是总体流程仍然是和之前几乎一致。
我们先到 helpers.js 看看 require()
是怎么来的:
var require = makeRequireFunction(this);var result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname);
通过上面 ?的代码看到,require
是调用 makeRequireFunction(module)
来生成的。
代码中的 this
是一个模块实例(module),至于模块实例是怎么生成的我们之后再细看。
require
和 exports
, filename
, dirname
等作为参数, 执行了 完成编译之后模块代码的包装函数 compiledWrapper
,这里就是我们的模块由文件字符到 JS 代码被执行的阶段。
模块代码通过 Module.wrap
包装,之后调用 vm.runInThisContext
执行得到了模块包装函数。
Module.wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});'];Module.wrap = function(script) { return Module.wrapper[0] + script + Module.wrapper[1];};
这里就是在模块代码中常用到的 module
, exports
, require
,__dirname
, __filename
的来源。
经过 Module.wrap()
包装之后的代码使用 vm.runInThisContext()
在当前上下文中编译执行。
// content 来自模块的代码var wrapper = Module.wrap(content);var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true});
完成上面的铺垫,回归主线,开始解析 makeRequireFunction
做了什么。
makeRequireFunction
的实现// internal/modules/cjs/helpers.js:20// 调用 makeRequireFunction(module) 这里的 | module | 是 Module对象// 生成当前模块上下文的 require()function makeRequireFunction(mod) { const Module = mod.constructor; // 创建一个模块相关的 require // 依赖深度机制实现十分巧妙 function require(path) { try { exports.requireDepth += 1; return mod.require(path); } finally { exports.requireDepth -= 1; } } // resolve 是对 Module._resolveFilename 的封装 function resolve(request, options) { if (typeof request !== 'string') { throw new ERR_INVALID_ARG_TYPE('request', 'string', request); } return Module._resolveFilename(request, mod, false, options); } require.resolve = resolve; // resolve.paths 是对 Module._resolveLookupPaths 的封装 function paths(request) { if (typeof request !== 'string') { throw new ERR_INVALID_ARG_TYPE('request', 'string', request); } return Module._resolveLookupPaths(request, mod, true); } resolve.paths = paths; // process.mainModule 入口模块 require.main = process.mainModule; // 启用支持以添加额外的扩展类型 // 我们可以实现 require.extensions['.xxx'] // 来实现对自定义文件类型模块的支持 require.extensions = Module._extensions; // 模块的缓存 // key: 模块路径 // value: 模块实例 require.cache = Module._cache; return require;}
通过上面的代码我们完整了解了 makeRequireFunction
的实现,它主要实现了:
module
上下文的 require
方法,内部调用 module
实例上 的 require
方法require.resolve
添加类型校验,封装了 Module._resolveFilename
require.resolve.paths
封装了 Module._resolveLookupPaths
require.main
添加入口模块 process.mainModule
require.extensions
暴露 Module._extensions
,提供了扩展能力require.cache
暴露了 Module._cache
,我们可以通过 require.cache
操作模块缓存。module.require
// Loads a module at the given file path. Returns that module's// `exports` property.Module.prototype.require = function(id) { // 参数校验 // 必须为 非空字符 if (typeof id !== 'string') { throw new ERR_INVALID_ARG_TYPE('id', 'string', id); } if (id === '') { throw new ERR_INVALID_ARG_VALUE('id', id, 'must be a non-empty string'); } // 调用 Module._load return Module._load(id, this, /* isMain */ false);};
可以看到 module.require
参数校验完成之后,就调用 Module._load
Module._load 的主要逻辑:
NativeModule.require()
Module._load = function(request, parent, isMain) { if (parent) { debug('Module._load REQUEST %s parent: %s', request, parent.id); } if (experimentalModules && isMain) { asyncESM.loaderPromise.then((loader) => { return loader.import(getURLFromFilePath(request).pathname); }) .catch((e) => { decorateErrorStack(e); console.error(e); process.exit(1); }); return; } var filename = Module._resolveFilename(request, parent, isMain); var cachedModule = Module._cache[filename]; if (cachedModule) { updateChildren(parent, cachedModule, true); return cachedModule.exports; } if (NativeModule.nonInternalExists(filename)) { debug('load native module %s', request); return NativeModule.require(filename); } // Don't call updateChildren(), Module constructor already does. var module = new Module(filename, parent); if (isMain) { process.mainModule = module; module.id = '.'; } Module._cache[filename] = module; tryModuleLoad(module, filename); return module.exports;};
可以看到,上面代码中的 Module._cache[filename] = module
, 对还没有开始加载的模块就写入缓存可能是不安全的。
tryModuleLoad
这这里做了检测,如果模块加载失败,会清理模块的缓存。
function tryModuleLoad(module, filename) { var threw = true; try { module.load(filename); threw = false; } finally { if (threw) { delete Module._cache[filename]; } }}
module.load
的主要逻辑:
module.filename
module.paths
filename
文件后缀,选择不同的处理方式ESMLoader
加载模块// 设置文件名,并选择相应的文件处理// Given a file name, pass it to the proper extension handler.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)); var extension = path.extname(filename) || '.js'; if (!Module._extensions[extension]) extension = '.js'; Module._extensions[extension](this, filename); this.loaded = true; if (experimentalModules) { // ES Module 相关逻辑,下篇文章分析 ... }};
同步读取文件,清除文件中的 BOM
编码字符,然后调用 module._compile
编译
// Native extension for .jsModule._extensions['.js'] = function(module, filename) { var content = fs.readFileSync(filename, 'utf8'); module._compile(stripBOM(content), filename);};
module._compile
module._compile
的主要工作:
以下的代码是 module._compile
的部分代码,去掉了 调试相关和文件 stat 缓存的代码。
Module.prototype._compile = function(content, filename) { // 去除 Shebang 比如:#!/bin/sh content = stripShebang(content); // create wrapper function // 创建封装函数 var wrapper = Module.wrap(content); // 在当前上下文编译模块的封装函数代码 // 传入当前模块的文件名,用于定义堆栈跟踪信息 // 如在解析代码的时候发生错误Error,引起错误的行将会被加入堆栈跟踪信息 var compiledWrapper = vm.runInThisContext(wrapper, { filename: filename, lineOffset: 0, displayErrors: true }); ... var dirname = path.dirname(filename); var require = makeRequireFunction(this); var depth = requireDepth; ... // 运行模块的封装函数 // 并传入 `exports` , `require`, `module` `filename`, `dirname` var result = compiledWrapper.call(this.exports, this.exports, require, this, filename, dirname); return result;};
模块的代码最终在 Module.prototype._compile
中完成了封装、编译、执行。最后我们在回看 Module._load
,之前我们看到的 Module.load
, Module.prototype._compile
都在 tryModuleLoad()
中被调用, 当这些都执行完成之后,Module._load
返回 module.exports
Module._load = function(request, parent, isMain) { ... tryModuleLoad(module, filename); return module.exports;};
所以我们在模块可以使用下面的方式暴露模块 API
function add () {}module.exports.add = addexports.add = addthis.add = addmodule.exports = { add}
模块请求 -> 路径的缓存
Module._findPath = function(request, paths, isMain) { if (path.isAbsolute(request)) { paths = ['']; } else if (!paths || paths.length === 0) { return false; } var cacheKey = request + '\x00' + (paths.length === 1 ? paths[0] : paths.join('\x00')); var entry = Module._pathCache[cacheKey]; if (entry) return entry; ... }
模块路径 -> 模块实例的缓存
Module._load = function(request, parent, isMain) { ... var filename = Module._resolveFilename(request, parent, isMain); var cachedModule = Module._cache[filename]; if (cachedModule) { updateChildren(parent, cachedModule, true); return cachedModule.exports; } ... }
当我们启动 Node.js 时,入口文件也是作为一个模块被加载执行。
// bootstrap main module.Module.runMain = function() { // Load the main module--the command line argument. Module._load(process.argv[1], null, true); // Handle any nextTicks added in the first tick of the program process._tickCallback();};
我们之前看到 require.resolve()
封装了 Module._resolveFilename
。从最初调用 require()
输入的字符 request
通过_resolveFilename
匹配到了模块文件路径 ,这在继续深入会发现 Module._resolveLookupPaths
,。
Module._resolveLookupPaths()
会返回一个数组,第一个元素是我们的 request
,第二个元素
console.log(Module._resolveLookupPaths('app', module))[ "app", [ "/Users/awe/Desktop/code/test/node-module/node_modules", "/Users/awe/Desktop/code/test/node_modules", "/Users/awe/Desktop/code/node_modules", "/Users/awe/Desktop/node_modules", "/Users/awe/node_modules", "/Users/node_modules", "/node_modules", "/Users/awe/Desktop/code/test/node-module", "/Users/awe/.node_modules", "/Users/awe/.node_libraries", "/Users/awe/.nvm/versions/node/v10.0.0/lib/node" ]]
// --inspect-brk if (debug_options.wait_for_connect()) { READONLY_DONT_ENUM_PROPERTY(process, "_breakFirstLine", True(env->isolate())); }
回想最开头提到的找不到模块的错误是在 Module._resolveFilename
,我们可以在 node 源码中看看它的实现
Module._resolveFilename
的功能是根据我们在 require
输入的参数,尝试查找可以引入的模块。
Module._resolveFilename = function(request, parent, isMain, options) { ... // look up the filename first, since that's the cache key. var filename = Module._findPath(request, paths, isMain); if (!filename) { // 可以看到,找不到模块的报错来自于这里 // eslint-disable-next-line no-restricted-syntax var err = new Error(`Cannot find module '${request}'`); err.code = 'MODULE_NOT_FOUND'; throw err; } return filename;};
通过这篇文章,我们从代码实现了解了 Node.js 的 CommonJS 模块系统的实现,再次回顾模块系统的流程:
转载地址:http://gjkei.baihongyu.com/