震驚7淞帧!就連業(yè)界大佬的為之顫抖的JavaScript

1睁蕾、JavaScript 模塊發(fā)展史

1.1 Vanilla JS(1995~2009)

JavaScript 被開發(fā)出來的時(shí)候瀑凝,是沒有模塊標(biāo)準(zhǔn)的粤咪,因?yàn)?JavaScript 的設(shè)計(jì)初衷就是作為一個(gè) toy script,在瀏覽器中做一些簡單的交互囊拜。但是隨著互聯(lián)網(wǎng)的高速發(fā)展冠跷,人們已經(jīng)不再滿足于簡單的交互,而代碼的復(fù)雜度也日益增長盗冷,維護(hù)難度也越來越高仪糖。

那么維護(hù)指的是維護(hù)什么呢?指的是維護(hù)變量故爵。因?yàn)殡S著項(xiàng)目不斷迭代诬垂,多人協(xié)同開發(fā)是不可避免的。在 JS 初期所有變量都寫在全局作用域上隧枫,那么很可能出現(xiàn)的問題是什么呢官脓?變量的覆蓋斤讥、篡改和刪除,這是一個(gè)很頭疼的問題搀缠。很可能突然有一天你的功能報(bào)錯(cuò)了艺普,就是因?yàn)槟愕哪硞€(gè)變量被另一位開發(fā)者所刪除了。

所以對于模塊的引入初衷是為了解對變量的控制瑰步。當(dāng)然還有其他的好處缩焦,例如對代碼的封裝、復(fù)用等等题翻。

那么初期在沒有模塊標(biāo)準(zhǔn)的支持下,開發(fā)者們是如何實(shí)現(xiàn)類似模塊的效果呢猾普?有 2 種方式初家。

1.1.1 Object?Literal?Pattern(對象字面量)

使用 JS 內(nèi)置的對象對變量進(jìn)行控制:

function Person(name) {

? this.name = name;

}

Person.prototype.talk = function () {

? console.log("my name is", this.name);

};

const p = new Person("anson");

p.talk();

復(fù)制代碼

這樣就可以通過 new Person 的方式把變量都控制在對象內(nèi)部陌知。

1.1.2 IIFE(Immediately Invoked Function Expression)

我們知道在 JavaScript 中有作用域(Scope)的概念,在作用域內(nèi)的變量沿盅,只在作用域內(nèi)可見腰涧。在 ES6 之前,作用域只有 2 種费彼,分別是:

全局作用域(Global Scope)

函數(shù)作用域(Function Scope)

上面提到了對變量的控制箍铲,那么肯定是把變量的作用范圍控制的越小越好,所以毫無疑問把變量寫在函數(shù)內(nèi)是最好的辦法芙粱。但是春畔,這又引發(fā)了另一個(gè)問題,函數(shù)中的變量要如何提供給外部使用呢择份?

這個(gè)問題在初期并沒有很好的解決方法凤价,你必須把變量暴露到全局作用域中利诺,例如經(jīng)典的 jQuery。

而開發(fā)者們通常會使用 IIFE 去實(shí)現(xiàn):

// lib.js

(function() {

? const base = 10;

? this.sumDOM = function(id) {

? ? // 依賴 jQuery

? ? return base + +$(id).text();

? }

})();

復(fù)制代碼

在 HTML 中引入 lib.js:

// index.html

<html>

? <head>

? ? <script src="/path/to/jquery.js"></script>

? ? <script src="/path/to/lib.js"></script>

? </head>

? <body>

? ? <script>

? ? ? window.sumDOM(20);

? ? </script>

? </body>

</html>

復(fù)制代碼

但是 IIFE 有幾個(gè)問題:

至少一個(gè)變量污染全局作用域;

模塊之間的依賴關(guān)系模糊胜卤,不明確(lib.js 不能直觀看出依賴 jquery.js);

加載順序無法保證悠菜,不好維護(hù)(必須確保 jquery.js 必須在 lib.js 前加載完成悔醋,否則會報(bào)錯(cuò))。

所以账阻,JavaScript 非常需要一個(gè)模塊標(biāo)準(zhǔn)來解決上述問題。

1.2 Non-Native Module Format & Module Loader(2009~2015)

