We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
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
作者:FESKY 链接:https://juejin.im/post/6844903957752463374
作为前端开发者,不可避免每天都要跟 Node.js 打交道。Node 遵循 Commonjs 规范,规范的核心是通过 require 来加载依赖的其他模块。我们已经常习惯于使用社区提供的各种库,但对于模块引用的背后原理知之甚少。这篇文章通过源码阅读,浅析在 commonjs 规范中 require 背后的工作原理。
Node.js
Node
Commonjs
require
commonjs
大家都知道,在 node js 的模块 / 文件中,有些 “全局” 变量是可以直接使用的,比如 require, module, __dirname, __filename, exports。其实这些变量或方法并不是 “全局” 的,而是在 commonjs 模块加载中, 通过包裹的形式,提供的局部变量。
node js
require, module, __dirname, __filename, exports
module.exports = function () {
经过 compile 之后,就有了 module,__dirname 等变量可以直接使用。
compile
module
__dirname
(function (exports, require, module, __filename, __dirname) { module.exports = function () {
这也可以很好解答初学者常常会困惑的问题,为什么给 exports 赋值,require 之后得到的结果是 undefined?
exports
undefined?
(function (exports, module) {
直接赋值只是修改了局部变脸 exports 的值。最终 export 出去的 module.exports 没有被赋值。
export
module.exports
文档中描述得非常清楚,简化版 require 模块的查找过程如下:在 Y 路径下,require(X)
Y
require(X)
如果X是内置模块(http, fs, path 等), 直接返回内置模块,不再执行
如果 X 以 '/' 开头,把 Y 设置为文件系统根目录
如果 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)
搜索 NODE_MODULE,若存在就返回模块 a. 从路径 Y 开始,一层层往上找,尝试加载(路径 + 'node_modules/' + X) b. 在 GLOBAL_FOLDERS node_modules 目录中查找 X
抛出 "Not Found" Error 复制代码例如在 /Users/helkyle/projects/learning-module/foo.js` 中 require('bar') 将会从`/Users/helkyle/projects/learning-module/ 开始逐层往上查找bar 模块(不是以 './', '/', '../' 开头)。
/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 所在的路径。举个例子,假设有两个模块。
npm link
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我之前踩过的坑
foo
link bar
/usr/lib/foo/node_modules/bar
/usr/lib/bar
require('quux')
/usr/lib/bar/node_modules/
/usr/lib/foo/node_modules
在实践过程中能了解到,实际上 Node module require 的过程会有缓存。也就是两次 require 同一个 module会得到一样的结果。
Node module require
const a1 = require('./a.js');const a2 = require('./a.js');
执行 node b.js,可以看到,第二次 require a.js 跟第一次 require 得到的是相同的模块引用。从源码上看,require 是对 module 常用方法的封装。
node b.js
require a.js
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 的过程发生了什么?
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 看看。
module cache
key
Module._cache
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。
a.js, b.js
value
require('./a.js')
require.cache['/Users/helkyle/projects/learning-module/a.js'].exports
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 是 Facebook 开源的前端测试库,提供了很多非常强大又实用的功能。mock module 是其中非常抢眼的特性。使用方式是在需要被 mock 的文件模块同级目录下的 __mock__ 文件夹添加同名文件,执行测试代码时运行 jest.mock(modulePath),jest 会自动加载 mock 版本的 module。举个例子,项目中有个 apis 文件,提供对接后端 api。
jest
mock module
__mock__
jest.mock(modulePath),jest
mock
getUsers: () => fetch('api/users')
在跑测试过程中,不希望它真的连接后端请求。这时候根据 jest 文档,在 apis 文件同级目录创建 mock file
mock file
测试文件中,主动调用 jest.mock('./apis.js') 即可。
const apis = require('./apis.js');
了解 require 的基础原理之后,我们也来实现类似的功能,将加载 api.js 的语句改写成加载 mock/api.js。
由于缓存机制的存在,提前写入目标缓存,再次 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');
基于 require.cache 的方式,需要提前 require mock module。????提到了,由于最终都是通过 Module._load来加载模块,在这个位置进行拦截即可完成按需 mock。
require mock module
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 有出入。比如在 jest 中 require module 并不会写入 require.cache。
Jest
require module
查阅 Node 文档发现,在 Command Line 章节也有一个 --require ,使用这个参数可以在执行业务代码之前预先加载特定模块。举个例子,编写 setup 文件,往 global 对象上挂载 it, assert 等方法。
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 不用在测试文件中引入。
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
The text was updated successfully, but these errors were encountered:
No branches or pull requests
作为前端开发者,不可避免每天都要跟
Node.js
打交道。Node
遵循Commonjs
规范,规范的核心是通过require
来加载依赖的其他模块。我们已经常习惯于使用社区提供的各种库,但对于模块引用的背后原理知之甚少。这篇文章通过源码阅读,浅析在commonjs
规范中require
背后的工作原理。require 从哪里来?
大家都知道,在
node js
的模块 / 文件中,有些 “全局” 变量是可以直接使用的,比如require, module, __dirname, __filename, exports
。其实这些变量或方法并不是 “全局” 的,而是在commonjs
模块加载中, 通过包裹的形式,提供的局部变量。经过
compile
之后,就有了module
,__dirname
等变量可以直接使用。这也可以很好解答初学者常常会困惑的问题,为什么给
exports
赋值,require
之后得到的结果是undefined?
直接赋值只是修改了局部变脸
exports
的值。最终export
出去的module.exports
没有被赋值。require 的查找过程
文档中描述得非常清楚,简化版
require
模块的查找过程如下:在Y
路径下,require(X)
如果X是内置模块(http, fs, path 等), 直接返回内置模块,不再执行
如果 X 以 '/' 开头,把 Y 设置为文件系统根目录
如果 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)
搜索 NODE_MODULE,若存在就返回模块 a. 从路径 Y 开始,一层层往上找,尝试加载(路径 + 'node_modules/' + X) b. 在 GLOBAL_FOLDERS node_modules 目录中查找 X
抛出 "Not Found" Error 复制代码例如在
/Users/helkyle/projects/learning-module/foo.js` 中 require('bar') 将会从`/Users/helkyle/projects/learning-module/
开始逐层往上查找bar
模块(不是以'./', '/', '../'
开头)。需要注意的是,在使用
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
会得到一样的结果。执行
node b.js
,可以看到,第二次require a.js
跟第一次require
得到的是相同的模块引用。从源码上看,require
是对module
常用方法的封装。跟踪代码看到,
require()
最终调用的是Module._load
方法:// 忽略代码,看看load
的过程发生了什么?到这里,
module cache
的原理也很清晰,模块在首次加载后,会以模块绝对路径为key
缓存在Module._cache
属性上,再次require
时会直接返回已缓存的结果以提高 效率。在控制台打印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
。应用——实现 Jest 的 mock module 效果
jest
是 Facebook 开源的前端测试库,提供了很多非常强大又实用的功能。mock module
是其中非常抢眼的特性。使用方式是在需要被 mock 的文件模块同级目录下的__mock__
文件夹添加同名文件,执行测试代码时运行jest.mock(modulePath),jest
会自动加载mock
版本的module
。举个例子,项目中有个 apis 文件,提供对接后端 api。在跑测试过程中,不希望它真的连接后端请求。这时候根据 jest 文档,在 apis 文件同级目录创建
mock file
测试文件中,主动调用 jest.mock('./apis.js') 即可。
了解
require
的基础原理之后,我们也来实现类似的功能,将加载 api.js 的语句改写成加载 mock/api.js。使用 require.cache
由于缓存机制的存在,提前写入目标缓存,再次 require 将得到我们期望的结果。
魔改 module._load
基于
require.cache
的方式,需要提前require mock module
。????提到了,由于最终都是通过Module._load
来加载模块,在这个位置进行拦截即可完成按需mock
。注意:以上内容仅供参考。从实际运行结果上看,
Jest
有自己实现的模块加载机制,跟commonjs
有出入。比如在jest
中require module
并不会写入require.cache
。程序启动时的
require
查阅
Node
文档发现,在Command Line
章节也有一个--require
,使用这个参数可以在执行业务代码之前预先加载特定模块。举个例子,编写setup
文件,往global
对象上挂载it
,assert
等方法。给启动代码添加
--require
参数。引入global.assert
,global.it
,就可以在代码中直接使用assert, it
不用在测试文件中引入。https://blog.csdn.net/xgangzai/article/details/108505416
The text was updated successfully, but these errors were encountered: