Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Node.js 中的 require 是如何工作的?_程序员成长指北-CSDN博客 #31

Open
zepang opened this issue Jan 20, 2022 · 0 comments

Comments

@zepang
Copy link
Owner

zepang commented Jan 20, 2022

作者:FESKY 链接:https://juejin.im/post/6844903957752463374

作为前端开发者,不可避免每天都要跟 Node.js 打交道。Node 遵循 Commonjs 规范,规范的核心是通过 require 来加载依赖的其他模块。我们已经常习惯于使用社区提供的各种库,但对于模块引用的背后原理知之甚少。这篇文章通过源码阅读,浅析在 commonjs 规范中 require 背后的工作原理。

require 从哪里来?

大家都知道,在 node js 的模块 / 文件中,有些 “全局” 变量是可以直接使用的,比如 require, module, __dirname, __filename, exports。其实这些变量或方法并不是 “全局” 的,而是在 commonjs 模块加载中, 通过包裹的形式,提供的局部变量。

module.exports = function () {

经过 compile 之后,就有了 module__dirname 等变量可以直接使用。

(function (exports, require, module, __filename, __dirname) {    module.exports = function () {

这也可以很好解答初学者常常会困惑的问题,为什么给 exports 赋值,require 之后得到的结果是 undefined?

(function (exports, module) {

直接赋值只是修改了局部变脸 exports 的值。最终 export 出去的 module.exports 没有被赋值。

require 的查找过程

文档中描述得非常清楚,简化版 require 模块的查找过程如下:在 Y 路径下,require(X)

  1. 如果X是内置模块(http, fs, path 等), 直接返回内置模块,不再执行

  2. 如果 X 以 '/' 开头,把 Y 设置为文件系统根目录

  3. 如果 X 以 './', '/', '../' 开头 a. 按照文件的形式加载(Y + X),根据 extensions 依次尝试加载文件 [X, X.js, X.json, X.node] 如果存在就返回该文件,不再继续执行。b. 按照文件夹的形式加载(Y + X),如果存在就返回该文件,不再继续执行,若找不到将抛出错误 a. 尝试解析路径下 package.json main 字段 b. 尝试加载路径下的 index 文件(index.js, index.json, index.node)

  4. 搜索 NODE_MODULE,若存在就返回模块 a. 从路径 Y 开始,一层层往上找,尝试加载(路径 + 'node_modules/' + X) b. 在 GLOBAL_FOLDERS node_modules 目录中查找 X

  5. 抛出 "Not Found" Error 复制代码例如在 /Users/helkyle/projects/learning-module/foo.js` 中 require('bar') 将会从`/Users/helkyle/projects/learning-module/ 开始逐层往上查找bar 模块(不是以 './', '/', '../' 开头)。

'/Users/helkyle/projects/learning-module/node_modules','/Users/helkyle/projects/node_modules','/Users/helkyle/node_modules',

需要注意的是,在使用 npm link 功能的时候,被 link 模块内的 require 会以被 link 模块在文件系统中的绝对路径进行查找,而不是 main module 所在的路径。举个例子,假设有两个模块。

通过 link 形式在 foo 模块中 link bar,会产生软连 /usr/lib/foo/node_modules/bar 指向 /usr/lib/bar,这种情况下 bar 模块下 require('quux') 的查找路径是 /usr/lib/bar/node_modules/而不是 /usr/lib/foo/node_modules我之前踩过的坑

Cache 机制

在实践过程中能了解到,实际上 Node module require 的过程会有缓存。也就是两次 require 同一个 module会得到一样的结果。

const a1 = require('./a.js');const a2 = require('./a.js');

执行 node b.js,可以看到,第二次 require a.js 跟第一次 require 得到的是相同的模块引用。从源码上看,require 是对 module 常用方法的封装。

function makeRequireFunction(mod, redirects) {const Module = mod.constructor;  require = function require(path) {return mod.require(path);  function resolve(request, options) {    validateString(request, 'request');return Module._resolveFilename(request, mod, false, options);  require.resolve = resolve;  function paths(request) {    validateString(request, 'request');return Module._resolveLookupPaths(request, mod);  require.main = process.mainModule;  require.extensions = Module._extensions;  require.cache = Module._cache;

跟踪代码看到,require() 最终调用的是 Module._load 方法:// 忽略代码,看看 load 的过程发生了什么?

Module._load = function(request, parent, isMain) {const filename = Module._resolveFilename(request, parent, isMain);const cachedModule = Module._cache[filename];if (cachedModule !== undefined) {return cachedModule.exports;const mod = loadNativeModule(filename, request, experimentalModules);if (mod && mod.canBeRequiredByUsers) return mod.exports;const module = new Module(filename, parent);    process.mainModule = module;  Module._cache[filename] = module;

到这里,module cache 的原理也很清晰,模块在首次加载后,会以模块绝对路径为 key 缓存在 Module._cache属性上,再次 require 时会直接返回已缓存的结果以提高 效率。在控制台打印 require.cache 看看。

console.log(require.cache);

缓存中有两个key,分别是 a.js, b.js 文件在系统中的绝对路径。value 则是对应模块 load 之后的 module 对象。所以第二次 require('./a.js') 的结果是 require.cache['/Users/helkyle/projects/learning-module/a.js'].exports 和第一次 require 指向的是同一个 Object

'/Users/helkyle/projects/learning-module/b.js':          filename: '/Users/helkyle/projects/learning-module/b.js',          [ '/Users/helkyle/projects/learning-module/node_modules','/Users/helkyle/projects/node_modules','/Users/helkyle/node_modules','/Users/helkyle/projects/learning-module/a.js':          id: '/Users/helkyle/projects/learning-module/a.js',            filename: '/Users/helkyle/projects/learning-module/b.js',         filename: '/Users/helkyle/projects/learning-module/a.js','/Users/helkyle/projects/learning-module/node_modules','/Users/helkyle/projects/node_modules','/Users/helkyle/node_modules',

应用——实现 Jest 的 mock module 效果

jest  是 Facebook 开源的前端测试库,提供了很多非常强大又实用的功能。mock module 是其中非常抢眼的特性。使用方式是在需要被 mock 的文件模块同级目录下的 __mock__ 文件夹添加同名文件,执行测试代码时运行 jest.mock(modulePath),jest 会自动加载 mock 版本的 module。举个例子,项目中有个 apis 文件,提供对接后端 api。

    getUsers: () => fetch('api/users')

在跑测试过程中,不希望它真的连接后端请求。这时候根据 jest 文档,在 apis 文件同级目录创建 mock file

测试文件中,主动调用 jest.mock('./apis.js') 即可。

const apis = require('./apis.js');

了解 require 的基础原理之后,我们也来实现类似的功能,将加载 api.js 的语句改写成加载 mock/api.js。

使用 require.cache

由于缓存机制的存在,提前写入目标缓存,再次 require 将得到我们期望的结果。

require('./__mock__/apis.js');const originalPath = require.resolve('./apis.js');require.cache[originalPath] = require.cache[require.resolve('./__mock__/apis.js')];const apis = require('./apis.js');

魔改 module._load

基于 require.cache 的方式,需要提前 require mock module。????提到了,由于最终都是通过 Module._load来加载模块,在这个位置进行拦截即可完成按需 mock

const Module = require('module');const originalLoad = Module._load;Module._load = function (path, ...rest) {if (path === './apis.js') {    path = './__mock__/apis.js';return originalLoad.apply(Module, [path, ...rest]);const apis = require('./apis.js');

注意:以上内容仅供参考。从实际运行结果上看,Jest 有自己实现的模块加载机制,跟 commonjs 有出入。比如在 jestrequire module 并不会写入 require.cache

程序启动时的 require

查阅 Node 文档发现,在 Command Line 章节也有一个 --require ,使用这个参数可以在执行业务代码之前预先加载特定模块。举个例子,编写 setup 文件,往 global 对象上挂载 it, assert 等方法。

global.it = async function test(title, callback) {    console.log(`✓ ${title}`);    console.error(`✕ ${title}`);global.assert = require('assert');

给启动代码添加 --require 参数。引入 global.assert, global.it,就可以在代码中直接使用 assert, it 不用在测试文件中引入。

node --require './setup.js' foo.test.js
it('add two numbers', () => {
❤️爱心三连击1.看到这里了就点个在看支持下吧,你的「在看」是我创作的动力。2.关注公众号程序员成长指北,回复「1」加入Node进阶交流群!「在这里有好多 Node 开发者,会讨论 Node 知识,互相学习」!3.也可添加微信【ikoala520】,一起成长。

https://blog.csdn.net/xgangzai/article/details/108505416

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant