Heim  >  Artikel  >  Web-Frontend  >  In diesem Artikel erfahren Sie mehr über das Modulsystem im Knoten

In diesem Artikel erfahren Sie mehr über das Modulsystem im Knoten

青灯夜游
青灯夜游nach vorne
2023-01-14 19:09:482563Durchsuche

Dieser Artikel hilft Ihnen, Node zu lernen und das Modulsystem in Node auf einfache und unkomplizierte Weise zu verstehen.

In diesem Artikel erfahren Sie mehr über das Modulsystem im Knoten

Ich habe vor zwei Jahren einen Artikel geschrieben, um das Modulsystem vorzustellen: Das Konzept von Front-End-Modulen verstehen: CommonJs und ES6Module. Das Wissen in diesem Artikel richtet sich an Anfänger und ist relativ einfach. Ich möchte auch ein paar Fehler im Artikel korrigieren:

  • [Modul] und [Modulsystem] sind zwei verschiedene Dinge. Ein Modul ist eine Einheit in der Software, und ein Modulsystem ist ein Satz von Syntax oder Werkzeugen. Das Modulsystem ermöglicht Entwicklern, Module in Projekten zu definieren und zu verwenden.
  • Die Abkürzung für ECMAScript Module ist ESM oder ESModule, nicht ES6Module.

Das Grundwissen über das Modulsystem wurde im vorherigen Artikel fast abgedeckt, daher konzentriert sich dieser Artikel auf die internen Prinzipien des Modulsystems und eine umfassendere Einführung die Unterschiede zwischen verschiedenen Modulsystemen oben Der Inhalt das in einem Artikel vorkommt, wird hier nicht wiederholt. [Verwandte Tutorial-Empfehlungen: nodejs-Video-Tutorial, Programmierunterricht]

Modulsystem

Nicht alle Programmiersprachen verfügen über ein integriertes Modulsystem. Nach der Geburt gab es lange Zeit kein Modulsystem von JavaScript.

In der Browserumgebung können Sie das <script>-Tag nur zum Einführen nicht verwendeter Codedateien verwenden. Diese Methode weist einen gemeinsamen globalen Umfang auf, der mit der schnellen Entwicklung des Frontends verbunden ist entspricht nicht mehr den aktuellen Bedürfnissen. Bevor das offizielle Modulsystem erschien, erstellte die Front-End-Community ein eigenes Modulsystem von Drittanbietern. Die am häufigsten verwendeten sind: asynchrone Moduldefinition AMD, universelle Moduldefinition UMD usw. Natürlich die bekanntesten Eine davon ist CommonJS.

Da Node.js eine JavaScript-Laufumgebung ist, kann es direkt auf das zugrunde liegende Dateisystem zugreifen. Deshalb haben die Entwickler es übernommen und ein Modulsystem gemäß den CommonJS-Spezifikationen implementiert.

Am Anfang konnte CommonJS nur auf der Node.js-Plattform verwendet werden. Mit dem Aufkommen von Modulpaketierungstools wie Browserify und Webpack kann CommonJS endlich auf der Browserseite ausgeführt werden.

Erst mit der Veröffentlichung der ECMAScript6-Spezifikation im Jahr 2015 gab es einen formalen Standard für das Modulsystem. Das nach diesem Standard erstellte Modulsystem hieß „ECMAScript-Modul“, abgekürzt als [ESM]. begann mit der Vereinheitlichung der Node.js-Umgebung und der Browserumgebung. Natürlich stellt ECMAScript6 nur Syntax und Semantik bereit. Für die Implementierung müssen verschiedene Browserdienstanbieter und Node-Entwickler hart arbeiten. Deshalb haben wir das babel-Artefakt, um das uns andere Programmiersprachen beneiden. Die Implementierung eines Modulsystems ist keine leichte Aufgabe. Node.js unterstützt ESM nur in Version 13.2. Aber egal was passiert, ESM ist der „Sohn“ von JavaScript und es ist nichts Falsches daran, es zu lernen!

Die Grundidee des Modulsystems

Im Zeitalter der Brandrodung wurde JavaScript zur Entwicklung von Anwendungen verwendet. Skriptdateien können nur über Skript-Tags eingeführt werden. Eines der schwerwiegenderen Probleme ist das Fehlen eines Namespace-Mechanismus, was bedeutet, dass jedes Skript denselben Gültigkeitsbereich hat. Es gibt eine bessere Lösung für dieses Problem in der Community:

Revevaling-Modul

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

