Maison  >  Article  >  interface Web  >  Comprenez rapidement le système de modules dans Nodejs en un seul article

Comprenez rapidement le système de modules dans Nodejs en un seul article

青灯夜游
青灯夜游avant
2021-09-14 10:32:102362parcourir

Cet article vous fera découvrir le système de modules dans Nodejs J'espère qu'il vous sera utile !

Comprenez rapidement le système de modules dans Nodejs en un seul article

Arrière-plan modulaire

Au début, JavaScript était utilisé pour implémenter une logique d'interaction de page simple, mais avec l'évolution de l'époque, les navigateurs peuvent non seulement présenter des interactions simples, mais divers sites Web ont commencé à briller. À mesure que les sites Web commencent à devenir plus complexes et que les codes frontaux augmentent par rapport à d'autres langages statiques, le manque de modularité de JavaScript commence à être révélé, comme les conflits de noms. Par conséquent, afin de faciliter la maintenance et la gestion du code front-end, la communauté a commencé à définir des spécifications modulaires. Dans ce processus, de nombreuses spécifications modulaires ont émergé, telles que CommonJS, AMD, CMD, ES modules , ce L'article explique principalement la modularisation implémentée dans Node basée sur 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

Spécification CommonJS🎜🎜🎜Tout d'abord, dans le monde Node, le système de modules est conforme à la spécification CommonJS, CommonJS code> définition de la spécification, en termes simples : 🎜<ul> <li>Chaque fichier est un module</li> <li>Les informations d'un module sont représentées par l'objet <code>module Utilisez exports pour exporter les informations exposées par le module
  • Utilisez require pour référencer un module
  • 🎜Classification des modules de nœuds🎜🎜
    • Modules de base : modules tels que fs, http, path, etc. Ces modules n'ont pas besoin d'être installés et sont déjà chargés dans la mémoire à exécution. [Apprentissage recommandé : "tutoriel nodejs🎜"]
    • Modules tiers : stockés dans node_modules lors de l'installation.
    • Module personnalisé : fait principalement référence au module de fichier, qui est introduit via un chemin absolu ou un chemin relatif.

    🎜Objet module🎜🎜🎜Nous avons dit plus haut qu'un fichier est un module, et un objet module est utilisé pour décrire les informations actuelles du module, un module L'objet correspond aux propriétés suivantes : - id : l'identifiant du module actuel - path : le chemin correspondant au module courant - exports : variables exposées par le module courant - parent : est également un objet module, indiquant le module parent du module courant, c'est-à-dire le module qui appelle le module courant - nom de fichier : le nom de fichier du module actuel (chemin absolu), qui peut être utilisé pour ajouter le module chargé au cache global du module lorsque les introductions ultérieures peuvent obtenir directement la valeur du cache. - chargé : indique si le module actuel a été chargé - children : est un tableau qui stocke les modules appelés par le module courant - paths : est un tableau, enregistrant la recherche du répertoire node_modules à partir du module actuel, et recherchant récursivement vers le haut jusqu'au répertoire node_modules sous le répertoire racine🎜

    🎜module.exports et exports🎜🎜🎜 Après avoir parlé de la spécification CommonJS, parlons d'abord de la différence entre module.exports et exports. 🎜🎜Tout d'abord, nous utilisons un nouveau module pour effectuer une vérification simple🎜
    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;
    };
    🎜Nous pouvons constater que module.exports et epxorts pointent en fait vers la même variable de référence. 🎜🎜🎜demo1🎜🎜
    Module.prototype._compile = function(content, filename) {
      // ...
      const compiledWrapper = wrapSafe(filename, content, this);
      // 
      result = compiledWrapper.call(thisValue, exports, require, module,
                                        filename, dirname);
      
      // ...
      return result;
    };
    🎜Ainsi, dans demo1 ci-dessus, pourquoi les attributs sont-ils ajoutés via module.exports et exports dans le module Les deux existent lorsqu'ils sont introduits, car tous deux ajoutent finalement des attributs à la même variable de référence. Sur la base de cette démo, nous pouvons tirer la conclusion : module.exports et exports code> pointent vers la même chose. variable de référence🎜🎜🎜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>🎜Dans l'exemple de démonstration ci-dessus, <code>module.exports est réaffecté et exports a de nouveaux attributs augmentés, mais après l'introduction du module. , la valeur définie par module.exports est finalement exportée. On peut en conclure que le module noed exporte finalement module.exports, tandis que exports l'est. juste une référence à module.exports, similaire au code suivant : 🎜
    // test.mjs
    export default {
    	a: &#39;xxx&#39;
    }
    // import.js
    import a from &#39;./test.mjs&#39;;
    
    console.log(a); // {a: &#39;xxx&#39;}
    🎜En raison de l'exécution de la fonction, exports n'est qu'une référence à la variable correspondante du module.exports d'origine, lors de l'attribution d'une valeur à module.exports, la variable correspondant à exports n'est pas la même variable que le dernier module.exports🎜

    🎜méthode require🎜🎜🎜requireLe processus d'introduction d'un module est principalement divisé en les étapes suivantes :🎜
    • 解析文件路径成绝对路径
    • 查看当前需要加载的模块是否已经有缓存, 如果有缓存, 则直接使用缓存的即可
    • 查看是否是 node 自带模块, 如 http,fs 等, 是就直接返回
    • 根据文件路径创建一个模块对象
    • 将该模块加入模块缓存中
    • 通过对应的文件解析方式对文件进行解析编译执行(node 默认仅支持解析.js,.json, .node后缀的文件)
    • 返回加载后的模块 exports 对象

    Comprenez rapidement le système de modules dans Nodejs en un seul article

    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 模块

    Module ES

    Adresse originale : https://juejin.cn/post/7007233910681632781

    Auteur : Mangez lentement

    Pour plus de connaissances liées à la programmation, veuillez visiter : Vidéo de programmation ! !

    Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

    Déclaration:
    Cet article est reproduit dans:. en cas de violation, veuillez contacter admin@php.cn Supprimer