溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

node中的模塊系統(tǒng)是什么

發(fā)布時間:2022-08-24 09:47:59 來源:億速云 閱讀:166 作者:iii 欄目:web開發(fā)

這篇文章主要介紹“node中的模塊系統(tǒng)是什么”,在日常操作中,相信很多人在node中的模塊系統(tǒng)是什么問題上存在疑惑,小編查閱了各式資料,整理出簡單好用的操作方法,希望對大家解答”node中的模塊系統(tǒng)是什么”的疑惑有所幫助!接下來,請跟著小編一起來學(xué)習(xí)吧!

node中的模塊系統(tǒng)是什么

模塊系統(tǒng)

并不是所有編程語言都有內(nèi)置的模塊系統(tǒng),JavaScript誕生之后的很長一段時間里都沒有模塊系統(tǒng)。

在瀏覽器環(huán)境中只能使用<script>標(biāo)簽來引入不用的代碼文件,這種方法共享一個全局作用域,可謂是問題多多;加上前端日新月異的發(fā)展,這種方法已經(jīng)不滿足當(dāng)下的需求了。在沒官方的模塊系統(tǒng)出現(xiàn)前,前端社區(qū)自己創(chuàng)建第三方模塊系統(tǒng),用的較多的有:異步模塊定義AMD、通用模塊定義UMD等,當(dāng)然最著名還得是CommonJS。

由于Node.js它是一個JavaScript的運(yùn)行環(huán)境,可以直接訪問底層的文件系統(tǒng)。所以開發(fā)者通過它,并按照CommonJS規(guī)范實(shí)現(xiàn)了一套模塊系統(tǒng)。

最開始,CommonJS只能用于Node.js平臺,隨著Browserify和Webpack之類的模塊打包工具的出現(xiàn),CommonJS也終于能在瀏覽器端運(yùn)行了。

到2015年發(fā)布了ECMAScript6規(guī)范,才有了模塊系統(tǒng)的正式標(biāo)準(zhǔn),按照該標(biāo)準(zhǔn)打造出來的模塊系統(tǒng)稱為ECMAScript module簡稱【ESM】,由此ESM就開始統(tǒng)一了Node.js環(huán)境與瀏覽器環(huán)境。當(dāng)然ECMAScript6只是提供了語法和語義,至于實(shí)現(xiàn)部分得由各瀏覽器服務(wù)廠商和Node開發(fā)者去努力。所以才有了令其他編程語言羨慕不已的babel神器,實(shí)現(xiàn)模塊系統(tǒng)并不是一件容易的事,Node.js也是到了13.2版本才算是比較穩(wěn)定的支持ESM。

但不管怎么樣,ESM才是JavaScript的“親兒子”,學(xué)習(xí)它一定不會有錯!

模塊系統(tǒng)的基本思路

在刀耕火種的年代中使用JavaScript開發(fā)應(yīng)用,腳本文件只能通過script標(biāo)簽引入。其中遇到比較嚴(yán)重的問題就是缺乏命名空間機(jī)制,這意味著每個腳本都共享同一作用域。這個問題在社區(qū)中有一個比較好的解決方法:Revevaling module

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

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

運(yùn)行結(jié)果如下:

node中的模塊系統(tǒng)是什么

這個模式很簡單,利用IIFE創(chuàng)建一個私有的作用域,同時使用return需要暴露的變量。而屬于內(nèi)部的變量(比如_privateFn、_privateAttr)是不能從外面的作用域訪問的。

【revealing module】正是利用了這些特性,來隱藏私有的信息,同時把應(yīng)該公布給外界的API導(dǎo)出。后面的模塊系統(tǒng)也正是基于這樣的思路開發(fā)的。

CommonJS

基于上面思路,來開發(fā)一個模塊加載器。

首先編寫一個加載模塊內(nèi)容的函數(shù),并把這個函數(shù)包裹在私有作用域里面,然后通過eval()求值,以運(yùn)行該函數(shù):

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

和【revealing module】一樣,把模塊的源代碼包裹在函數(shù)里面,區(qū)別在于,還把一系列變量(module, module.exports, require)傳給該函數(shù)。