由于模塊能為我們解決上述問題蒲牧,所以開發(fā)者嘗試著自己去設(shè)計(jì)一些非原生模塊標(biāo)準(zhǔn)如 CommonJS冰抢、AMD (Asynchronous?Module?Definition)喘蟆、UMD (Universal Module Definition)蕴轨,然后搭配對應(yīng)的 Module Loader 如 cjs-loader、RequireJS棘脐、SystemJS 可以實(shí)現(xiàn)模塊的效果蛀缝,我們下面過一下幾個(gè)流行的非原生模塊標(biāo)準(zhǔn)。

1.2.1 CommonJS (CJS)

2009 年在讶,來自 Mozilla 的工程師 Kevin 提出了為運(yùn)行在瀏覽器以外的 JavaScript 建立一個(gè)模塊標(biāo)準(zhǔn) CommonJS构哺,主要應(yīng)用在服務(wù)端如 Node.js。因?yàn)槭褂眯Ч诲e(cuò)碟嘴,隨后也被用在瀏覽器的模塊開發(fā)中臀防,但由于瀏覽器并不支持 CommonJS,所以代碼需要通過 Babel 等 transpiler 轉(zhuǎn)換為 ES5 才能在瀏覽器上運(yùn)行致燥。

CommonJS 的特征是使用 require 來導(dǎo)入依賴嫌蚤,exports 來導(dǎo)出接口智政。

// lib.js

module.exports.add = function add() {};

// main.js

const { add } = require("./lib.js");

add();

復(fù)制代碼

1.2.2 AMD

因?yàn)?CommonJS 設(shè)計(jì)初衷是應(yīng)用在服務(wù)端的,所以模塊的加載執(zhí)行也都是同步的(因?yàn)楸镜匚募?IO 很快)牙瓢。但是同步的方式運(yùn)用到瀏覽器就不友好了矾克,因?yàn)樵跒g覽器中模塊文件都是通過網(wǎng)絡(luò)加載的,單線程阻塞在模塊加載上汉嗽,這是不可接受的。所以在 2011 年有人提出了 AMD洗做,對 CommonJS 兼容的同時(shí)支持異步加載。

AMD 的特征是使用 define(deps, callback) 來異步加載模塊畦徘。

// Calling define with a dependency array and a factory function

define(['dep1', 'dep2'], function (dep1, dep2) {

? ? //Define the module value by returning a value.

? ? return function () {};

});

復(fù)制代碼

1.2.3 UMD

因?yàn)?CommonJS 和 AMD 的流行,隨后又有人提出了 UMD 的模塊標(biāo)準(zhǔn)杯缺,UMD 通過對不同的環(huán)境特性進(jìn)行檢測萍肆,對 AMD包雀、CommonJS 和 Global Variable 三種格式兼容馏艾。

// UMD

(function (root, factory) {

? if (typeof define === 'function' && define.amd) {

? ? // AMD

? ? define(['jquery', 'underscore'], factory);

? } else if (typeof exports === 'object') {

? ? // Node, CommonJS-like

? ? module.exports = factory(require('jquery'), require('underscore'));

? } else {

? ? // Browser globals (root is window)

? ? root.returnExports = factory(root.jQuery, root._);

? }

}(this, function ($, _) {

? //? ? methods

? function a(){};? ? //? ? private because it's not returned (see below)

? function b(){};? ? //? ? public because it's returned

? function c(){};? ? //? ? public because it's returned

? //? ? exposed public methods

? return {

? ? b: b,

? ? c: c

? }

}));

復(fù)制代碼

因?yàn)?UMD 的兼容性好,不少庫都會提供 UMD 的版本房资。

1.3 ESM(2015~now)

隨著 ECMAScript 的逐漸規(guī)范化轰异、標(biāo)準(zhǔn)化,終于在 2015 年發(fā)布了 ES6(ES 2015)牙肝,在這次版本更新中配椭,制定了 JS 模塊標(biāo)準(zhǔn)即 ES Modules股缸,ES Modules 使用 import 聲明依賴,export 聲明接口镰惦。

// lib.mjs

const lib = function() {};

export default lib;

// main.js

import lib from './lib.mjs';

復(fù)制代碼

