Cours d'introduction rapide à Node.js : participez pour apprendre
Il y a deux ans, j'ai écrit un article présentant le système de modules : Comprendre le concept des modules front-end : CommonJs et ES6Module. Les connaissances contenues dans cet article s'adressent aux débutants et sont relativement simples. Ici, nous corrigeons également 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, cet article se concentrera donc sur les principes internes du système de modules et une introduction plus complète aux différences entre les différents systèmes de modules. Le contenu de l'article précédent est dans Ceci. ne se répétera plus.
Tous les langages de programmation n'ont pas de système de modules intégré, et JavaScript n'a pas eu de système de modules pendant longtemps après sa naissance.
Dans l'environnement du navigateur, vous ne pouvez utiliser la balise que pour introduire les fichiers de code inutilisés. Cette méthode partage une portée globale, qui peut être considérée comme pleine de problèmes. Couplée au développement rapide du front-end, cette méthode n'est pas disponible. 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. Bien sûr, le plus célèbre 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'avec la publication 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 conformément à cette norme a été appelé module ECMAScript (ESM). À partir de ce moment-là, ESM a commencé à s'unifier. l'environnement Node.js et l'environnement du navigateur. Bien entendu, ECMAScript6 ne fournit que la syntaxe et la sémantique. Quant à l'implémentation, il appartient aux différents fournisseurs de services de navigateur et aux développeurs de nœuds de travailler dur. C'est pourquoi nous avons l'artefact Babel qui fait l'envie des autres langages de programmation. La mise en œuvre d'un système de modules n'est pas une tâche facile. Node.js n'a un support relativement stable pour ESM que dans la version 13.2.
Quoi qu’il en soit, ESM est le « fils » de JavaScript, et il n’y a rien de mal à l’apprendre !
À l'ère de l'agriculture sur brûlis, JavaScript était utilisé pour développer des applications, et les fichiers de script ne pouvaient être introduits que via des balises de script. L'un des problèmes les plus sérieux est l'absence d'un 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 monModule = (() => { const _privateFn = () => {} const_privateAttr = 1 retour { publicFn : () => {}, publicAttr : 2 } })() console.log(monModule) console.log(monModule.publicFn, monModule._privateFn)
Les résultats en cours d'exécution sont les suivants :
Ce modèle est très simple, utilisez IIFE pour créer une portée privée et utilisez des variables de retour pour être exposées. 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.
Sur la base des idées 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 (nom de fichier, module, require) { const enveloppéSrc = `(fonction (module, exports, require) { ${fs.readFileSync(nom de fichier, 'utf8)} }(module, module.exports, require)` eval(encapsuléSrc) }
Comme [module révélateur], le code source du module est enveloppé dans une fonction. La différence est qu'une série de variables (module, module.exports, require) sont également transmises à la fonction.
Il convient de 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.
Simulez ensuite la fonction require(), dont la fonction principale est de charger le module.
fonction require(nommodule) { const id = require.resolve (nom du module) if (require.cache[id]) { retourner require.cache[id].exports } // Métadonnées du module const module = { exportations : {}, IDENTIFIANT } //Mise à jour du cache require.cache[id] = module //Charger le module loadModule(id, module, require) // Renvoie les variables exportées return module.exports } require.cache = {} require.resolve = (moduleName) => { // Analyse l'identifiant complet du module en fonction du nom du module }
(1) Une fois que la fonction a reçu le nom du module, elle analyse d'abord le chemin complet du module et l'attribue à l'identifiant.
(2) Si cache[id]
est vrai, cela signifie que le module a été chargé, et le résultat du cache sera renvoyé directement (3) Sinon, un environnement sera configuré pour le premier chargement. Plus précisément, créez un objet module, comprenant les exportations (c'est-à-dire le contenu exporté) et l'identifiant (la fonction est comme ci-dessus)
(4) Mettez en cache le module chargé pour la première fois (5) Lisez le code source à partir du fichier source du module via loadModule (6) Enfin, return module.exports
renvoie le contenu que vous souhaitez exporter.
Lors de la simulation de la fonction require, il y a un détail très important : la fonction require doit être synchrone . Sa fonction est uniquement de renvoyer directement le contenu du module et n'utilise pas le mécanisme de rappel. Il en va de même pour require dans Node.js. Par conséquent, l'opération d'affectation pour module.exports doit également être synchrone. Si un mode asynchrone est utilisé, des problèmes surviendront :
// Quelque chose s'est mal passé setTimeout(() => { module.exports = fonction () {} }, 1000)
Le fait que require soit une fonction synchrone a un impact très important sur la façon de définir les modules, car cela nous oblige à utiliser uniquement du code synchrone lors de la définition des modules, de sorte que Node.js fournit à cet effet des versions synchrones de la plupart des API asynchrones.
Les premiers Node.js avaient une version asynchrone de la fonction require, mais elle a été rapidement supprimée car elle rendrait la fonction très compliquée.
ESM fait partie de la spécification ECMAScript2015, qui spécifie un système de modules officiel pour le langage JavaScript afin de s'adapter à divers environnements d'exécution.
Par défaut, Node.js traite les fichiers avec un suffixe .js comme étant écrits à l'aide de la syntaxe CommonJS. Si vous utilisez la syntaxe ESM directement dans le fichier .js, l'interpréteur signalera une erreur.
Il existe trois façons de convertir l'interpréteur Node.js en syntaxe ESM :
1. Modifiez l'extension du fichier en .mjs ;
2. Ajoutez un champ de type au dernier fichier package.json avec la valeur « module » ;
3. La chaîne est passée dans --eval
en tant que paramètre, ou transmise au nœud via le tube STDIN avec l'indicateur --input-type=module
Par exemple:
node --input-type=module --eval "import { sep } from 'node:path'; console.log(septembre);"
ESM peut être analysé et mis en cache sous forme d'URL (ce qui signifie également que les caractères spéciaux doivent être codés en pourcentage). Prend en charge les protocoles URL tels que file:
node:
et data:
fichier :URL
Le module est chargé plusieurs fois si le spécificateur d'importation utilisé pour résoudre le module a des requêtes ou des fragments différents
// Considéré comme deux modules différents import './foo.mjs?query=1'; importer './foo.mjs?query=2';
données:URL
Prend en charge l'importation à l'aide de types MIME :
text/javascript
pour les modules ES
application/json
pour JSON
application/wasm
pour Wasm
importer 'data:text/javascript,console.log("bonjour !");'; import _ from 'data:application/json,"world!"' assert { type: 'json' };
data:URL
analyse uniquement les spécificateurs nus et absolus pour les modules intégrés. L'analyse des spécificateurs relatifs ne fonctionne pas car data:
n'est pas un protocole spécial et n'a aucun concept d'analyse relative.
Assertion d'importation
Cet attribut ajoute une syntaxe en ligne à l'instruction d'importation du module pour transmettre plus d'informations à côté du spécificateur de module.
importer fooData depuis './foo.json' assert { type : 'json' } ; const { par défaut : barData } = wait import('./bar.json', { assert: { type: 'json' } });
Actuellement, seul le module JSON est pris en charge et assert { type: 'json' }
est obligatoire.
Importation de modules Wash
L'importation de modules WebAssembly est prise en charge sous l'indicateur --experimental-wasm-modules
, permettant à n'importe quel fichier .wasm d'être importé en tant que module normal, tout en prenant également en charge l'importation de leurs modules.
// index.mjs importer * en tant que M depuis './module.wasm' ; console.log(M)
Utilisez la commande suivante pour exécuter :
nœud --experimental-wasm-modules index.mjs
Le mot clé wait peut être utilisé au niveau supérieur dans ESM.
// un.mjs exporter const cinq = attendre Promise.resolve (5) // b.mjs importer { cinq } depuis './a.mjs' console.log(cinq) // 5
Comme mentionné précédemment, la résolution des dépendances de module par l'instruction import est statique, elle présente donc deux limitations célèbres :
Les identifiants de module ne peuvent pas attendre l'exécution pour les construire ;
Les instructions d'importation de module doivent être écrites en haut du fichier et ne peuvent pas être imbriquées dans des instructions de flux de contrôle ;
Cependant, pour certaines situations, ces deux restrictions sont sans doute trop strictes. Par exemple, il existe une exigence relativement courante : le chargement paresseux :
Lorsque vous rencontrez un gros module, vous ne souhaitez charger cet énorme module que lorsque vous avez vraiment besoin d'utiliser une certaine fonction dans le module.
A cet effet, ESM fournit un mécanisme d'introduction asynchrone. Cette opération d'introduction peut être réalisée via import()
lorsque le programme est en cours d'exécution. D'un point de vue syntaxique, cela équivaut à une fonction qui reçoit un identifiant de module en tant que paramètre et renvoie une promesse. Une fois la promesse résolue, l'objet module analysé peut être obtenu.
Utilisez un exemple de dépendance circulaire pour illustrer le processus de chargement ESM :
// index.js importer * en tant que foo depuis './foo.js' ; importer * sous forme de barre depuis './bar.js' ; console.log(foo); console.log(barre); // foo.js importer * en tant que barre depuis './bar.js' export let chargé = faux ; exporter const bar = Bar ; chargé = vrai ; //bar.js importer * en tant que Foo depuis './foo.js' ; export let chargé = faux ; exporter const foo = Foo ; chargé = vrai
Jetons d’abord un coup d’œil aux résultats en cours :
On peut observer via le chargement que les deux 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 ce résultat se produit.
Le processus de chargement peut être divisé en trois étapes :
La première étape : l’analyse
Deuxième étape : déclaration
La troisième étape : l'exécution
Étape d'analyse :
L'interpréteur part 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 également 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 import * as foo from './foo.js'
et accédez au fichier foo.js.
2. Continuez l'analyse à partir du fichier foo.js et recherchez import * as Bar from './bar.js'
, allant ainsi à bar.js.
3. Continuez l'analyse à partir de bar.js et recherchez import * as Foo from './foo.js'
, qui forme une dépendance circulaire. Cependant, puisque l'interpréteur traite déjà le module foo.js, il n'y entrera pas. à nouveau, puis continuez. Analysez le module bar.
4. Après avoir analysé le module bar, il s'avère qu'il n'y a pas d'instruction d'importation, il revient donc à foo.js et continue 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.
Enfin, le graphique de dépendances est entièrement affiché grâce à une approche en profondeur :
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, toutes les propriétés à exporter par le module sont recherchées et les identifiants des valeurs exportées sont déclarés 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.
3. 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é.
Après avoir déclaré tous les identifiants d'exportation, parcourez à nouveau le graphique de dépendances pour relier la relation entre l'importation et l'exportation.
On peut 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 l’approche utilisée par le système CommonJS. Si un module importe un module CommonJS, le système copiera l'intégralité de l'objet exports 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 :
A ce stade, 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 réalité des différences majeures. 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 faut 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. N'attendez pas que le code soit exécuté.
En plus des nombreuses différences mentionnées précédemment, il convient de noter quelques différences :
Lorsque vous utilisez le mot-clé import dans ESM pour résoudre des spécificateurs relatifs ou absolus, l'extension de 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 s'exécute en mode strict par défaut et ce mode strict ne peut pas être désactivé. Par conséquent, vous ne pouvez pas utiliser de variables non déclarées, ni utiliser des fonctionnalités disponibles uniquement en mode non strict (comme avec).
CommonJS fournit certaines variables globales. Ces variables ne peuvent pas être utilisées sous ESM. Si vous essayez d'utiliser ces variables, une ReferenceError se produira. inclure
require
exports
module.exports
__filename
__dirname
Parmi eux, __filename
fait référence au chemin absolu du fichier du module actuel et __dirname
est le chemin absolu du dossier où se trouve le fichier. Ces deux variables sont très utiles lors de la construction du chemin relatif du fichier actuel, c'est pourquoi ESM fournit quelques méthodes pour implémenter les fonctions des deux variables.
Dans ESM, vous pouvez utiliser l'objet import.meta
pour obtenir une référence, qui fait référence à l'URL du fichier actuel. Plus précisément, le chemin du fichier du module actuel est obtenu via import.meta.url
. Le format de ce chemin est similaire à file:///path/to/current_module.js
. Sur la base de ce chemin, le chemin absolu exprimé par __filename
et __dirname
est construit :
importer { fileURLToPath } depuis 'url' importer { nom de répertoire } depuis 'chemin' const __filename = fileURLToPath(import.meta.url) const __dirname = dirname(__filename)
Il peut également simuler la fonction require() dans CommonJS
importer { createRequire } depuis 'module' const require = createRequire(import.meta.url)
Dans la portée globale d'ESM, ceci n'est pas défini, mais dans le système de modules CommonJS, il s'agit d'une référence aux exportations :
//ESM console.log(this) // non défini //CommonJS console.log(this === exporte) // vrai
Comme mentionné ci-dessus, la fonction CommonJS require() peut être simulée dans ESM pour charger des modules CommonJS. De plus, vous pouvez également utiliser la syntaxe d'importation standard pour introduire des modules CommonJS, mais cette méthode d'importation ne peut importer que les éléments exportés par défaut :
import packageMain from 'commonjs-package' // Il est tout à fait possible d'importer { méthode } depuis 'commonjs-package' // Erreur
Le module require du module CommonJS traite toujours les fichiers auxquels il fait référence comme CommonJS. Le chargement de modules ES à l'aide de require n'est pas pris en charge car les modules ES ont une exécution asynchrone. Mais vous pouvez utiliser import()
pour charger des modules ES à partir de modules CommonJS.
Bien qu'ESM soit lancé depuis 7 ans, node.js le prend également en charge de manière stable. Lorsque nous développons des bibliothèques de composants, nous ne pouvons prendre en charge qu'ESM. Mais afin d’être compatible avec les anciens projets, le support de CommonJS est également indispensable. Il existe deux méthodes largement utilisées pour qu'une bibliothèque de composants prenne en charge les exportations à partir des deux systèmes de modules.
Écrivez des packages dans CommonJS ou convertissez le code source du module ES en CommonJS et créez des fichiers wrapper de module ES qui définissent les exportations nommées. Utilisez l'exportation conditionnelle, l'importation utilise le wrapper du module ES et require utilise le point d'entrée CommonJS. Par exemple, dans le module exemple
// package.json { "type": "module", "exportations": { "import": "./wrapper.mjs", "require": "./index.cjs" } }
Utilisez les extensions d'affichage .cjs
et .mjs
, car l'utilisation uniquement .js
sera soit CommonJS par défaut, soit "type": "module"
fera que ces fichiers seront traités comme des modules ES.
// ./index.cjs export.name = 'nom'; // ./wrapper.mjs importer cjsModule depuis './index.cjs' exporter le nom const = cjsModule.name ;
Dans cet exemple :
// Utilisez ESM pour introduire import { name } from 'example' // Utilisez CommonJS pour introduire const { name } = require('example')
Le nom introduit dans les deux sens est le même singleton.
Le fichier package.json peut définir directement des points d'entrée distincts pour les modules CommonJS et ES :
// package.json { "type": "module", "exportations": { "import": "./index.mjs", "require": "./index.cjs" } }
Cela peut être fait si les versions CommonJS et ESM du package sont équivalentes, par exemple parce que l'une est une sortie transpilée de l'autre et que la gestion de l'état du package est soigneusement isolée (ou que le package est sans état) ;
La raison pour laquelle l'état est un problème est que les versions CommonJS et ESM du package peuvent être utilisées dans l'application ; par exemple, le code référent de l'utilisateur peut importer la version ESM, alors que la dépendance nécessite la version CommonJS. Si cela se produit, deux copies du package seront chargées en mémoire, donc deux états différents se produiront. Cela peut conduire à des erreurs difficiles à résoudre.
En plus d'écrire des packages sans état (par exemple, si Math de JavaScript était un package, il serait sans état car toutes ses méthodes sont statiques), il existe des moyens d'isoler l'état afin qu'il puisse être utilisé dans CommonJS et ESM potentiellement chargés. instances de package :
Si possible, incluez tous les états dans l'objet instancié. Par exemple, la date de JavaScript doit être instanciée pour contenir un état ; s'il s'agit d'un package, elle sera utilisée comme ceci :
importer la date à partir de « date » ; const someDate = new Date(); // someDate contient l'état ; la date ne le fait pas
Le mot-clé new n'est pas obligatoire ; les fonctions du package peuvent renvoyer de nouveaux objets ou modifier les objets transmis pour conserver leur état en dehors du package.
Isoler l'état dans un ou plusieurs fichiers CommonJS partagés entre les versions CommonJS et ESM d'un package. Par exemple, les points d'entrée pour CommonJS et ESM sont respectivement index.cjs et index.mjs :
// index.cjs const state = require('./state.cjs') module.exports.state = état ; // index.mjs importer l'état depuis './state.cjs' exporter { État }
Même si example est utilisé dans une application via require et import, chaque référence à example contient le même état et les modifications apportées à l'état par l'un ou l'autre des systèmes de modules s'appliqueront aux deux ;
Si cet article vous est utile, merci de lui donner un like et de le soutenir. Votre "j'aime" est pour moi la motivation de continuer à créer.
Cet article cite les informations suivantes :
documentation officielle de node.js
Modèles de conception Node.js