值得注意的是,通過【readFileSync】讀取模塊內(nèi)容。一般來說,在調(diào)用涉及文件系統(tǒng)的API時,不應(yīng)該使用同步版本。但此時不同,因?yàn)橥ㄟ^CommonJs系統(tǒng)來加載模塊,本身就應(yīng)該實(shí)現(xiàn)成同步操作,以確保多個模塊能夠按照正確的依賴順序得到引入。

接著模擬require()函數(shù),主要功能是加載模塊。

function require(moduleName) {
  const id = require.resolve(moduleName)
  if (require.cache[id]) {
    return require.cache[id].exports
  }
  // 模塊的元數(shù)據(jù)
  const module = {
    exports: {},
    id
  }
  // 更新緩存
  require.cache[id] = module
  
  // 載入模塊
  loadModule(id, module, require)
  
  // 返回導(dǎo)出的變量
  return module.exports
}
require.cache = {}
require.resolve = (moduleName) => {
  // 根據(jù)moduleName解析出完整的模塊id
}

(1)函數(shù)接收到moduleName后,首先解析出模塊的完整路徑,賦值給id。
(2)如果cache[id]為true,說明該模塊已經(jīng)被加載過了,直接返回緩存結(jié)果
(3)否則,就配置一套環(huán)境,用于首次加載。具體來說,創(chuàng)建module對象,包含exports(也就是導(dǎo)出內(nèi)容),id(作用如上)
(4)將首次加載的module緩存起來
(5)通過loadModule從模塊的源文件中讀取源代碼
(6)最后return module.exports返回想要導(dǎo)出的內(nèi)容。

require是同步的

在模擬require函數(shù)的時候,有一個很重要的細(xì)節(jié):require函數(shù)必須是同步的。它的作用僅僅是直接將模塊內(nèi)容返回而已,并沒有用到回調(diào)機(jī)制。Node.js中的require也是如此。所以針對module.exports的賦值操作,也必須是同步的,如果用異步就會出問題:

// 出問題
setTimeout(() => {
    module.exports = function () {}
}, 1000)

require是同步函數(shù)這一點(diǎn)對定義模塊的方式有著非常重要的影響,因?yàn)樗仁刮覀冊诙x模塊時只能使用同步的代碼,以至于Node.js都為此,提供了大多數(shù)異步API的同步版本。

早期的Node.js有異步版本的require函數(shù),但很快就移除了,因?yàn)檫@會讓函數(shù)的功能變得十分復(fù)雜。

ESM

ESM是ECMAScript2015規(guī)范的一部分,該規(guī)范給JavaScript語言指定了一套官方的模塊系統(tǒng),以適應(yīng)各種執(zhí)行環(huán)境。

在Node.js中使用ESM

Node.js默認(rèn)會把.js后綴的文件,都當(dāng)成是采用CommonJS語法所寫的。如果直接在.js文件中采用ESM語法,解釋器會報(bào)錯。

有三種方法可以在讓Node.js解釋器轉(zhuǎn)為ESM語法:
1、把文件后綴名改為.mjs;
2、給最近的package.json文件添加type字段,值為“module”;
3、字符串作為參數(shù)傳入--eval,或通過STDIN管道傳輸?shù)絥ode,帶有標(biāo)志--input-type=module
比如:

node --input-type=module --eval "import { sep } from 'node:path'; 
console.log(sep);"
不同類型模塊引用

ESM可以被解析并緩存為URL(這也意味著特殊字符必須是百分比編碼)。支持file:、node:data:等的URL協(xié)議

file:URL
如果用于解析模塊的import說明符具有不同的查詢或片段,則會多次加載模塊

// 被認(rèn)為是兩個不同的模塊
import './foo.mjs?query=1';
import './foo.mjs?query=2';

data:URL
支持使用MIME類型導(dǎo)入:

  • 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僅解析內(nèi)置模塊的裸說明符和絕對說明符。解析相對說明符不起作用,因?yàn)?code>data:不是特殊協(xié)議,沒有相對解析的概念。

導(dǎo)入斷言
這個屬性為模塊導(dǎo)入語句添加了內(nèi)聯(lián)語法,以便在模塊說明符旁邊傳入更多信息。

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

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