截止到 2018 年,大部分主流瀏覽器都已經(jīng)支持 ES Modules眨业,在 HTML 中通過為 <script> 中添加 type="module" 屬性來聲明 ESM 類型龄捡。

在 HTML 中使用 ES Modules 有幾個(gè)注意點(diǎn):

默認(rèn)啟用嚴(yán)格模式即 "use strict"晨雳;

默認(rèn) defer 加載執(zhí)行餐禁;

默認(rèn)啟用 CORS 跨域;

在同一個(gè)文檔中末盔,相同的模塊只會加載陨舱、執(zhí)行一次;

隨著 ES Modules 模塊標(biāo)準(zhǔn)的發(fā)布背桐,JS 的周邊生態(tài)系統(tǒng)也在慢慢向 ES Modules 靠攏。Node.js 在 14.x 添加了對 ES Modules 的支持又沾;Module Bundler 如 Rollup 均以 ES Modules 作為默認(rèn)模塊標(biāo)準(zhǔn)杖刷;還有 Deno、TypeScript 等等表窘。

現(xiàn)在仍然在使用的模塊標(biāo)準(zhǔn)主要就是 CJS 和 ESM瘤袖,CJS 的存在主要是 Node 的歷史原因捂敌。下面我們對 ESM 工作原理進(jìn)行介紹,并結(jié)合 CJS 進(jìn)行對比逆济。

2、ESM 工作原理

在介紹 ES Modules 工作原理前升薯,先理解幾個(gè)概念。

Module Scope(模塊作用域)

我們都知道 ES6 引入了一個(gè)新的作用域:塊作用域(Block Scope)蛛枚,但是還有一個(gè)模塊作用域(Module Scope),用于管理模塊內(nèi)部的變量撞蜂。與函數(shù)作用域不同的是溉贿,在模塊作用域中,你需要顯式地指定導(dǎo)出的變量宣蠕,這也叫作一個(gè) export裳擎;同時(shí)你需要顯式地指定導(dǎo)入的變量,這也叫作一個(gè) import惶我。所以你不再需要污染全局作用域了绸贡。

正因?yàn)槟K之間的依賴關(guān)系是顯示的虑绵、明確的声搁,所以你不用再擔(dān)心你的模塊是否會因?yàn)?jquery 沒有前置加載而報(bào)錯(cuò)了捕发,因?yàn)檫@在編譯階段就會提示你了疏旨。

Module Record(模塊記錄)

當(dāng)我們使用模塊的時(shí)候,實(shí)則是在構(gòu)建一個(gè)模塊依賴圖扎酷。你傳遞一個(gè)模塊文件作為入口(Entry Point)檐涝,JS 引擎根據(jù)模塊中的 import 聲明語句遞歸查詢、下載法挨、解析子模塊。

在這里 main.js 作為入口惫企,然后依賴另外兩個(gè)子模塊 counter.js 和 display.js页畦。

解析指的是把模塊文件解析為一種數(shù)據(jù)結(jié)構(gòu) Module Record,Module Record 記錄了模塊中的 import敬拓、export木人、code 等信息,用于后續(xù)的 Linking、Evaluation绍傲。

Module Environment Record(模塊環(huán)境記錄)

當(dāng) JS 引擎執(zhí)行到一個(gè)作用域時(shí)钩骇,會創(chuàng)建一個(gè) Environment Record(環(huán)境記錄)綁定到該作用域唐瀑,用于存儲作用域內(nèi)的變量和函數(shù)气嫁。Module Environment Record 除了保存著模塊內(nèi)的 top-level 的變量聲明巫员,還保存著模塊內(nèi)的 import 綁定變量陪白。

Environment Record 有一個(gè)很重要的字段 [[OuterEnv]] 用于指向外部的 Environment Record拆吆,這與原型鏈?zhǔn)窒嗨浦羟幔┒藶?null。

如上圖所示 lib-a.js 與 lib-b.js 是兩個(gè)獨(dú)立的模塊掂恕,環(huán)境記錄分別為 ModuleEnvironmentRecord-lib-a 與 ModuleEnvironmentRecord-lib-b灌闺,兩者的 [[OuterEnv]] 都指向 GlobalEnvironmentRecord缀棍,這樣做實(shí)現(xiàn)了模塊之間的變量分離。

