Maison  >  Article  >  interface Web  >  Cet article vous amènera à comprendre le système de modules dans node

Cet article vous amènera à comprendre le système de modules dans node

青灯夜游
青灯夜游avant
2023-01-14 19:09:482461parcourir

Cet article vous aidera à apprendre Node et à comprendre le système de modules dans Node d'une manière simple et facile. J'espère qu'il vous sera utile !

Cet article vous amènera à comprendre le système de modules dans node

J'ai écrit un article il y a deux ans pour présenter le système de modules : Comprendre le concept du module front-end : CommonJs et ES6Module. Les connaissances contenues dans cet article s'adressent aux débutants et sont relativement simples. Je voudrais également corriger quelques erreurs dans l'article :

  • [Module] et [Module System] sont deux choses différentes. Un module est une unité dans un logiciel et un système de modules est un ensemble de syntaxes ou d'outils. Le système de modules permet aux développeurs de définir et d'utiliser des modules dans des projets.
  • L'abréviation de ECMAScript Module est ESM, ou ESModule, et non ES6Module.

Les connaissances de base sur le système de modules sont presque couvertes dans l'article précédent, donc cet article se concentrera sur les principes internes du système de modules et une introduction plus complète les différences entre les différents systèmes de modules, ci-dessus Le contenu qui apparaît dans un article ne sera pas répété ici. [Recommandations de didacticiels connexes : Tutoriel vidéo Nodejs, Enseignement de la programmation]

Système de modules

Tous les langages de programmation n'ont pas de système de modules intégré. Il n'y a pas eu de système de modules pendant longtemps après la naissance. de JavaScript.

Dans l'environnement du navigateur, vous ne pouvez utiliser la balise <script> que pour introduire les fichiers de code inutilisés. Cette méthode partage une portée globale, dont on peut dire qu'elle est pleine de problèmes couplée au développement rapide du front-end, cette méthode non ; ne répond plus aux besoins actuels. Avant l'apparition du système de modules officiel, la communauté front-end a créé son propre système de modules tiers. Les plus couramment utilisés sont : la définition de module asynchrone AMD, la définition de module universelle UMD, etc. l'un est CommonJS.

Étant donné que Node.js est un environnement d'exécution JavaScript, il peut accéder directement au système de fichiers sous-jacent. Les développeurs l'ont donc adopté et implémenté un système de modules conformément aux spécifications CommonJS.

Au début, CommonJS ne pouvait être utilisé que sur la plateforme Node.js. Avec l'émergence d'outils de packaging de modules tels que Browserify et Webpack, CommonJS peut enfin fonctionner côté navigateur.

Ce n'est qu'à la sortie de la spécification ECMAScript6 en 2015 qu'il existe une norme formelle pour le système de modules. Le système de modules construit selon cette norme s'appelle Module ECMAScripten abrégé [ESM]. a commencé à unifier l'environnement Node.js et l'environnement du navigateur. Bien entendu, ECMAScript6 ne fournit que la syntaxe et la sémantique. Quant à la mise en œuvre, il appartient aux différents fournisseurs de services de navigateur et aux développeurs de Node de travailler dur. C'est pourquoi nous avons l'artefact babel qui fait l'envie des autres langages de programmation. Implémenter un système de modules n'est pas une tâche facile Node.js est relativement stable dans la prise en charge d'ESM uniquement dans la version 13.2.

Mais quoi qu’il en soit, ESM est le « fils » de JavaScript, et il n’y a rien de mal à l’apprendre !

L'idée de base du système de modules

À l'ère de l'agriculture sur brûlis, JavaScript était utilisé pour développer des applications. Les fichiers de script ne peuvent être introduits que via des balises de script. L'un des problèmes les plus sérieux rencontrés est l'absence de mécanisme d'espace de noms, ce qui signifie que chaque script partage la même portée. Il existe une meilleure solution à ce problème dans la communauté : Module Revevaling

const myModule = (() => {
    const _privateFn = () => {}
    const _privateAttr = 1
    return {
        publicFn: () => {},
        publicAttr: 2
    }
})()

console.log(myModule)
console.log(myModule.publicFn, myModule._privateFn)

Les résultats en cours d'exécution sont les suivants :

Cet article vous amènera à comprendre le système de modules dans node

Ce mode est très simple. Utilisez IIFE pour créer une portée privée et utilisez return. pour l'exposer. Les variables internes (telles que _privateFn, _privateAttr) ne sont pas accessibles depuis la portée extérieure.