目前只支持JSON模塊,而且assert { type: 'json' }語法是具有強(qiáng)制性的。

導(dǎo)入Wash模塊
--experimental-wasm-modules標(biāo)志下支持導(dǎo)入WebAssembly模塊,允許將任何.wasm文件作為普通模塊導(dǎo)入,同時也支持它們的模塊導(dǎo)入。

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

使用如下命令執(zhí)行:

node --experimental-wasm-modules index.mjs
頂層await

await關(guān)鍵字可以用在ESM中的頂層。

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

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

前面說過,import語句對模塊依賴的解決是靜態(tài)的,因此有兩項(xiàng)著名的限制:

  • 模塊標(biāo)識符不能等到運(yùn)行的時候再去構(gòu)造;

  • 模塊引入語句,必須寫在文件的頂端,而且不能套在控制流語句里;

然而,對于某些情況來說,這兩項(xiàng)限制無疑是過于嚴(yán)格。就比如說有一個還算是比較常見的需求:延遲加載

在遇到一個體積很大的模塊時,只想在真正需要用到模塊里的某個功能時,再去加載這個龐大的模塊。

為此,ESM提供了異步引入機(jī)制。這種引入操作,可以在程序運(yùn)行的時候,通過import()運(yùn)算符實(shí)現(xiàn)。從語法上看,相當(dāng)于一個函數(shù),接收模塊標(biāo)識符作為參數(shù),并返回一個Promise,待Promise resolve后就能得到解析后的模塊對象。

ESM的加載過程

用一個循環(huán)依賴的例子來說明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

先看看運(yùn)行結(jié)果:

node中的模塊系統(tǒng)是什么

通過loaded可以觀察到,foo和bar這兩個模塊都能log出加載完整的模塊信息。而CommonJS卻不一樣,一定會有一個模塊無法打印出完整加載后的樣子。

我們深入加載過程,看看為什么會出現(xiàn)這樣的結(jié)果。
加載過程可以分為三個階段:

  • 第一個階段:解析

  • 第二個階段:聲明

  • 第三個階段:執(zhí)行

解析階段:
解釋器從入口文件出發(fā)(也就是index.js),解析模塊之間的依賴關(guān)系,以圖的形式展示出來,這張圖也被稱為依賴關(guān)系圖。

在這個階段只關(guān)注與import語句,并把這些語句想要引入的模塊所對應(yīng)的源碼,給加載進(jìn)來。并以深度解析的方式得到最后的依賴關(guān)系圖。以上面例子說明:
1、從index.js開始,發(fā)現(xiàn)import * as foo from './foo.js'語句,從而去到foo.js文件中。
2、從foo.js文件繼續(xù)解析,發(fā)現(xiàn)import * as Bar from './bar.js'語句,從而去到bar.js中。
3、從bar.js繼續(xù)解析,發(fā)現(xiàn)import * as Foo from './foo.js'語句,形式循環(huán)依賴,但由于解釋器已經(jīng)在處理foo.js模塊了,所以不會再進(jìn)入其中,然后繼續(xù)解析bar模塊。
4、解析完bar模塊后,發(fā)現(xiàn)沒有import語句了,所以返回foo.js,并繼續(xù)往下解析。一路都沒有再次發(fā)現(xiàn)import語句,返回index.js。
5、在index.js中發(fā)現(xiàn)import * as bar from './bar.js',但由于bar.js已經(jīng)解析過了,所以略過,繼續(xù)往下執(zhí)行。

最后通過深度優(yōu)先的方式把依賴圖完整的展示出來:

node中的模塊系統(tǒng)是什么

聲明階段:
解釋器從得到的依賴圖出發(fā),從底到上的順序?qū)γ總€模塊進(jìn)行聲明。具體來說,每到達(dá)一個模塊,就尋找該模塊所要導(dǎo)出的全部屬性,并在內(nèi)存中聲明導(dǎo)出值的標(biāo)識符。請注意,該階段只作聲明,不會進(jìn)行賦值操作。
1、解釋器從bar.js模塊出發(fā),聲明loaded和foo的標(biāo)識符。
2、向上回溯,到了foo.js模塊,聲明loaded和bar標(biāo)識符。
3、到了index.js模塊,但這個模塊沒有導(dǎo)出語句,所以沒有聲明任何標(biāo)識符。