ES Modules 工作過程主要可以劃分 3 個(gè)階段:

Construction- 查詢、下載铃岔、解析模塊為 Module Record衅枫;

Linking- 創(chuàng)建 Environment Record 并關(guān)聯(lián)模塊之間的 import粒督、export 關(guān)系花枫;

Evaluation- 執(zhí)行 top-level 代碼并填充 Environment Record腥刹。

大家都說 ESM 是異步執(zhí)行的,是因?yàn)檫@ 3 個(gè)階段是獨(dú)立的边翼、可分離的棺弊,但是這并不表示一定需要使用異步去實(shí)現(xiàn),它也是可以通過同步去執(zhí)行的,例如在 CJS 中就是同步去執(zhí)行的。

因?yàn)樵?ESM spec 里面只說到如何解析 Module Record蹂析;如何做模塊之間的 Linking;如何執(zhí)行模塊的 Evaluation。但是并沒有提到如何獲取到模塊文件豫柬,這在不同的運(yùn)行環(huán)境中由不同的 loader 去負(fù)責(zé)加載完成。對于瀏覽器而言,在 HTML spec 中使用的是異步加載的方式湖雹。

loader 不僅僅負(fù)責(zé)模塊的加載,同時(shí)它負(fù)責(zé)調(diào)用 ESM 的方法如 ParseModule、Module.Link、Module.Evaluate寡喝。loader 控制著這些方法的執(zhí)行順序痘括。

2.1 Construction

Construction 階段主要分為 3 個(gè)步驟:

找到模塊路徑住诸,也叫 module resolution驾胆;

獲取模塊文件(從網(wǎng)絡(luò)下載或從文件系統(tǒng)加載);

解析模塊文件為 Module Record贱呐;

loader 負(fù)責(zé)對模塊進(jìn)行尋址以及下載丧诺。首先我們需要一個(gè)入口文件,這在 HTML 中通常是一個(gè) <script type="module"> 的標(biāo)簽來表示一個(gè)模塊文件(在 Node 中通常使用 *.mjs 來表示一個(gè)模塊文件或修改 package.json 中的 "type": "module")

那模塊是怎么找到下一個(gè)子模塊的呢奄薇?這就需要通過 import 聲明語句了驳阎,在 import 聲明語句中有一部分被稱為 module specifier,這告訴 loader 要如何找到下一個(gè)子模塊的地址。

注意 module specifier 在不同環(huán)境(瀏覽器呵晚、Node)中有不同的解釋方法蜘腌,解釋的過程也叫作 module resolution。例如在瀏覽器中只支持 URL 作為 module specifier饵隙;而 Node 除此以外還支持 bare module specifier撮珠,也就是我們平常寫的 import moment from "moment";。W3C 也在推進(jìn) import maps 特性來支持 bare module specifier金矛。

你只有在解析完當(dāng)前模塊為 Module Record 之后芯急,才知道當(dāng)前模塊依賴的是哪些子模塊,然后你需要 resolve 子模塊绷柒、fetch 子模塊志于、parse 子模塊,不斷的循環(huán)這套流程 resolving -> fetching -> parsing废睦,如下圖所示:

如果整個(gè)過程伺绽,主線程都在等待每個(gè)模塊文件的下載,那么整個(gè)任務(wù)隊(duì)列都會掛起嗜湃。因?yàn)槟阍跒g覽器中下載是很慢的奈应,這也是為什么在 ESM spec 中把模塊加載拆分為 3 個(gè)階段的原因。

階段的拆分也是 CJS 與 ESM 主要的一個(gè)不同點(diǎn)购披,因?yàn)?CJS 加載的都是本地文件杖挣,自然不需要考慮 IO 的問題。這意味著 Node 會阻塞主線程去做這個(gè)模塊的加載動(dòng)作刚陡,接著同步執(zhí)行 Linking惩妇、Evaluation。

上圖代碼執(zhí)行到 require筐乳,然后需要加載子模塊了歌殃,馬上切換到 fetch 子模塊,然后繼續(xù)執(zhí)行 evaluate 子模塊蝙云,這一切都是同步發(fā)生的氓皱。這也是為什么在 Node 中,你可以在 module specifier 中使用變量勃刨。