[module de révélation] profite de ces fonctionnalités pour masquer les informations privées et exporter les API qui devraient être exposées au monde extérieur. Le système de modules ultérieur est également développé sur la base de cette idée.

CommonJS

Sur la base de l'idée ci-dessus, développez un chargeur de module.

Écrivez d'abord une fonction qui charge le contenu du module, enveloppez cette fonction dans une portée privée, puis évaluez-la via eval() pour exécuter la fonction :

function loadModule (filename, module, require) {
  const wrappedSrc = 
    `(function (module, exports, require) {
      ${fs.readFileSync(filename, 'utf8)}
    }(module, module.exports, require)`
  eval(wrappedSrc)
}

Identique à [module révélateur], mettez le code source de le module Enveloppé dans une fonction, la différence est qu'une série de variables (module, module.exports, require) sont également transmises à la fonction.

Il est à noter que le contenu du module est lu via [readFileSync]. De manière générale, vous ne devez pas utiliser la version synchronisée lors de l'appel d'API impliquant le système de fichiers. Mais cette fois, c'est différent, car le chargement des modules via le système CommonJs lui-même doit être implémenté comme une opération synchrone pour garantir que plusieurs modules peuvent être introduits dans le bon ordre de dépendance.

接着模拟require()函数,主要功能是加载模块。

function require(moduleName) {
  const id = require.resolve(moduleName)
  if (require.cache[id]) {
    return require.cache[id].exports
  }
  // 模块的元数据
  const module = {
    exports: {},
    id
  }
  // 更新缓存
  require.cache[id] = module
  
  // 载入模块
  loadModule(id, module, require)
  
  // 返回导出的变量
  return module.exports
}
require.cache = {}
require.resolve = (moduleName) => {
  // 根据moduleName解析出完整的模块id
}

(1)函数接收到moduleName后,首先解析出模块的完整路径,赋值给id。
(2)如果cache[id]为true,说明该模块已经被加载过了,直接返回缓存结果
(3)否则,就配置一套环境,用于首次加载。具体来说,创建module对象,包含exports(也就是导出内容),id(作用如上)
(4)将首次加载的module缓存起来
(5)通过loadModule从模块的源文件中读取源代码
(6)最后return module.exports返回想要导出的内容。

require是同步的

在模拟require函数的时候,有一个很重要的细节:require函数必须是同步的。它的作用仅仅是直接将模块内容返回而已,并没有用到回调机制。Node.js中的require也是如此。所以针对module.exports的赋值操作,也必须是同步的,如果用异步就会出问题:

// 出问题
setTimeout(() => {
    module.exports = function () {}
}, 1000)

require是同步函数这一点对定义模块的方式有着非常重要的影响,因为它迫使我们在定义模块时只能使用同步的代码,以至于Node.js都为此,提供了大多数异步API的同步版本。

早期的Node.js有异步版本的require函数,但很快就移除了,因为这会让函数的功能变得十分复杂。

ESM

ESM是ECMAScript2015规范的一部分,该规范给JavaScript语言指定了一套官方的模块系统,以适应各种执行环境。

在Node.js中使用ESM

Node.js默认会把.js后缀的文件,都当成是采用CommonJS语法所写的。如果直接在.js文件中采用ESM语法,解释器会报错。

有三种方法可以在让Node.js解释器转为ESM语法:
1、把文件后缀名改为.mjs;
2、给最近的package.json文件添加type字段,值为“module”;
3、字符串作为参数传入--eval,或通过STDIN管道传输到node,带有标志--input-type=module
比如:

node --input-type=module --eval "import { sep } from 'node:path'; 
console.log(sep);"

不同类型模块引用

ESM可以被解析并缓存为URL(这也意味着特殊字符必须是百分比编码)。支持file:node:data:等的URL协议

file:URL
如果用于解析模块的import说明符具有不同的查询或片段,则会多次加载模块

// 被认为是两个不同的模块
import './foo.mjs?query=1';
import './foo.mjs?query=2';

data:URL
支持使用MIME类型导入:

  • text/javascript用于ES模块
  • application/json用于JSON
  • application/wasm用于Wasm
import 'data:text/javascript,console.log("hello!");';
import _ from 'data:application/json,"world!"' assert { type: 'json' };

data:URL仅解析内置模块的裸说明符和绝对说明符。解析相对说明符不起作用,因为data:不是特殊协议,没有相对解析的概念。

导入断言
这个属性为模块导入语句添加了内联语法,以便在模块说明符旁边传入更多信息。