node中的模塊系統(tǒng)是什么

聲明完所有導(dǎo)出標(biāo)識符后,再走一遍依賴圖,把import引入和export導(dǎo)出的關(guān)系連接起來。

node中的模塊系統(tǒng)是什么

可以看到,由import引進(jìn)來的模塊與export所導(dǎo)出值之間,建立了一種類似于const的綁定關(guān)系,引入方這一端是只能讀而不能寫。而且在index.js讀取的bar模塊,與在foo.js讀取的bar模塊實(shí)質(zhì)是同一個實(shí)例。

所以這就是為什么在這個例子的結(jié)果中都能輸出完整的解析結(jié)果的原因。

這跟CommonJS系統(tǒng)所用的方法有根本的區(qū)別。如果有某個模塊要引入CommonJS模塊,那么系統(tǒng)會對后者的整個exports對象做拷貝,從而將其中的內(nèi)容復(fù)制到當(dāng)前模塊里面,這樣的話,如果受引入的那個模塊修改了自身的那一份變量,那么用戶這邊是看不到新值的。

執(zhí)行階段:
在這個階段中,引擎才會去執(zhí)行模塊的代碼。依然采用從底向上的順序訪問依賴圖,并逐個執(zhí)行訪問到的文件。從bar.js文件開始執(zhí)行,到foo.js,最后才是index.js。在這個過程中,逐步完善export表中標(biāo)識符的值。

這套流程與CommonJS看似沒有太大區(qū)別,但實(shí)際有著重大差異。由于CommonJS是動態(tài)的,因此它一邊解析依賴圖,一邊執(zhí)行相關(guān)的文件。所以只要看到一條require語句,就可以肯定的說,當(dāng)程序來到這條語句時,已經(jīng)把前面的代碼都執(zhí)行完了。因此,require語句不一定非要出現(xiàn)在文件的開頭,而是可以出現(xiàn)在任意地方,而且,模塊標(biāo)識符也可以通過變量來構(gòu)造。

但ESM不同,在ESM里,上述這三個階段是彼此分離的,它必須先把依賴圖完整地構(gòu)造出來,然后才能執(zhí)行代碼,因此,引入模塊與導(dǎo)出模塊的操作,都必須是靜態(tài)的,而不能等到執(zhí)行代碼的時候再去做。

ESM與CommonJS區(qū)別

除了前面提到的幾個區(qū)別之外,還有一些區(qū)別是值得注意的:

強(qiáng)制的文件擴(kuò)展名

在ESM中使用import關(guān)鍵字解析相對或絕對的說明符時,必須提供文件擴(kuò)展名,還必須完全指定目錄索引('./path/index.js')。而CommonJS的require函數(shù)則允許省略這個擴(kuò)展名。

嚴(yán)格模式

ESM是默認(rèn)運(yùn)行于嚴(yán)格模式之下,而且該嚴(yán)格模式是不能禁用。所以不能使用未聲明的變量,也不能使用那些僅僅在非嚴(yán)格模式下才能使用的特性(例如with)。

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

CommonJS中提供了一些全局變量,這些變量不能在ESM下使用,如果試圖使用這些變量會導(dǎo)致ReferenceError錯誤。包括

  • require

  • exports

  • module.exports

  • __filename

  • __dirname

其中__filename指的是當(dāng)前這個模塊文件的絕對路徑,__dirname則是該文件所在文件夾的絕對路徑。這連個變量在構(gòu)建當(dāng)前文件的相對路徑時很有幫助,所以ESM提供了一些方法去實(shí)現(xiàn)兩個變量的功能。

在ESM中,可以使用import.meta對象來獲取一個引用,這個引用指的是當(dāng)前文件的URL。具體來說,就是通過import.meta.url來獲取當(dāng)前模塊的文件路徑,這個路徑的格式類似file:///path/to/current_module.js。根據(jù)這條路徑,構(gòu)造出__filename__dirname所表達(dá)的絕對路徑:

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

而且還能模擬CommonJS中require()函數(shù)

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