但是對于 ESM 來說就不同了波材,因?yàn)?ESM 在執(zhí)行 Evaluation 之前,就需要構(gòu)建好整個(gè)模塊依賴圖身隐,這包括所有模塊的resolving廷区、fetching、parsing贾铝。所以 ESM 在 module specifier 中是無法使用變量的躲因。

但是這也有一個(gè)好處早敬,那就是 Rollup、Webpack 等 Module Bundler 可以在編譯時(shí)對 ESM 進(jìn)行靜態(tài)分析大脉,做 Tree Shaking 移除 dead code搞监。

如果實(shí)在想在 ESM 中使用變量作為 module specifier,那么可以使用 dynamic import import(${path}/foo.js) 來導(dǎo)入新的模塊镰矿,新的模塊入口會自動(dòng)創(chuàng)建一個(gè)新的模塊依賴圖琐驴。

雖然是新的模塊依賴圖,但是并不會創(chuàng)建新的 Module Record秤标,loader 使用 Module Map 對全局的 Module Record 進(jìn)行追蹤绝淡、緩存。這樣可以保證模塊文件只被 fetch 一次苍姜。每個(gè)全局作用域中會有一個(gè)獨(dú)立的 Module Map牢酵,也就說每個(gè) iframe 會有獨(dú)立的 Module Map。

可以把 Module Map 想象為一個(gè)簡單的 key/value 映射對象衙猪。例如初次加載的模塊會標(biāo)記狀態(tài)為 fetching馍乙,然后發(fā)起請求,接著繼續(xù) fetch 下一個(gè)模塊文件垫释。

我們可以通過查看下圖來理解 Document 與 Module Map 之間的關(guān)系:

Document 與 Module Map 是一對一的關(guān)系丝格,main.js 有自己的 Module Map;底下的 iframe-a棵譬、iframe-b 也會有自己的 Module Map显蝌。所以盡管它們內(nèi)部依賴模塊的地址是一樣的,仍然會重復(fù)去請求下載订咸。

好曼尊,那么下載完文件后,JS 引擎會把模塊文件解析為 Module Record脏嚷,保存著模塊中的 import涩禀、export、code 等信息然眼。

Module Record 會放置到 Module Map 中緩存。

2.2 Linking

在所有 Module Record 被解析完后葵腹,接下來 JS 引擎需要把所有模塊進(jìn)行鏈接高每。JS 引擎以入口文件 main.js 的 Module Record 作為起點(diǎn),以深度優(yōu)先的順序去遞歸鏈接模塊践宴,為每個(gè) Module Record 創(chuàng)建一個(gè) Module Environment Record鲸匿,用于管理 Module Record 中的變量。

具體是如何進(jìn)行鏈接的呢阻肩?JS 引擎會對當(dāng)前模塊 main.js 下的所有子模塊 counter.js带欢、display.js 創(chuàng)建 Module Environment Record运授,對子模塊中的 export 變量進(jìn)行綁定,為其分配內(nèi)存空間乔煞。

然后控制權(quán)回到上一級吁朦,也就是當(dāng)前模塊 main.js,對 main.js 中 import 的變量進(jìn)行關(guān)聯(lián)渡贾,注意這里 main.js 中 import 指向的內(nèi)存位置與 count.js逗宜、display.js 中 export 變量指向的內(nèi)存位置是一致的,這樣就把父子模塊之間的關(guān)系鏈接起來了空骚。

但是在 CJS 在這一點(diǎn)上面不同纺讲。在 CJS 里面,會對整個(gè) module.exports 對象進(jìn)行復(fù)制囤屹。

這意味著 exporting module 在后面修改變量值熬甚,importing module 并不會自動(dòng)更新。

相反肋坚,ESM 用的一種技術(shù)叫作 live bindings乡括。父子模塊指向相同的內(nèi)存位置,所以 exporting module 修改變量值冲簿,importing module 會馬上得到更新粟判。

需要注意的是,只有 exporting module 才可以對 export 變量值進(jìn)行改變峦剔,importing module 是無法改變档礁。可以說 exporting 模塊有讀寫權(quán)限吝沫,而 importing 模塊只有讀權(quán)限呻澜。