import fooData from './foo.json' assert { type: 'json' };

const { default: barData } = await import('./bar.json', { assert: { type: 'json' } });

目前只支持JSON模块,而且assert { type: 'json' }语法是具有强制性的。

导入Wash模块
--experimental-wasm-modules标志下支持导入WebAssembly模块,允许将任何.wasm文件作为普通模块导入,同时也支持它们的模块导入。

// index.mjs
import * as M from './module.wasm';
console.log(M)

使用如下命令执行:

node --experimental-wasm-modules index.mjs

顶层await

await关键字可以用在ESM中的顶层。

// a.mjs
export const five = await Promise.resolve(5)

// b.mjs
import { five } from './a.mjs'
console.log(five) // 5

异步引用

前面说过,import语句对模块依赖的解决是静态的,因此有两项著名的限制:

  • 模块标识符不能等到运行的时候再去构造;
  • 模块引入语句,必须写在文件的顶端,而且不能套在控制流语句里;

然而,对于某些情况来说,这两项限制无疑是过于严格。就比如说有一个还算是比较常见的需求:延迟加载

在遇到一个体积很大的模块时,只想在真正需要用到模块里的某个功能时,再去加载这个庞大的模块。

为此,ESM提供了异步引入机制。这种引入操作,可以在程序运行的时候,通过import()运算符实现。从语法上看,相当于一个函数,接收模块标识符作为参数,并返回一个Promise,待Promise resolve后就能得到解析后的模块对象。

ESM的加载过程

用一个循环依赖的例子来说明ESM的加载过程:

// index.js
import * as foo from './foo.js';
import * as bar from './bar.js';
console.log(foo);
console.log(bar);

// foo.js
import * as Bar from './bar.js'
export let loaded = false;
export const bar = Bar;
loaded = true;

// bar.js
import * as Foo from './foo.js';
export let loaded = false;
export const foo = Foo;
loaded = true

先看看运行结果:

Cet article vous amènera à comprendre le système de modules dans node

On peut observer grâce au chargement que les modules foo et bar peuvent enregistrer les informations complètes du module chargé. Mais CommonJS est différent. Il doit y avoir un module qui ne peut pas imprimer à quoi il ressemble une fois complètement chargé.

Examinons le processus de chargement pour voir pourquoi un tel résultat se produit.
Le processus de chargement peut être divisé en trois étapes :

  • Première étape : analyse
  • Deuxième étape : déclaration
  • Troisième étape : exécution