console.log(myModule)
console.log(myModule.publicFn, myModule._privateFn)
Die laufenden Ergebnisse sind wie folgt:

In diesem Artikel erfahren Sie mehr über das Modulsystem im KnotenDieser Modus ist sehr einfach. Verwenden Sie

IIFE

, um einen privaten Bereich zu erstellen, und verwenden Sie die Rückgabe um es variabel darzustellen. Auf interne Variablen (z. B. _privateFn, _privateAttr) kann von außen nicht zugegriffen werden. [Revealing-Modul] nutzt diese Funktionen, um private Informationen zu verbergen und APIs zu exportieren, die der Außenwelt zugänglich gemacht werden sollten. Auf dieser Idee basiert auch die Entwicklung des nachfolgenden Modulsystems.

CommonJS

Entwickeln Sie basierend auf der obigen Idee einen Modullader.

Schreiben Sie zuerst eine Funktion, die den Inhalt des Moduls lädt, packen Sie diese Funktion in einen privaten Bereich und werten Sie sie dann über eval() aus, um die Funktion auszuführen:

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

Gleiche wie [offenbarndes Modul], geben Sie den Quellcode von ein Das Modul ist in eine Funktion eingebettet. Der Unterschied besteht darin, dass auch eine Reihe von Variablen (Modul, Modul.Export, Require) an die Funktion übergeben werden.

Es ist erwähnenswert, dass der Modulinhalt über [readFileSync] gelesen wird. Im Allgemeinen sollten Sie die synchronisierte Version nicht verwenden, wenn Sie APIs aufrufen, die das Dateisystem betreffen. Diesmal ist es jedoch anders, da das Laden von Modulen über das CommonJs-System selbst als synchroner Vorgang implementiert werden sollte, um sicherzustellen, dass mehrere Module in der richtigen Abhängigkeitsreihenfolge eingeführt werden können.

接着模拟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

先看看运行结果:

In diesem Artikel erfahren Sie mehr über das Modulsystem im Knoten

Durch geladen kann beobachtet werden, dass beide Module foo und bar die gesamten geladenen Modulinformationen protokollieren können. Aber CommonJS ist anders. Es muss ein Modul geben, das nach dem vollständigen Laden nicht ausdrucken kann.

Lassen Sie uns einen Blick auf den Ladevorgang werfen, um herauszufinden, warum ein solches Ergebnis auftritt.
Der Ladevorgang kann in drei Phasen unterteilt werden:

  • Erste Phase: Parsen
  • Zweite Phase: Deklaration
  • Dritte Phase: Ausführung

Parsing-Phase:
Der Interpreter beginnt mit der Eintragsdatei (d. h. index.js) analysiert die Abhängigkeiten zwischen Modulen und stellt sie in Form eines Diagramms dar. Dieses Diagramm wird auch als Abhängigkeitsdiagramm bezeichnet.

Zu diesem Zeitpunkt konzentrieren wir uns nur auf die Importanweisungen und laden den Quellcode, der den Modulen entspricht, die diese Anweisungen einführen möchten. Und erhalten Sie das endgültige Abhängigkeitsdiagramm durch eine eingehende Analyse. Nehmen Sie das obige Beispiel zur Veranschaulichung:
1. Suchen Sie ausgehend von index.js die Anweisung import * as foo from './foo.js' und gehen Sie dann zur Datei 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. Fahren Sie mit dem Parsen der foo.js-Datei fort und suchen Sie die Anweisung import * as Bar from './bar.js', um zu bar.js zu gelangen.

3. Fahren Sie mit dem Parsen von bar.js fort und stellen Sie fest, dass die Anweisung import * as Foo from './foo.js' eine zirkuläre Abhängigkeit bildet, da der Interpreter jedoch bereits das foo.js-Modul verarbeitet Daher wird es nicht erneut eingegeben und das Balkenmodul wird weiterhin analysiert.

4. Nach dem Parsen des Bar-Moduls haben wir festgestellt, dass es keine Importanweisung gibt, also kehren wir zu foo.js zurück und fahren mit dem Parsen fort. Die Importanweisung wurde nicht vollständig gefunden und index.js wurde zurückgegeben.

5. import * as bar from './bar.js' wird in index.js gefunden, aber da bar.js bereits analysiert wurde, wird es übersprungen und die Ausführung fortgesetzt.

In diesem Artikel erfahren Sie mehr über das Modulsystem im KnotenSchließlich wird das Abhängigkeitsdiagramm vollständig durch die Tiefenmethode angezeigt:



Deklarationsphase:

Der Interpreter beginnt mit dem erhaltenen Abhängigkeitsdiagramm und deklariert jedes Modul der Reihe nach von unten nach oben. Insbesondere werden jedes Mal, wenn ein Modul erreicht wird, alle vom Modul zu exportierenden Attribute durchsucht und die Kennung des exportierten Werts im Speicher deklariert. Bitte beachten Sie, dass in dieser Phase nur Deklarationen vorgenommen und keine Zuweisungsvorgänge durchgeführt werden.

1. Der Interpreter startet vom Modul bar.js und deklariert die Bezeichner von „loaded“ und „foo“.

2. Verfolgen Sie zurück zum foo.js-Modul und deklarieren Sie die geladenen und bar-Identifikatoren. In diesem Artikel erfahren Sie mehr über das Modulsystem im Knoten3. Wir sind beim Modul index.js angekommen, aber dieses Modul hat keine Exportanweisung, daher ist kein Bezeichner deklariert.

In diesem Artikel erfahren Sie mehr über das Modulsystem im KnotenNachdem Sie alle Exportkennungen deklariert haben, gehen Sie das Abhängigkeitsdiagramm erneut durch und stellen Sie die Beziehung zwischen Import und Export her.

Sie können sehen, dass zwischen dem durch Import eingeführten Modul und dem durch Export exportierten Wert eine Bindungsbeziehung ähnlich wie bei const hergestellt wird. Die importierende Seite kann nur lesen, aber nicht schreiben. Darüber hinaus sind das in index.js gelesene Balkenmodul und das in foo.js gelesene Balkenmodul im Wesentlichen dieselbe Instanz.

Deshalb werden in den Ergebnissen dieses Beispiels die vollständigen Analyseergebnisse ausgegeben.

Dies unterscheidet sich grundlegend von der vom CommonJS-System verwendeten Methode. Wenn ein Modul ein CommonJS-Modul importiert, kopiert das System dessen gesamtes Exportobjekt und kopiert seinen Inhalt in das aktuelle Modul. Wenn das importierte Modul in diesem Fall seine eigene Kopiervariable ändert, kann der Benutzer den neuen Wert nicht sehen .

Ausführungsphase:

In dieser Phase führt die Engine den Modulcode aus. Der Zugriff auf das Abhängigkeitsdiagramm erfolgt weiterhin in der Reihenfolge von unten nach oben und die Dateien, auf die zugegriffen wird, werden einzeln ausgeführt. Die Ausführung beginnt in der Datei bar.js über foo.js und schließlich index.js. Dabei wird der Wert der Kennung in der Exporttabelle schrittweise verbessert.

Dieser Prozess scheint sich nicht viel von CommonJS zu unterscheiden, aber es gibt tatsächlich große Unterschiede. Da CommonJS dynamisch ist, analysiert es das Abhängigkeitsdiagramm, während es zugehörige Dateien ausführt. Solange Sie also eine require-Anweisung sehen, können Sie sicher sein, dass alle vorherigen Codes ausgeführt wurden, wenn das Programm zu dieser Anweisung kommt. Daher muss die require-Anweisung nicht unbedingt am Anfang der Datei stehen, sondern kann überall stehen, und Modulbezeichner können auch aus Variablen erstellt werden.

Aber in ESM sind die oben genannten drei Phasen voneinander getrennt. Bevor der Code ausgeführt werden kann, müssen die Vorgänge zum Einführen und Exportieren von Modulen statisch sein. anstatt zu warten, bis der Code ausgeführt wird.

Unterschiede zwischen ESM und CommonJS

Zusätzlich zu den verschiedenen zuvor erwähnten Unterschieden gibt es einige bemerkenswerte Unterschiede:

Erzwungene Dateierweiterungen

🎜🎜Verwenden Sie das Schlüsselwort import, um relative oder absolute Spezifizierer in ESM zu analysieren Die Dateierweiterung muss angegeben werden und der Verzeichnisindex („./path/index.js“) muss vollständig angegeben werden. Die CommonJS-Require-Funktion ermöglicht das Weglassen dieser Erweiterung. 🎜

严格模式

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的每个引用都包含相同的状态;并且任一模块系统修改状态将适用二者皆是。

最后

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

Dieser Artikel zitiert die folgenden Informationen:

  • node.js offizielle Dokumentation
  • Node.js Design Patterns

Weitere Informationen zu Knoten finden Sie unter: nodejs Tutorial!

Das obige ist der detaillierte Inhalt vonIn diesem Artikel erfahren Sie mehr über das Modulsystem im Knoten. 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