使用 live bindings 的一個(gè)原因是,它可以幫助把所有模塊關(guān)聯(lián)起來惨险,而不需要跑任何代碼羹幸。這在當(dāng)我們 Evaluation 遇到循環(huán)依賴(cyclic dependencies)的時(shí)候很有幫助。

下面我們要開始執(zhí)行代碼辫愉,并填充上面的內(nèi)存了栅受。

2.3 Evaluation

在模塊彼此鏈接之后,JS 引擎通過執(zhí)行模塊中的 top-level 代碼來實(shí)現(xiàn)恭朗,所以你的 import屏镊、export 語句是不能寫在函數(shù)里面的。

但是執(zhí)行 top-level 代碼是可能會產(chǎn)生副作用痰腮,例如發(fā)送網(wǎng)絡(luò)請求而芥,所以你肯定不希望同一個(gè)模塊執(zhí)行多次。這也是為什么會使用 Module Map 來做全局緩存 Module Record 的原因膀值,如果一個(gè) Module Record 的狀態(tài)為 evaluated棍丐,那么下次執(zhí)行會自動(dòng)跳過误辑,從而保證一個(gè)模塊只會執(zhí)行一次。與 Linking 階段一樣的是歌逢,同樣是對 Module Record 執(zhí)行深度優(yōu)先遍歷的操作巾钉。

在 Linking 結(jié)尾提到的依賴循環(huán)問題,通常是錯(cuò)綜復(fù)雜的依賴循環(huán)趋翻,這里以簡單的例子說明下:

main.js 與 counter.js 之間循環(huán)依賴彼此睛琳。 我們先來看看 CommonJS 中的依賴循環(huán)問題:

首先 main.js 執(zhí)行到 require("./counter.js"),然后進(jìn)入 counter.js 執(zhí)行獲取 main.js 中的 message踏烙,而這時(shí)是 undefined 的师骗,所以 counter.js 復(fù)制了 undefined。

在 counter.js 執(zhí)行完成后(注意最后我們設(shè)置了一個(gè) setTimeout 來查看 message 是否會自動(dòng)更新)讨惩,在控制權(quán)返回到 main.js 繼續(xù)執(zhí)行代碼辟癌,最后對 message 賦值為 "Eval complete"。

但因?yàn)樵?CommonJS 中 import 的變量值是對 export 變量值的復(fù)制荐捻,所以 counter.js 中的 message 并不會更新黍少。

而 ES Modules 使用的是 live bindings,所以在 counter.js 中會自動(dòng)更新 message 的值处面。

3厂置、CJS 與 ESM 之間的混用

因?yàn)闅v史原因,npm 上大多數(shù)的包都使用 CJS 編寫魂角,但是隨著 ESM 的出現(xiàn)昵济,開發(fā)者們開始使用 ESM 去編寫模塊。而為了最大程度復(fù)用 npm 上的包野揪,在 ESM 中難免會需要導(dǎo)入 CJS访忿。而因?yàn)槟K加載方式的差異性,CJS 無法導(dǎo)入 ESM斯稳,而 ESM 可以導(dǎo)入 CJS海铆。

雖然 ESM 可以導(dǎo)入 CJS,但是使用上仍然有些限制挣惰。

3.1 ESM 只支持 default import CJS

ESM 支持 CJS 的 default import卧斟,但是不支持 named import,即:

import pkg from 'lib.cjs'; // work

import { fn1, fn2 } from 'lib.cjs'; // error

復(fù)制代碼

為什么呢憎茂?結(jié)合上面的 ESM 工作原理珍语,ESM 是對模塊變量進(jìn)行靜態(tài)分析的,而 CJS 的模塊變量是動(dòng)態(tài)計(jì)算的唇辨。所以 ESM 還在還沒執(zhí)行代碼的第一階段 Construction,又如何能計(jì)算出 CJS 的模塊變量呢能耻?

但是在 Node 14.13.0 版本中赏枚,Node 添加了對 CJS named export 的支持亡驰,可以支持大部分的 CJS 模塊。

為什么說大部分呢饿幅?Node官網(wǎng)做出了說明:

