Heim  >  Artikel  >  Web-Frontend  >  Verstehen Sie schnell das Modulsystem in Nodejs in einem Artikel

Verstehen Sie schnell das Modulsystem in Nodejs in einem Artikel

青灯夜游
青灯夜游nach vorne
2021-09-14 10:32:102454Durchsuche

Dieser Artikel führt Sie durch das Modulsystem in Nodejs. Ich hoffe, er wird Ihnen hilfreich sein!

Verstehen Sie schnell das Modulsystem in Nodejs in einem Artikel

Modularer Hintergrund

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实现的模块化。

CommonJS 规范

首先,在 Node 世界里,模块系统是遵守CommonJS规范的,CommonJS规范里定义,简单讲就是:

  • 每一个文件就是一个模块
  • 通过module对象来代表一个模块的信息
  • 通过exports用来导出模块对外暴露的信息
  • 通过require来引用一个模块

Node 模块分类

  • 核心模块: 如 fs,http,path 等模块, 这些模块不需要安装, 在运行时已经加载在内存中。【推荐学习:《nodejs 教程》】
  • 第三方模块: 通过安装存放在 node_modules 中。
  • 自定义模块: 主要是指 file 模块,通过绝对路径或者相对路径进行引入。

Module 对象

我们上面说过,一个文件就是一个模块,且通过一个 module 对象来描述当前模块信息,一个 module 对象对应有以下属性: - id: 当前模块的id - path: 当前模块对应的路径 - exports: 当前模块对外暴露的变量 - parent: 也是一个module对象,表示当前模块的父模块,即调用当前模块的模块 - filename: 当前模块的文件名(绝对路径), 可用于在模块引入时将加载的模块加入到全局模块缓存中, 后续引入直接从缓存里进行取值 - loaded: 表示当前模块是否加载完毕 - children: 是一个数组,存放着当前模块调用的模块 - paths: 是一个数组,记录着从当前模块开始查找node_modules目录,递归向上查找到根目录下的node_modules目录下

module.exports 与 exports

说完CommonJS规范,我们先讲下module.exportsexports的区别。

首先,我们用个新模块进行一个简单验证

console.log(module.exports === exports); // true

可以发现,module.exportsepxorts实际上就是指向同一个引用变量。

demo1

// a模块
module.exports.text = 'xxx';
exports.value = 2;

// b模块代码
let a = require('./a');

console.log(a); // {text: 'xxx', value: 2}

从而也就验证了上面demo1中,为什么通过module.exportsexports添加属性,在模块引入时两者都存在, 因为两者最终都是往同一个引用变量上面进行属性的添加.根据该 demo, 可以得出结论: module.exportsexports指向同一个引用变量

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 方法

require

