Dieser Artikel führt Sie durch das Modulsystem in Nodejs. Ich hoffe, er wird Ihnen hilfreich sein!
Früher wurde JavaScript verwendet, um einfache Seiteninteraktionslogik zu implementieren, aber mit der Entwicklung der Zeit konnten Browser nicht nur einfache Interaktionen darstellen, sondern auch verschiedene Websites begannen zu glänzen. Da Websites immer komplexer werden und die Zahl der Frontend-Codes im Vergleich zu anderen statischen Sprachen zunimmt, wird die mangelnde Modularität von JavaScript deutlich, beispielsweise durch Namenskonflikte. Um die Wartung und Verwaltung des Front-End-Codes zu erleichtern, begann die Community daher, modulare Spezifikationen zu definieren. In diesem Prozess sind viele modulare Spezifikationen entstanden, wie z. B. CommonJS
, AMD
, CMD
, ES-Module
usw Der Artikel erläutert hauptsächlich die in Node
implementierte Modularisierung basierend auf CommonJS
. CommonJS
, AMD
, CMD
, ES modules
,本文章主要讲解Node
中根据CommonJS
实现的模块化。
首先,在 Node 世界里,模块系统是遵守CommonJS
规范的,CommonJS
规范里定义,简单讲就是:
module
对象来代表一个模块的信息exports
用来导出模块对外暴露的信息require
来引用一个模块我们上面说过,一个文件就是一个模块,且通过一个 module 对象来描述当前模块信息,一个 module 对象对应有以下属性: - id: 当前模块的id - path: 当前模块对应的路径 - exports: 当前模块对外暴露的变量 - parent: 也是一个module对象,表示当前模块的父模块,即调用当前模块的模块 - filename: 当前模块的文件名(绝对路径), 可用于在模块引入时将加载的模块加入到全局模块缓存中, 后续引入直接从缓存里进行取值 - loaded: 表示当前模块是否加载完毕 - children: 是一个数组,存放着当前模块调用的模块 - paths: 是一个数组,记录着从当前模块开始查找node_modules目录,递归向上查找到根目录下的node_modules目录下
说完CommonJS
规范,我们先讲下module.exports
与exports
的区别。
首先,我们用个新模块进行一个简单验证
console.log(module.exports === exports); // true
可以发现,module.exports
和epxorts
实际上就是指向同一个引用变量。
demo1
// a模块 module.exports.text = 'xxx'; exports.value = 2; // b模块代码 let a = require('./a'); console.log(a); // {text: 'xxx', value: 2}
从而也就验证了上面demo1
中,为什么通过module.exports
和exports
添加属性,在模块引入时两者都存在, 因为两者最终都是往同一个引用变量上面进行属性的添加.根据该 demo, 可以得出结论: module.exports
和exports
指向同一个引用变量
demo2
// a模块 module.exports = { text: 'xxx' } exports.value = 2; // b模块代码 let a = require('./a'); console.log(a); // {text: 'xxx'}
上面的 demo 例子中,对module.exports
进行了重新赋值, exports
进行了属性的新增, 但是在引入模块后最终导出的是module.exports
定义的值, 可以得出结论: noed 的模块最终导出的是module.exports
, 而exports
仅仅是对 module.exports 的引用, 类似于以下代码:
exports = module.exports = {}; (function (exports, module) { // a模块里面的代码 module.exports = { text: 'xxx' } exports.value = 2; console.log(module.exports === exports); // false })(exports, module)
由于在函数执行中, exports 仅是对原module.exports
对应变量的一个引用,当对module.exports
进行赋值时,exports
对应的变量和最新的module.exports
并不是同一个变量
require
CommonJS
-Spezifikation, CommonJS</ code>-Spezifikation Definition, einfach ausgedrückt: 🎜<ul><li>Jede Datei ist ein Modul</li><li>Die Informationen eines Moduls werden durch das <code>module
-Objekt< dargestellt li>Verwenden Sie exports
, um vom Modul bereitgestellte Informationen zu exportierenrequire
, um auf ein Modul zu verweisenCommonJS
-Spezifikation gesprochen haben, sprechen wir zunächst über den Unterschied zwischen module.exports
und exports
. 🎜🎜Zuerst verwenden wir ein neues Modul, um eine einfache Überprüfung durchzuführen🎜Module.prototype.require = function(id) { // ... try { // 主要通过Module的静态方法_load加载模块 return Module._load(id, this, /* isMain */ false); } finally {} // ... }; // ... Module._load = function(request, parent, isMain) { let relResolveCacheIdentifier; // ... // 解析文件路径成绝对路径 const filename = Module._resolveFilename(request, parent, isMain); // 查看当前需要加载的模块是否已经有缓存 const cachedModule = Module._cache[filename]; // 如果有缓存, 则直接使用缓存的即可 if (cachedModule !== undefined) { // ... return cachedModule.exports; } // 查看是否是node自带模块, 如http,fs等, 是就直接返回 const mod = loadNativeModule(filename, request); if (mod && mod.canBeRequiredByUsers) return mod.exports; // 根据文件路径初始化一个模块 const module = cachedModule || new Module(filename, parent); // ... // 将该模块加入模块缓存中 Module._cache[filename] = module; if (parent !== undefined) { relativeResolveCache[relResolveCacheIdentifier] = filename; } // ... // 进行模块的加载 module.load(filename); return module.exports; };
module.exports
und epxorts
tatsächlich auf dieselbe Referenzvariable verweisen. 🎜🎜🎜demo1🎜🎜Module.prototype._compile = function(content, filename) { // ... const compiledWrapper = wrapSafe(filename, content, this); // result = compiledWrapper.call(thisValue, exports, require, module, filename, dirname); // ... return result; };
demo1
oben, warum Attribute über module.exports
und exports
im Modul hinzugefügt werden Beide existieren wenn sie eingeführt werden, da beide letztendlich Attribute zu derselben Referenzvariablen hinzufügen. Basierend auf dieser Demo können wir die Schlussfolgerung ziehen: module.exports
und exports</ code> zeigen auf dasselbe Referenzvariable🎜🎜🎜demo2🎜🎜<div class="code" style="position:relative; padding:0px; margin:0px;"><div class="code" style="position:relative; padding:0px; margin:0px;"><pre class="brush:js;toolbar:false;">function wrapSafe(filename, content, cjsModuleInstance) {
// ...
const wrapper = Module.wrap(content);
// ...
}
let wrap = function(script) {
return Module.wrapper[0] + script + Module.wrapper[1];
};
const wrapper = [
&#39;(function (exports, require, module, __filename, __dirname) { &#39;,
&#39;\n});&#39;
];
ObjectDefineProperty(Module, &#39;wrap&#39;, {
get() {
return wrap;
},
set(value) {
patched = true;
wrap = value;
}
});</pre><div class="contentsignin">Nach dem Login kopieren</div></div><div class="contentsignin">Nach dem Login kopieren</div></div>🎜Im obigen Demo-Beispiel wird <code>module.exports
neu zugewiesen und exports
weist neue Attribute auf, aber nachdem das Modul eingeführt wurde, Der durch module.exports
definierte Wert wird schließlich exportiert. Daraus kann geschlossen werden, dass das noed-Modul schließlich module.exports
exportiert, während < code>exports einfach ist ein Verweis auf module.exports, ähnlich dem folgenden Code: 🎜// test.mjs export default { a: 'xxx' } // import.js import a from './test.mjs'; console.log(a); // {a: 'xxx'}
module.exports
, wenn < ein Wert zugewiesen wird code>module.exports, die Variable, die exports
entspricht, ist nicht dieselbe Variable wie die neueste module.exports
🎜require
Der Prozess der Einführung eines Moduls ist hauptsächlich in die folgenden Schritte unterteilt:🎜.js
,.json
, .node
后缀的文件)Module.prototype.require = function(id) { // ... try { // 主要通过Module的静态方法_load加载模块 return Module._load(id, this, /* isMain */ false); } finally {} // ... }; // ... Module._load = function(request, parent, isMain) { let relResolveCacheIdentifier; // ... // 解析文件路径成绝对路径 const filename = Module._resolveFilename(request, parent, isMain); // 查看当前需要加载的模块是否已经有缓存 const cachedModule = Module._cache[filename]; // 如果有缓存, 则直接使用缓存的即可 if (cachedModule !== undefined) { // ... return cachedModule.exports; } // 查看是否是node自带模块, 如http,fs等, 是就直接返回 const mod = loadNativeModule(filename, request); if (mod && mod.canBeRequiredByUsers) return mod.exports; // 根据文件路径初始化一个模块 const module = cachedModule || new Module(filename, parent); // ... // 将该模块加入模块缓存中 Module._cache[filename] = module; if (parent !== undefined) { relativeResolveCache[relResolveCacheIdentifier] = filename; } // ... // 进行模块的加载 module.load(filename); return module.exports; };
至此, node 的模块原理流程基本过完了。目前 node v13.2.0 版本起已经正式支持 ESM 特性。
在接触 node 中,你是否会困惑 __filename
, __dirname
是从哪里来的, 为什么会有这些变量呢? 仔细阅读该章节,你会对这些有系统性的了解。
Module.prototype._compile = function(content, filename) { // ... const compiledWrapper = wrapSafe(filename, content, this); // result = compiledWrapper.call(thisValue, exports, require, module, filename, dirname); // ... return result; };
function wrapSafe(filename, content, cjsModuleInstance) { // ... const wrapper = Module.wrap(content); // ... } let wrap = function(script) { return Module.wrapper[0] + script + Module.wrapper[1]; }; const wrapper = [ '(function (exports, require, module, __filename, __dirname) { ', '\n});' ]; ObjectDefineProperty(Module, 'wrap', { get() { return wrap; }, set(value) { patched = true; wrap = value; } });
综上, 也就是之所以模块里面有__dirname
,__filename
, module
, exports
, require
这些变量, 其实也就是 node 在执行过程传入的, 看完是否解决了多年困惑的问题^_^
package.json
增加"type": "module"配置// test.mjs export default { a: 'xxx' } // import.js import a from './test.mjs'; console.log(a); // {a: 'xxx'}
较明显的区别是在于执行时机:
import
导入的模块会先进行预解析处理, 先于模块内的其他模块执行// entry.js console.log('execute entry'); let a = require('./a.js') console.log(a); // a.js console.log('-----a--------'); module.exports = 'this is a'; // 最终输出顺序为: // execute entry // -----a-------- // this is a
// entry.js console.log('execute entry'); import b from './b.mjs'; console.log(b); // b.mjs console.log('-----b--------'); export default 'this is b'; // 最终输出顺序为: // -----b-------- // execute entry // this is b
if
代码块中),如果需要动态引入, 需要使用import()
动态加载;ES 模块对比 CommonJS 模块, 还有以下的区别:
没有 require
、exports
或 module.exports
在大多数情况下,可以使用 ES 模块 import 加载 CommonJS 模块。(CommonJS 模块文件后缀为 cjs)
如果需要引入.js
后缀的 CommonJS 模块, 可以使用module.createRequire()
在 ES 模块中构造require
函数
// test.cjs export default { a: 'xxx' } // import.js import a from './test.cjs'; console.log(a); // {a: 'xxx'}
// test.cjs export default { a: 'xxx' } // import.js import a from './test.cjs'; console.log(a); // {a: 'xxx'}
// test.cjs export default { a: 'xxx' } // import.mjs import { createRequire } from 'module'; const require = createRequire(import.meta.url); // test.js 是 CommonJS 模块。 const siblingModule = require('./test'); console.log(siblingModule); // {a: 'xxx'}
没有 __filename 或 __dirname
这些 CommonJS 变量在 ES 模块中不可用。
没有 JSON 模块加载
JSON 导入仍处于实验阶段,仅通过 --experimental-json-modules 标志支持。
没有 require.resolve
没有 NODE_PATH
没有 require.extensions
没有 require.cache
在 CommonJS 中引入 ES 模块
由于 ES Modules 的加载、解析和执行都是异步的,而 require() 的过程是同步的、所以不能通过 require() 来引用一个 ES6 模块。
ES6 提议的 import() 函数将会返回一个 Promise,它在 ES Modules 加载后标记完成。借助于此,我们可以在 CommonJS 中使用异步的方式导入 ES Modules:
// b.mjs export default 'esm b' // entry.js (async () => { let { default: b } = await import('./b.mjs'); console.log(b); // esm b })()
在 ES 模块中引入 CommonJS
在 ES6 模块里可以很方便地使用 import 来引用一个 CommonJS 模块,因为在 ES6 模块里异步加载并非是必须的:
// a.cjs module.exports = 'commonjs a'; // entry.js import a from './a.cjs'; console.log(a); // commonjs a
至此,提供 2 个 demo 给大家测试下上述知识点是否已经掌握,如果没有掌握可以回头再进行阅读。
demo module.exports&exports
// a模块 exports.value = 2; // b模块代码 let a = require('./a'); console.log(a); // {value: 2}
demo module.exports&exports
// a模块 exports = 2; // b模块代码 let a = require('./a'); console.log(a); // {}
require&_cache 模块缓存机制
// origin.js let count = 0; exports.addCount = function () { count++ } exports.getCount = function () { return count; } // b.js let { getCount } = require('./origin'); exports.getCount = getCount; // a.js let { addCount, getCount: getValue } = require('./origin'); addCount(); console.log(getValue()); // 1 let { getCount } = require('./b'); console.log(getCount()); // 1
根据上述例子, 模块在 require 引入时会加入缓存对象require.cache
中。 如果需要删除缓存, 可以考虑将该缓存内容清除,则下次require
模块将会重新加载模块。
let count = 0; exports.addCount = function () { count++ } exports.getCount = function () { return count; } // b.js let { getCount } = require('./origin'); exports.getCount = getCount; // a.js let { addCount, getCount: getValue } = require('./origin'); addCount(); console.log(getValue()); // 1 delete require.cache[require.resolve('./origin')]; let { getCount } = require('./b'); console.log(getCount()); // 0
至此,本文主要介绍了 Node 中基于CommonJS
实现的模块化机制,并且通过源码的方式对模块化的整个流程进行了分析,有关于模块的介绍可查看下面参考资料。有疑问的欢迎评论区留言,谢谢。
Ursprüngliche Adresse: https://juejin.cn/post/7007233910681632781
Autor: Langsam essen
Weitere Programmierkenntnisse finden Sie unter: Programmiervideo! !
Das obige ist der detaillierte Inhalt vonVerstehen Sie schnell das Modulsystem in Nodejs in einem Artikel. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!