The detection of named exports is based on common syntax patterns but does not always correctly detect named exports. In these cases, using the default import form described above can be a better option.

注意 detect 這一關(guān)鍵字凡辱,它是基于的 CJS 模塊語法對文本分析得到 named exports,所以并不能保證正確栗恩。在這種情況透乾,使用 default import 是更好的選擇。

Node 使用了一個(gè)叫做的 cjs-module-lexer 的語法分析庫磕秤,對 CJS 模塊內(nèi)容進(jìn)行靜態(tài)語法分析乳乌,只支持簡單的 exports 寫法,如 exports.name = ... 或 module.exports = require('...')市咆,這里舉個(gè)可被分析的例子:

// correct.cjs

exports.a = 1;

exports.b = 2;

if (Math.random() > 0.5) {

? exports.c = 3;

}

// main.mjs

import { a, b, c } from './correct.cjs';

// 執(zhí)行 main.mjs 無異常

復(fù)制代碼

無法分析的例子:

// wrong.cjs

// 使用 tmp 來設(shè)置 exports

const tmp = exports;

tmp.a = 1;

tmp.b = 2;

if (Math.random() > 0.5) {

? tmp.c = 3;

}

// main.mjs

import { a, b, c } from './wrong.cjs';

// 執(zhí)行 main.mjs 報(bào)錯(cuò)

復(fù)制代碼

執(zhí)行上面的例子會報(bào)以下錯(cuò)誤:

file:///E:/javascript-modules/esm-app/dual/index.mjs:1

import { a, b, c } from "./lib.cjs";

? ? ? ? ^

SyntaxError: Named export 'a' not found. The requested module './lib.cjs' is a CommonJS module, which may not support all module.exports as named exports.

CommonJS modules can always be imported via the default export, for example using:

import pkg from './lib.cjs';

const { a, b, c } = pkg;

復(fù)制代碼

你可能會想汉操,誰會這么寫阿,不巧蒙兰,蠻多有名的庫是這么寫的磷瘤,例如 lodash、chalk搜变。

對于無法分析 named exports 的模塊采缚,Node 會在錯(cuò)誤里面給我們提出建議使用 default import,然后再進(jìn)行解構(gòu)挠他,也就多一行代碼:

CommonJS modules can always be imported via the default export, for example using:

import pkg from './lib.cjs'; const { a, b, c } = pkg;

3.2 使用 ESM Wrapper 為 CJS 實(shí)現(xiàn) named exports

如果我們實(shí)在希望在 ESM 中使用 named exports CJS扳抽,那么我們可以為 CJS 提供一個(gè) ESM Wrapper,其實(shí)就是根據(jù) Node 的錯(cuò)誤提示去封裝一層代碼绩社,對 CJS 采用 default import摔蓝,然后對里面指定的變量 re-export 一次:

// lib.cjs

const tmp = exports;

tmp.a = 1;

tmp.b = 2;

if (Math.random() > 0.5) {

? tmp.c = 3;

}

// lib-esm-wrapper.mjs

import lib from "./lib.cjs";

export const { a, b, c } = lib;

// main.mjs

import { a, b, c } from "./lib-esm-wrapper.mjs";

console.log(a);

console.log(b);

console.log(c);

復(fù)制代碼

所以當(dāng)用戶需要 ESM 模塊,而當(dāng)前只有 CJS 模塊時(shí)愉耙,可以考慮編寫一個(gè)簡單的 ESM Wrapper 進(jìn)行包裝贮尉。

4、編寫支持多種模塊格式的庫

有時(shí)候我們在編寫庫的時(shí)候朴沿,希望我們的庫支持 CJS 和 ESM 兩種格式猜谚,大家可能對 package.json 的 module 字段比較熟悉,它是一個(gè)約定俗成的字段赌渣,主要用在 Module Bundler 如 Webpack魏铅、Rollup 對包是否支持 ESM 的檢查,然而 Node 并不會對該字段識別坚芜。

在 Node 12+ 我們可以使用 package.json 的 exports 字段來為包配置支持不同的模塊文件览芳,Node 會根據(jù)你使用 import 還是 require 來加載,返回相應(yīng)的模塊文件:

// package.json

{

? "exports": {

? "import": "./lib.mjs",

? ? "require": "./lib.cjs"

? }

}