CommonJS-Spezifikation🎜🎜🎜Zuallererst entspricht das Modulsystem in der Node-Welt der 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-ObjektVerwenden Sie exports, um vom Modul bereitgestellte Informationen zu exportieren
  • Verwenden Sie require, um auf ein Modul zu verweisen
  • 🎜Knotenmodulklassifizierung🎜🎜
    • Kernmodule: Module wie fs, http, path usw. Diese Module müssen nicht installiert werden und sind bereits in den Speicher geladen Laufzeit. [Empfohlenes Lernen: „nodejs Tutorial🎜"]
    • Module von Drittanbietern: werden durch die Installation in node_modules gespeichert.
    • Benutzerdefiniertes Modul: Bezieht sich hauptsächlich auf das Dateimodul, das über einen absoluten Pfad oder einen relativen Pfad eingeführt wird.

    🎜Modulobjekt🎜🎜🎜Wir haben oben gesagt, dass eine Datei ein Modul ist und ein Modulobjekt verwendet wird, um die aktuellen Modulinformationen zu beschreiben, a Modul Das Objekt entspricht den folgenden Eigenschaften: - id: die ID des aktuellen Moduls - Pfad: der Pfad, der dem aktuellen Modul entspricht - Exporte: Variablen, die vom aktuellen Modul bereitgestellt werden - parent: ist auch ein Modulobjekt, das das übergeordnete Modul des aktuellen Moduls angibt, dh das Modul, das das aktuelle Modul aufruft. - Dateiname: Der Dateiname des aktuellen Moduls (absoluter Pfad), der zum Hinzufügen des geladenen Moduls zum globalen Modulcache verwendet werden kann, wenn das Modul eingeführt wird, kann der Wert direkt aus dem Cache abgerufen werden. - geladen: zeigt an, ob das aktuelle Modul geladen wurde - Kinder: ist ein Array, das die vom aktuellen Modul aufgerufenen Module speichert - paths: ist ein Array, das die Suche nach dem Verzeichnis „node_modules“ beginnend mit dem aktuellen Modul aufzeichnet und rekursiv nach oben zum Verzeichnis „node_modules“ unter dem Stammverzeichnis sucht🎜

    🎜module.exports und exports🎜🎜🎜 Nachdem wir über die CommonJS-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;
    };
    🎜Wir können feststellen, dass 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;
    };
    🎜Also, in 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🎜🎜<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 = [ &amp;#39;(function (exports, require, module, __filename, __dirname) { &amp;#39;, &amp;#39;\n});&amp;#39; ]; ObjectDefineProperty(Module, &amp;#39;wrap&amp;#39;, { get() { return wrap; }, set(value) { patched = true; wrap = value; } });</pre>🎜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 exports einfach ist ein Verweis auf module.exports, ähnlich dem folgenden Code: 🎜
    // test.mjs
    export default {
    	a: &#39;xxx&#39;
    }
    // import.js
    import a from &#39;./test.mjs&#39;;
    
    console.log(a); // {a: &#39;xxx&#39;}
    🎜Aufgrund der Funktionsausführung ist exports nur ein Verweis auf die entsprechende Variable des ursprünglichen module.exports , wenn module.exports

    , die Variable, die exports entspricht, ist nicht dieselbe Variable wie die neueste module.exports🎜

    🎜require-Methode🎜🎜🎜requireDer Prozess der Einführung eines Moduls ist hauptsächlich in die folgenden Schritte unterteilt:🎜
    • 解析文件路径成绝对路径
    • 查看当前需要加载的模块是否已经有缓存, 如果有缓存, 则直接使用缓存的即可
    • 查看是否是 node 自带模块, 如 http,fs 等, 是就直接返回
    • 根据文件路径创建一个模块对象
    • 将该模块加入模块缓存中
    • 通过对应的文件解析方式对文件进行解析编译执行(node 默认仅支持解析.js,.json, .node后缀的文件)
    • 返回加载后的模块 exports 对象

    Verstehen Sie schnell das Modulsystem in Nodejs in einem Artikel

    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 特性。

    __filename, __dirname

    在接触 node 中,你是否会困惑 __filename, __dirname是从哪里来的, 为什么会有这些变量呢? 仔细阅读该章节,你会对这些有系统性的了解。

    • 顺着上面的 require 源码继续走, 当一个模块加载时, 会对模块内容读取
    • 将内容包裹成函数体
    • 将拼接的函数字符串编译成函数
    • 执行编译后的函数, 传入对应的参数
    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 = [
      &#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;
      }
    });

    综上, 也就是之所以模块里面有__dirname,__filename, module, exports, require这些变量, 其实也就是 node 在执行过程传入的, 看完是否解决了多年困惑的问题^_^

    NodeJS 中使用 ES Modules

    • package.json增加"type": "module"配置
    // test.mjs
    export default {
    	a: &#39;xxx&#39;
    }
    // import.js
    import a from &#39;./test.mjs&#39;;
    
    console.log(a); // {a: &#39;xxx&#39;}

    import 与 require 两种机制的区别

    较明显的区别是在于执行时机:

    • ES 模块在执行时会将所有import导入的模块会先进行预解析处理, 先于模块内的其他模块执行
    // entry.js
    console.log(&#39;execute entry&#39;);
    let a = require(&#39;./a.js&#39;)
    
    console.log(a);
    
    // a.js
    console.log(&#39;-----a--------&#39;);
    
    module.exports = &#39;this is a&#39;;
    // 最终输出顺序为:
    // execute entry
    // -----a--------
    // this is a
    // entry.js
    console.log(&#39;execute entry&#39;);
    import b from &#39;./b.mjs&#39;;
    
    console.log(b);
    
    // b.mjs
    console.log(&#39;-----b--------&#39;);
    
    export default &#39;this is b&#39;;
    // 最终输出顺序为:
    // -----b--------
    // execute entry
    // this is b
    • import 只能在模块的顶层,不能在代码块之中(比如在if代码块中),如果需要动态引入, 需要使用import()动态加载;

    ES 模块对比 CommonJS 模块, 还有以下的区别:

    • 没有 requireexportsmodule.exports

      在大多数情况下,可以使用 ES 模块 import 加载 CommonJS 模块。(CommonJS 模块文件后缀为 cjs) 如果需要引入.js后缀的 CommonJS 模块, 可以使用module.createRequire()在 ES 模块中构造require函数

    // test.cjs
    export default {
    a: &#39;xxx&#39;
    }
    // import.js
    import a from &#39;./test.cjs&#39;;
    
    console.log(a); // {a: &#39;xxx&#39;}
    // test.cjs
    export default {
    a: &#39;xxx&#39;
    }
    // import.js
    import a from &#39;./test.cjs&#39;;
    
    console.log(a); // {a: &#39;xxx&#39;}
    // test.cjs
    export default {
    a: &#39;xxx&#39;
    }
    // import.mjs
    import { createRequire } from &#39;module&#39;;
    const require = createRequire(import.meta.url);
    
    // test.js 是 CommonJS 模块。
    const siblingModule = require(&#39;./test&#39;);
    console.log(siblingModule); // {a: &#39;xxx&#39;}
    • 没有 __filename 或 __dirname

      这些 CommonJS 变量在 ES 模块中不可用。

    • 没有 JSON 模块加载

      JSON 导入仍处于实验阶段,仅通过 --experimental-json-modules 标志支持。

    • 没有 require.resolve

    • 没有 NODE_PATH

    • 没有 require.extensions

    • 没有 require.cache

    ES 模块和 CommonJS 的相互引用

    在 CommonJS 中引入 ES 模块

    由于 ES Modules 的加载、解析和执行都是异步的,而 require() 的过程是同步的、所以不能通过 require() 来引用一个 ES6 模块。

    ES6 提议的 import() 函数将会返回一个 Promise,它在 ES Modules 加载后标记完成。借助于此,我们可以在 CommonJS 中使用异步的方式导入 ES Modules:

    // b.mjs
    export default &#39;esm b&#39;
    // entry.js
    (async () => {
    	let { default: b } = await import(&#39;./b.mjs&#39;);
    	console.log(b); // esm b
    })()

    在 ES 模块中引入 CommonJS

    在 ES6 模块里可以很方便地使用 import 来引用一个 CommonJS 模块,因为在 ES6 模块里异步加载并非是必须的:

    // a.cjs
    module.exports = &#39;commonjs a&#39;;
    // entry.js
    import a from &#39;./a.cjs&#39;;
    
    console.log(a); // commonjs a

    至此,提供 2 个 demo 给大家测试下上述知识点是否已经掌握,如果没有掌握可以回头再进行阅读。

    demo module.exports&exports

    // a模块
    exports.value = 2;
    
    // b模块代码
    let a = require(&#39;./a&#39;);
    
    console.log(a); // {value: 2}

    demo module.exports&exports

    // a模块
    exports = 2;
    
    // b模块代码
    let a = require(&#39;./a&#39;);
    
    console.log(a); // {}

    require&_cache 模块缓存机制

    // origin.js
    let count = 0;
    
    exports.addCount = function () {
    	count++
    }
    
    exports.getCount = function () {
    	return count;
    }
    
    // b.js
    let { getCount } = require(&#39;./origin&#39;);
    exports.getCount = getCount;
    
    // a.js
    let { addCount, getCount: getValue } = require(&#39;./origin&#39;);
    addCount();
    console.log(getValue()); // 1
    let { getCount } = require(&#39;./b&#39;);
    console.log(getCount()); // 1

    require.cache

    根据上述例子, 模块在 require 引入时会加入缓存对象require.cache中。 如果需要删除缓存, 可以考虑将该缓存内容清除,则下次require模块将会重新加载模块。

    let count = 0;
    
    exports.addCount = function () {
    	count++
    }
    
    exports.getCount = function () {
    	return count;
    }
    
    // b.js
    let { getCount } = require(&#39;./origin&#39;);
    exports.getCount = getCount;
    
    // a.js
    let { addCount, getCount: getValue } = require(&#39;./origin&#39;);
    addCount();
    console.log(getValue()); // 1
    delete require.cache[require.resolve(&#39;./origin&#39;)];
    let { getCount } = require(&#39;./b&#39;);
    console.log(getCount()); // 0

    结语

    至此,本文主要介绍了 Node 中基于CommonJS实现的模块化机制,并且通过源码的方式对模块化的整个流程进行了分析,有关于模块的介绍可查看下面参考资料。有疑问的欢迎评论区留言,谢谢。

    参考资料

    CommonJS 模块

    ES-Modul

    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!

    Stellungnahme:
    Dieser Artikel ist reproduziert unter:juejin.cn. Bei Verstößen wenden Sie sich bitte an admin@php.cn löschen
    Vorheriger Artikel:So verwenden Sie das es6-SetNächster Artikel:So verwenden Sie das es6-Set