Étape d'analyse syntaxique :
L'interprète démarre à partir du fichier d'entrée (c'est-à-dire index.js), analyse les dépendances entre modules et les affiche sous la forme d'un graphe. Ce graphe est aussi appelé graphe de dépendances.

A ce stade, nous nous concentrons uniquement sur les instructions d'importation, et chargeons le code source correspondant aux modules que ces instructions souhaitent introduire. Et obtenez le graphique de dépendance final grâce à une analyse approfondie. Prenons l'exemple ci-dessus pour illustrer :
1. À partir de index.js, recherchez l'instruction import * as foo from './foo.js', puis accédez au fichier foo.js. import * as foo from './foo.js'语句,从而去到foo.js文件中。
2、从foo.js文件继续解析,发现import * as Bar from './bar.js'语句,从而去到bar.js中。
3、从bar.js继续解析,发现import * as Foo from './foo.js'语句,形式循环依赖,但由于解释器已经在处理foo.js模块了,所以不会再进入其中,然后继续解析bar模块。
4、解析完bar模块后,发现没有import语句了,所以返回foo.js,并继续往下解析。一路都没有再次发现import语句,返回index.js。
5、在index.js中发现import * as bar from './bar.js'2. Continuez l'analyse à partir du fichier foo.js et recherchez l'instruction import * as Bar from './bar.js', allant ainsi vers bar.js.

3. Continuez l'analyse à partir de bar.js et constatez que l'instruction import * as Foo from './foo.js' forme une dépendance circulaire, mais puisque l'interpréteur traite déjà le module foo.js. , donc il n'y entrera plus et continuera à analyser le module bar.

4. Après avoir analysé le module bar, nous avons constaté qu'il n'y avait pas d'instruction d'importation, nous retournons donc à foo.js et continuons l'analyse. L'instruction d'importation n'a pas été retrouvée jusqu'au bout et index.js a été renvoyé.

5. import * as bar from './bar.js' se trouve dans index.js, mais comme bar.js a déjà été analysé, il est ignoré et continue son exécution.

Cet article vous amènera à comprendre le système de modules dans nodeEnfin, le graphe de dépendances est entièrement affiché grâce à la méthode de la profondeur d'abord :



Phase de déclaration :

L'interpréteur part du graphe de dépendances obtenu et déclare chaque module dans l'ordre de bas en haut. Concrètement, à chaque fois qu'un module est atteint, tous les attributs à exporter par le module sont recherchés, et l'identifiant de la valeur exportée est déclaré en mémoire. Attention, seules les déclarations sont effectuées à ce stade et aucune opération d'affectation n'est effectuée.

1. L'interpréteur part du module bar.js et déclare les identifiants deloaded et foo.

2. Remontez jusqu'au module foo.js et déclarez les identifiants chargés et bar. Cet article vous amènera à comprendre le système de modules dans node3. Nous sommes arrivés au module index.js, mais ce module n'a pas d'instruction d'exportation, donc aucun identifiant n'est déclaré.

Cet article vous amènera à comprendre le système de modules dans nodeAprès avoir déclaré tous les identifiants d'exportation, parcourez à nouveau le graphique de dépendances et connectez la relation entre l'importation et l'exportation.

Vous pouvez voir qu'une relation de liaison similaire à const est établie entre le module introduit par import et la valeur exportée par export Le côté importateur ne peut que lire mais pas écrire. De plus, le module bar lu dans index.js et le module bar lu dans foo.js sont essentiellement la même instance.

C'est pourquoi les résultats complets de l'analyse sont affichés dans les résultats de cet exemple.

Ceci est fondamentalement différent de la méthode utilisée par le système CommonJS. Si un module importe un module CommonJS, le système copiera l'intégralité de l'objet d'export de ce dernier et copiera son contenu dans le module courant. Dans ce cas, si le module importé modifie sa propre variable de copie, alors l'utilisateur ne pourra pas voir la nouvelle valeur. .

Phase d'exécution :

Dans cette phase, le moteur exécutera le code du module. Le graphe de dépendances est toujours accessible dans un ordre ascendant et les fichiers consultés sont exécutés un par un. L'exécution commence à partir du fichier bar.js, vers foo.js et enfin vers index.js. Dans ce processus, la valeur de l'identifiant dans la table d'exportation est progressivement améliorée.

Ce processus ne semble pas très différent de CommonJS, mais il existe en fait des différences significatives. Puisque CommonJS est dynamique, il analyse le graphique de dépendances lors de l'exécution des fichiers associés. Ainsi, tant que vous voyez une instruction require, vous pouvez être sûr que lorsque le programme arrive à cette instruction, tous les codes précédents ont été exécutés. Par conséquent, l'instruction require ne doit pas nécessairement apparaître au début du fichier, mais peut apparaître n'importe où, et les identifiants de module peuvent également être construits à partir de variables.

Mais ESM est différent.Dans ESM, les trois étapes ci-dessus sont séparées les unes des autres.Il doit d'abord construire complètement le graphe de dépendances avant de pouvoir exécuter le code.Par conséquent, les opérations d'introduction et d'exportation de modules doivent être statiques. plutôt que d'attendre que le code soit exécuté.

Différences entre ESM et CommonJS

En plus des nombreuses différences mentionnées précédemment, il existe quelques différences qui méritent d'être notées :

Extensions de fichiers forcées

🎜🎜Utilisez le mot-clé import pour analyser le relatif dans ESM ou le spécificateur absolu , l'extension du fichier doit être fournie et l'index du répertoire (« ./path/index.js ») doit être entièrement spécifié. La fonction require de CommonJS permet d'omettre cette extension. 🎜

严格模式

ESM是默认运行于严格模式之下,而且该严格模式是不能禁用。所以不能使用未声明的变量,也不能使用那些仅仅在非严格模式下才能使用的特性(例如with)。

ESM不支持CommonJS提供的某些引用

CommonJS中提供了一些全局变量,这些变量不能在ESM下使用,如果试图使用这些变量会导致ReferenceError错误。包括

  • require
  • exports
  • module.exports
  • __filename
  • __dirname

其中__filename指的是当前这个模块文件的绝对路径,__dirname则是该文件所在文件夹的绝对路径。这连个变量在构建当前文件的相对路径时很有帮助,所以ESM提供了一些方法去实现两个变量的功能。

在ESM中,可以使用import.meta对象来获取一个引用,这个引用指的是当前文件的URL。具体来说,就是通过import.meta.url来获取当前模块的文件路径,这个路径的格式类似file:///path/to/current_module.js。根据这条路径,构造出__filename__dirname所表达的绝对路径:

import { fileURLToPath } from 'url'
import { dirname } from 'path'
const __filename = fileURLToPath(import.meta.url)
const __dirname = dirname(__filename)

而且还能模拟CommonJS中require()函数

import { createRequire } from 'module'
const require = createRequire(import.meta.url)

this指向

在ESM的全局作用域中,this是未定义(undefined),但是在CommonJS模块系统中,它是一个指向exports的引用:

// ESM
console.log(this) // undefined

// CommonJS
console.log(this === exports) // true

ESM加载CommonJS

上面提到过在ESM中可以模拟CommonJS的require()函数,以此来加载CommonJS的模块。除此之外,还可以使用标准的import语法引入CommonJS模块,不过这种引入方式只能把默认导出的东西给引进来:

import packageMain from 'commonjs-package' // 完全可以
import { method } from 'commonjs-package' // 出错

而CommonJS模块的require总是将它引用的文件视为CommonJS。不支持使用require加载ES模块,因为ES模块具有异步执行。但可以使用import()从CommonJS模块中加载ES模块。

导出双重模块

虽然ESM已经推出了7年,node.js也已经稳定支持了,我们开发组件库的时候可以只支持ESM。但为了兼容旧项目,对CommonJS的支持也是必不可少的。有两种广泛使用的方法可以使得组件库同时支持两个模块系统的导出。

使用ES模块封装器

在CommonJS中编写包或将ES模块源代码转换为CommonJS,并创建定义命名导出的ES模块封装文件。使用条件导出,import使用ES模块封装器,require使用CommonJS入口点。举个例子,example模块中

// package.json
{
    "type": "module",
    "exports": {
        "import": "./wrapper.mjs",
        "require": "./index.cjs"
    }
}

使用显示扩展名.cjs.mjs,因为只用.js的话,要么是被默认为CommonJS,要么"type": "module"会导致这些文件都被视为ES模块。

// ./index.cjs
export.name = 'name';

// ./wrapper.mjs
import cjsModule from './index.cjs'
export const name = cjsModule.name;

在这个例子中:

// 使用ESM引入
import { name } from 'example'

// 使用CommonJS引入
const { name } = require('example')

这两种方式引入的name都是相同的单例。

隔离状态

package.json文件可以直接定义单独的CommonJS和ES模块入口点:

// package.json
{
    "type": "module",
    "exports": {
        "import": "./index.mjs",
        "require": "./index.cjs"
    }
}

如果包的CommonJS和ESM版本是等效的,则可以做到这一点,例如因为一个是另一个的转译输出;并且包的状态管理被仔细隔离(或包是无状态的)

状态是一个问题的原因是因为包的CommonJS和ESM版本都可能在应用程序中使用;例如,用户的引用程序代码可以importESM版本,而依赖项require CommonJS版本。如果发生这种情况,包的两个副本将被加载到内存中,因此将出现两个不同的状态。这可能会导致难以解决的错误。

除了编写无状态包(例如,如果JavaScript的Math是一个包,它将是无状态的,因为它的所有方法都是静态的),还有一些方法可以隔离状态,以便在可能加载的CommonJS和ESM之间共享它包的实例:

  • 如果可能,在实例化对象中包含所有状态。比如JavaScript的Date,需要实例化包含状态;如果是包,会这样使用:
import Date from 'date';
const someDate = new Date();
// someDate 包含状态;Date 不包含

new关键字不是必需的;包的函数可以返回新的对象,或修改传入的对象,以保持包外部的状态。

  • 在包的CommonJS和ESM版本之间共享的一个或过个CommonJS文件中隔离状态。比如CommonJS和ESM入口点分别是index.cjs和index.mjs:
// index.cjs
const state = require('./state.cjs')
module.exports.state = state;

// index.mjs
import state from './state.cjs'
export {
    state
}

即使example在应用程序中通过require和import使用example的每个引用都包含相同的状态;并且任一模块系统修改状态将适用二者皆是。

最后

如果本文对你有帮助,就点个赞支持下吧,你的「赞」是我持续进行创作的动力。

Cet article cite les informations suivantes :

  • documentation officielle de Node.js
  • Modèles de conception Node.js

Pour plus de connaissances sur les nœuds, veuillez visiter : Tutoriel Nodejs !

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