// app.mjs

import { value } from "lib";

console.log("value from mjs", value);

// app.cjs

const value = require("lib").value;

console.log("value from cjs", value);

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末鸿竖,一起剝皮案震驚了整個(gè)濱河市沧竟,隨后出現(xiàn)的幾起案子铸敏,更是在濱河造成了極大的恐慌,老刑警劉巖悟泵,帶你破解...
    沈念sama閱讀 219,539評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件杈笔,死亡現(xiàn)場離奇詭異,居然都是意外死亡糕非,警方通過查閱死者的電腦和手機(jī)蒙具,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,594評論 3 396
  • 文/潘曉璐 我一進(jìn)店門土榴,熙熙樓的掌柜王于貴愁眉苦臉地迎上來片效,“玉大人,你說我怎么就攤上這事投储【铣剩” “怎么了融师?”我有些...
    開封第一講書人閱讀 165,871評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長蚁吝。 經(jīng)常有香客問我旱爆,道長,這世上最難降的妖魔是什么窘茁? 我笑而不...
    開封第一講書人閱讀 58,963評論 1 295
  • 正文 為了忘掉前任怀伦,我火速辦了婚禮,結(jié)果婚禮上山林,老公的妹妹穿的比我還像新娘房待。我一直安慰自己,他們只是感情好驼抹,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,984評論 6 393
  • 文/花漫 我一把揭開白布桑孩。 她就那樣靜靜地躺著,像睡著了一般框冀。 火紅的嫁衣襯著肌膚如雪流椒。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,763評論 1 307
  • 那天明也,我揣著相機(jī)與錄音宣虾,去河邊找鬼。 笑死温数,一個(gè)胖子當(dāng)著我的面吹牛绣硝,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播撑刺,決...
    沈念sama閱讀 40,468評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼鹉胖,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起甫菠,我...
    開封第一講書人閱讀 39,357評論 0 276
  • 序言:老撾萬榮一對情侶失蹤败许,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后淑蔚,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,850評論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡愕撰,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,002評論 3 338
  • 正文 我和宋清朗相戀三年刹衫,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片搞挣。...
    茶點(diǎn)故事閱讀 40,144評論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡带迟,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出囱桨,到底是詐尸還是另有隱情仓犬,我是刑警寧澤,帶...
    沈念sama閱讀 35,823評論 5 346
  • 正文 年R本政府宣布舍肠,位于F島的核電站搀继,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏翠语。R本人自食惡果不足惜叽躯,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,483評論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望肌括。 院中可真熱鬧点骑,春花似錦、人聲如沸谍夭。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,026評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽紧索。三九已至袁辈,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間齐板,已是汗流浹背吵瞻。 一陣腳步聲響...
    開封第一講書人閱讀 33,150評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留甘磨,地道東北人橡羞。 一個(gè)月前我還...
    沈念sama閱讀 48,415評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像济舆,于是被迫代替她去往敵國和親卿泽。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,092評論 2 355

推薦閱讀更多精彩內(nèi)容

  • 原文鏈接:The state of JavaScript modules 最近 在 twitter 上有很多關(guān)于 ...
    沮溺閱讀 1,314評論 0 1
  • 原文:Using ES modules natively in Node.js作者:Axel Rauschmaye...
    沮溺閱讀 1,080評論 0 1
  • 2017-09-12: Node.js v8.5.0 發(fā)布齐邦,在這個(gè)版本中,正式以實(shí)驗(yàn)性的方式支持 ES6 Modu...
    XGHeaven閱讀 12,037評論 0 3
  • 模塊化開發(fā) 一第租、模塊化的優(yōu)缺點(diǎn) 優(yōu)點(diǎn): 相比于使用一個(gè)js文件措拇,這種多個(gè)js文件實(shí)現(xiàn)最簡單的模塊化的思想是進(jìn)步的。...
    圓圈圓circle閱讀 521評論 0 0
  • 模塊化開發(fā)是當(dāng)下最重要的前端開發(fā)范式之一 模塊化演變過程 Stage1 文件劃分方式具體的做法就是每個(gè)功能及其相關(guān)...
    amanohina閱讀 1,108評論 1 9