在ESM的全局作用域中,this是未定義(undefined),但是在CommonJS模塊系統(tǒng)中,它是一個指向exports的引用:

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

// CommonJS
console.log(this === exports) // true
ESM加載CommonJS

上面提到過在ESM中可以模擬CommonJS的require()函數(shù),以此來加載CommonJS的模塊。除此之外,還可以使用標(biāo)準(zhǔn)的import語法引入CommonJS模塊,不過這種引入方式只能把默認(rèn)導(dǎo)出的東西給引進(jìn)來:

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

而CommonJS模塊的require總是將它引用的文件視為CommonJS。不支持使用require加載ES模塊,因?yàn)镋S模塊具有異步執(zhí)行。但可以使用import()從CommonJS模塊中加載ES模塊。

導(dǎo)出雙重模塊

雖然ESM已經(jīng)推出了7年,node.js也已經(jīng)穩(wěn)定支持了,我們開發(fā)組件庫的時候可以只支持ESM。但為了兼容舊項(xiàng)目,對CommonJS的支持也是必不可少的。有兩種廣泛使用的方法可以使得組件庫同時支持兩個模塊系統(tǒng)的導(dǎo)出。

使用ES模塊封裝器

在CommonJS中編寫包或?qū)S模塊源代碼轉(zhuǎn)換為CommonJS,并創(chuàng)建定義命名導(dǎo)出的ES模塊封裝文件。使用條件導(dǎo)出,import使用ES模塊封裝器,require使用CommonJS入口點(diǎn)。舉個例子,example模塊中

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

使用顯示擴(kuò)展名.cjs.mjs,因?yàn)橹挥?code>.js的話,要么是被默認(rèn)為CommonJS,要么"type": "module"會導(dǎo)致這些文件都被視為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都是相同的單例。

隔離狀態(tài)

package.json文件可以直接定義單獨(dú)的CommonJS和ES模塊入口點(diǎn):

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

如果包的CommonJS和ESM版本是等效的,則可以做到這一點(diǎn),例如因?yàn)橐粋€是另一個的轉(zhuǎn)譯輸出;并且包的狀態(tài)管理被仔細(xì)隔離(或包是無狀態(tài)的)

狀態(tài)是一個問題的原因是因?yàn)榘腃ommonJS和ESM版本都可能在應(yīng)用程序中使用;例如,用戶的引用程序代碼可以importESM版本,而依賴項(xiàng)require CommonJS版本。如果發(fā)生這種情況,包的兩個副本將被加載到內(nèi)存中,因此將出現(xiàn)兩個不同的狀態(tài)。這可能會導(dǎo)致難以解決的錯誤。

除了編寫無狀態(tài)包(例如,如果JavaScript的Math是一個包,它將是無狀態(tài)的,因?yàn)樗乃蟹椒ǘ际庆o態(tài)的),還有一些方法可以隔離狀態(tài),以便在可能加載的CommonJS和ESM之間共享它包的實(shí)例:

  • 如果可能,在實(shí)例化對象中包含所有狀態(tài)。比如JavaScript的Date,需要實(shí)例化包含狀態(tài);如果是包,會這樣使用:

import Date from 'date';
const someDate = new Date();
// someDate 包含狀態(tài);Date 不包含

new關(guān)鍵字不是必需的;包的函數(shù)可以返回新的對象,或修改傳入的對象,以保持包外部的狀態(tài)。

  • 在包的CommonJS和ESM版本之間共享的一個或過個CommonJS文件中隔離狀態(tài)。比如CommonJS和ESM入口點(diǎn)分別是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在應(yīng)用程序中通過require和import使用example的每個引用都包含相同的狀態(tài);并且任一模塊系統(tǒng)修改狀態(tài)將適用二者皆是。

到此,關(guān)于“node中的模塊系統(tǒng)是什么”的學(xué)習(xí)就結(jié)束了,希望能夠解決大家的疑惑。理論與實(shí)踐的搭配能更好的幫助大家學(xué)習(xí),快去試試吧!若想繼續(xù)學(xué)習(xí)更多相關(guān)知識,請繼續(xù)關(guān)注億速云網(wǎng)站,小編會繼續(xù)努力為大家?guī)砀鄬?shí)用的文章!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI