javascript
vivado顶层模块怎么建_【第2040期】Node 模块化之争:为什么 CommonJS 和 ES Modules 无法相互协调...
前言
又到周五了。今日早讀文章由Shopee@周雨楠翻譯授權分享。
@周雨楠,Shopee金融事業群前端研發,自主學習前端技術3年,喜愛各類數字媒體技術、創意設計,多次參與翻譯工作。
福利:有兩張門票,有需要的跟@情封聯系,先到先得。
正文從這開始~~
兩者可以進行適配,但是會徒增負擔。
在 Node 14 版本下,現存兩類語法:老式的 CommonJS (CJS) 的語法和新式的 ESM 語法(aka MJS)。CJS 使用 require() 和 module.exports;ESM 使用 import 和 export。
ESM 和 CJS 可以看作是完全不同的動物。表面上看,ESM 和 CJS 很像,但是他們的實現卻是大相徑庭。如果說一個是蜜蜂,那么另一個就是殺人蜂。
圖中是一只黃蜂和一只蜜蜂。其中一個好比 ESM,另一個好比 CJS,但是我永遠記不住哪個是哪個。圖片來源:wikimedia,wikimedia
無論是在 ESM 中使用 CJS 還是反過來,都是有可能的,但這是徒增負擔。
以下是一些規則,我會在后文中詳細解釋。
在 ESM 代碼中無法使用 require();你只能?importESM代碼,比如:import{foo}from'foo'
CJS 代碼無法使用如上所示的靜態 import 語句;
ESM 代碼可以 import CJS 代碼,但是只能使用“默認導入(default import)”語法,如?import_from'lodash',而不是“命名導入(named import)”語法,如?import{shuffle}from'lodash',因此如果 CJS 代碼使用了命名導出,就會很麻煩;
ESM 代碼可以 require() CJS 代碼,即便是命名導出也可以,但是明顯不值得大費周章,因為這樣需要更多的框架平臺,而且最不好的一點就是諸如 Webpack 和 Rollup 這樣的包,不知道,也不會知道怎么處理含有 require() 的 ESM 代碼;
CJS 是默認允許使用的,而 ESM 模式則需要你選擇性加入。通過把代碼文件從 .js 重命名為 .mjs 就可以啟用 ESM 模式。除此之外,在 package.json 中設置 "type": "module",然后就可以通過把 .js 重命名為 .cjs 選擇退出 ESM 模式。(你甚至可以在某一個子目錄下添加一個只有一行 {"type": "module"} 的 package.json 文件來調整。)
這些條條框框太痛苦了。對于很多使用者,尤其是 Node 入門者來說是更為痛苦的,這些規則壓根不可理喻。(不慌,這篇文章里我都將解釋清楚。)
很多 Node 生態的關注者已經發現這些規則是由于先前領導的失敗,甚至是對 ESM 的敵意導致的。不過正如下文所說,所有的規則都有其存在的意義,這使得未來想要打破這些規則也很難。
我為開源庫的開發者整理了三條指南用于借鑒:
為你的開源庫提供一個 CJS 的版本;
為你的 CJS 版本提供一個較淺的 ESM 封裝;
在你的 package.json 文件中添加一個 exports 的映射。
一切就會好起來了。
背景介紹:CJS 是什么?ESM 又是什么?
從 Node 初見以來,Node 中的模塊就是以 CommonJS 模塊來寫的。我們使用 require() 來引入它們。當實現了一個模塊并且想讓他人使用時,我們就會定義 exports 內容,要么通過設置 module.exports.foo='bar' 進行“命名導出”,要么通過設置 module.exports = 'baz' 進行“默認導出”。
這是一個 CJS 使用命名導出的例子,util.cjs 有一個命名為 sum 的導出函數。
// 文件名: util.cjs
module.exports.sum = (x, y) => x + y;
// 文件名: main.cjs
const {sum} = require('./util.cjs');
console.log(sum(2, 4));
這是一個 CJS 在 util.cjs 中使用默認導出的例子。默認導出是不指定名字的,而是由使用 require() 的模塊自行定義名稱。
// 文件名: util.cjs
module.exports = (x, y) => x + y;
// 文件名: main.cjs
const whateverWeWant = require('./util.cjs');
console.log(whateverWeWant(2, 4));
在 ESM 代碼中,import 和 export 是這類語言的一部分。和 CJS 類似,它也有兩套不同的語法進行命名導出和默認導出。
這是一個 ESM 使用了命名導出的例子,util.mjs 有一個命名為 sum 的導出函數。
// 文件名: util.mjs
export const sum = (x, y) => x + y;
// 文件名: main.mjs
import {sum} from './util.mjs'
console.log(sum(2, 4));
這是一個 ESM 在 util.mjs 中設置了默認導出的例子。和 CJS 中一樣,默認導出是沒有名字的,但是使用了 import 的模塊會自行定義名稱。
// 文件名: util.mjs
export default (x, y) => x + y;
// 文件名: main.mjs
import whateverWeWant from './util.mjs'
console.log(whateverWeWant(2, 4));
ESM 和 CJS 是截然不同的動物
在 CommonJS 中,require() 是同步的。它不會返回一個 promise 或者調用回調函數。require() 從硬盤(或者甚至從網絡)中進行讀操作,然后立刻執行代碼。這樣就會使得它自行進行 I/O 或產生其它副作用,然后返回任何設置在 module.exports 上的值。
在 ESM 中,模塊加載器是在異步階段執行的。在第一個階段,它會做詞法分析,在不執行導入代碼的情況下檢測是否存在 import 和 export 的調用。在詞法轉換階段,ESM 加載器能夠立刻檢測到命名導入中的拼寫錯誤,并且在不執行依賴代碼的情況下拋出異常。
ESM 加載器接下來異步地下載并轉譯任何引入的代碼,然后對引入的代碼進行編碼,根據依賴建立出一個“模塊圖(module graph)”,直到最后它發現某塊代碼沒有引入任何東西。最后,這一塊代碼被允許執行,然后所有這一塊代碼所依賴的代碼被允許執行,依次類推。
ES 模塊圖中所有具有“兄弟”關系的代碼都是并行下載的,但是是按照次序執行的。這一次序由加載器指定并確保執行。
CJS 是默認模式,因為 ESM 改變了很多東西
ESM 改變了 JavaScript 中的很多東西。ESM 語法默認使用嚴格模式(use strict),它們的 this 不指向全局對象,作用域也有差異,等等。
這就是為什么甚至在瀏覽器中 標簽默認也不是 ESM 模式的。要添加一個 type="module" 屬性來選擇進入 ESM 模式。
從默認的 CJS 切換到 ESM 在向前兼容性方面存在很大斷層。(最近炙手可熱的 Node 替代品 Deno 將 ESM 作為默認,但是其結果就是 Deno 的生態環境要從零開始搭建。)
CJS 無法 require() ESM,因為有頂層的 await 限制
CJS 無法 require() ESM 的最簡單原因就是 ESM 可以進行最外層的 await ,但是 CJS 代碼不行。
頂層 await 能夠讓我們在 async 函數的外層使用 await 關鍵字,也就是處于“頂層”。
ESM 的多階段加載器使得 ESM 實現頂層 await 時不會搬起石頭砸自己的腳。從V8團隊的博客文章中引用一些話:
也許你讀過 Rich Harris 寫的臭名昭著的 gist,一開始就羅列了一些所擔心的關于頂層 await 的問題,并且迫切希望 JavaScript 語言不要實現出來。其中的一些問題是:
頂層 await 會阻塞執行;
頂層 await 會阻塞資源獲取;
CommonJS 模塊沒法再做清晰的內嵌了。
第三階段版本的提議也強調了這些問題:
因為兄弟關系的代碼是可以執行的,因此最終沒有造成阻塞;
頂層 await 出現在模塊圖的執行階段。在此階段,所有的資源都已經獲得并且建立了鏈接,因此是不存在阻塞資源獲取風險的;
頂層 await 僅僅限制在 ESM 模塊中使用,CommonJS 的模塊或者代碼中明確沒有對此的支持。
(Rich 現在已經同意了當前頂層 await 的實現。)
因為 CJS 不支持頂層 await,那么從 ESM 的頂層 await 轉譯為 CJS 就是不可能的。在 CJS 中怎么重寫這段代碼呢?
export const foo = await fetch('./data.json');
有點打擊人,因為絕大多數 ESM 代碼不會去使用頂層 await,但是正如這一條 thread 中的一個評論者所說,“我并不認為設計系統的時候,單單假定一些功能不會被使用,是一條可行的路。”
如何在 ESM 中進行 require() 的問題,在這條 thread 上依舊激烈爭論著。(請看完整條 thread 和其中關聯的討論后再進行評論。如果你深入研究,你就會發現頂層 await 并不是唯一一個有著問題的情形。你覺得如果你同步 require 一個能夠異步 import 一些能夠同步 require ESM 的 CJS 的 ESM 會發生什么呢?你就會得到像斑馬條紋那樣一會同步一會異步的能整死人的東西。頂層 await 就是棺材板上的最后一根釘子,也是最容易解釋的一個。)
通過對那些討論進行評審,似乎我們不再會在 ESM 里做 require() 了。
CJS 能夠 import() ESM,但是這并不好
目前為止,如果你在寫 CJS,你想 import 一段 ESM 代碼,你得使用異步動態的 import()。
(async () => {
const {foo} = await import('./foo.mjs');
})();
看上去……還行,只要別有 exports 就行。如果你需要做 exports,你就得導出一個 Promise,這對于你的用戶來說會是一個大大的不便。
ESM 無法引入命名引出的 CJS,除非 CJS 代碼脫離執行順序
你可以這樣寫:
import _ from './lodash.cjs'
但是你沒法這樣寫:
import {shuffle} from './lodash.cjs'
這是因為 CJS 代碼會在執行的時候計算它們的命名導出,而 ESM 的命名導出必須在轉譯階段才會被計算。
對我們而言,幸運的是有曲線救國的方式!這個曲線十分惱人,但是還是能做的。我們這樣引入 CJS 代碼就可以了:
import _ from './lodash.cjs';
const {shuffle} = _;
這樣做沒什么特別的弊端,而且感知了 ESM 的 CJS 庫甚至能夠提供它們自己的 ESM 包裹層,為我們封裝了這樣的寫法框架。
完全沒問題!要是能更好點就好了。
脫離執行順序也能工作,但是有更壞的結果
有一部分的人提出,在 ESM 引入之前執行 CJS 的引入是脫離了執行順序的。這樣一來,CJS 的命名導出會和 ESM 的命名導出在同時計算。
但是這樣就會產生一個新的問題。
import {liquor} from 'liquor';
import {beer} from 'beer';
如果 liquor 和 beer 最初都是 CJS,把 liquor 從 CJS 換成 ESM 就會使得順序從 liquor, beer 變成 beer, liquor,那么如果 beer 中依賴 liquor 中先執行的內容,這樣就會令人嘔吐地有問題。
脫離順序的執行依然在爭論當中, 雖然幾周之前這個話題就幾乎沒啥聲音了。
動態模塊能拯救,但是它們的星號有毒
有一個替代方案的提議,既不需要脫離執行順序,也不需要做封裝,稱作動態模塊(Dynamic Modules)。
在 ESM 中,導出的地方會靜態定義所有命名導出。在動態模塊方案下,引入的地方會在 import 中定義導出的名字。ESM 加載器一上來會信任動態模塊(CJS 代碼)能夠提供所有需要的命名導出,如果之后有地方不滿足,則再拋出一個異常。
然而,動態模塊需要 JavaScript 語言發生一些變化,這些變化需要 TC39 語言委員會進行同意。而他們不同意。
ESM 代碼可以 export*from'./foo.cjs',這意思是重新把 foo 中導出的所有名字進行導出。(稱為“星號導出(star export)”。?)
然而,如果我們從動態模塊中星號導出,加載器就無法知道導出的是什么。
動態模塊的星號導出在規范合格性上也產生了問題。比如, export*from'omg';export*from'bbq'; 應該拋出異常,因為 omg 和 bbq 都導出了相同名字的 wtf。允許這些命名能夠被用戶/消費者進行定義,意味著這個合法性校驗階段需要被滯后處理或者忽略。
動態模塊的提倡者提議在動態模塊中禁止星號導出方式,但是 TC39 拒絕了這一提議。一個 TC39 的成員把這個提議比作是“語法毒(syntax poisoning)“,因為星號導入在動態模塊中就像是被“下毒”了一樣。
這個帶毒的星星對你很生氣。圖片來源:seekpng在我看來,我們已經居住在一個語法毒的世界里了。在 Node 14 版中,命名導出就是被下毒的,在動態模塊中,星號導入也是被下毒的。因為命名導出極其普遍而星號導出相對罕見,動態模塊會在生態中減少語法毒的成分。
這并不意味著動態模塊已經窮途末路。案上依然有提議,讓所有 Node 模塊都成為動態模塊,甚至帶上純 ESM 模塊,并且在 Node 中棄用 ESM 的多階段加載器。讓人眼前一亮的是,這樣并不會產生用戶可見的影響,除了一些可能發生的輕微啟動性能下降。ESM 多階段加載器是在網絡緩慢的情況下加載代碼而設計的。
不過我依然不覺得會這么走運。Github 上關于動態模塊的 issue 最近被關閉了,因為去年沒有關于動態模塊的討論。
還有一個方案懸而未決,那就是做一次充分努力的嘗試,把 CJS 模塊進行詞法分析,從而檢測出導出內容,但是這個方案不可能在 100% 的用例中使用。(最新的 PR 在 npm 前 1000 的模塊中只有 62% 正常工作。)因為這種啟發式的東西太不可靠,一些 Node 模塊工作組成員是反對的。
ESM 可以 require(),但是很可能并不值得
require() 默認并不在 ESM 代碼范疇內,不過你可以輕松把它找回。
import { createRequire } from 'module';
const require = createRequire(import.meta.url);
const {foo} = require('./foo.cjs');
這個方法的問題在于它沒能幫多大忙。實際上也就比做一個默認導入然后解構多了幾行代碼。
import cjsModule from './foo.cjs';
const {foo} = cjsModule;
另外,像 Webpack 和 Rollup 這樣的打包工具并不知道如何處理 createRequire 這樣的模式,所以意義何在呢?
如何創建一個良好的包含了 CJS 和 ESM 的“二重包”
如果你手上至今都維護著一個庫,需要支持 CJS 和 ESM,那么就給你的用戶做點好事,按照上文的方針建造一個“二重包”,能夠在 CJS 和 ESM 下都良好工作。
給庫提供一個 CJS 的版本
這是為了方便你的 CJS 用戶。同時也確保了你的庫能夠在 Node 的早期版本中正常工作。
如果你使用的是 TypeScript 或者其它最終轉譯成 JS 的語言,那么就轉譯成 CJS 吧。
給 CJS 提供一個淺的 ESM 封裝
注意,給 CJS 庫寫一個 ESM 包裹層是不難的,但是給 ESM 庫寫一個 CJS 包裹層就不可能了。
import cjsModule from '../index.js';
export const foo = cjsModule.foo;
把 ESM 包裹層 放到一個 esm 的子目錄下,同時放入一個一行的 package.json,里面只放 {"type": "module"}。(你可以重命名你的包裹層文件為 .mjs,在 Node 14 下是正常的,但是有的工具和 .mjs 搭配不好,因此我傾向于使用一個子目錄。)
避免二次轉譯。如果你是在從 TypeScript 做轉譯,你可以轉譯成 CJS 和 ESM,但是這就會帶來一個潛在的危害,用戶可能偶然既 import 了你的 ESM 代碼,又 require() 了你的 CJS 代碼。(比如,假設一個庫 omg.mjs 依賴于 index.mjs,另一個庫 bbq.cjs 依賴于 index.cjs,然后你還既要依賴 omg.mjs 又要依賴 bbq.cjs。)
Node 自身會給模塊做去重,不過 Node 并不知道你的 CJS 和 ESM 其實是”相同的“文件,于是你的代碼就會執行兩次,并且保留你的庫狀態的兩份拷貝。這就能引發各種奇異的 Bug。
給你的 package.json 添加一個 exports 映射
就像這樣:
"exports": {
"require": "./index.js",
"import": "./esm/wrapper.js"
}
注意:添加一個 exports 映射永遠要作為“語義化版本控制中的主要層級”的重大變化。默認情況下,你的用戶能夠進入你的包,然后 require() 任何他們想要的代碼,甚至是你想要變成內部層的文件。exports 映射確保了用戶只能 require/import 你刻意暴露出來的入點文件。
這就快是一個好的東西了!但是這也是一個重大變化。
如果你跟著你的用戶進行 import 或者 require() 你的模塊里的其他文件,你也可以分開來設置入點。具體請查閱 ESM 的 Node 文檔。
始終要在導出映射目標中包含文件擴展名。寫成 "index.js" 而不是 "index" 或者一個類似 "./build" 的目錄。
如果你遵循了上述的方針,你的用戶就會很安分。一切都會變得很安分。
關于本文 譯者:@周雨楠 譯文:https://mp.weixin.qq.com/s/CxlolXUpK02wZbRNah_ZQA 作者:@Dan Fabulich 原文鏈接:https://redfin.engineering/node-modules-at-war-why-commonjs-and-es-modules-cant-get-along-9617135eeca1
為你推薦
【第1994期】ES11來了
【第1899期】調研 Federated Modules,應用秒開,應用集方案,微前端加載方案改進等
歡迎自薦投稿,前端早讀課等你來
總結
以上是生活随笔為你收集整理的vivado顶层模块怎么建_【第2040期】Node 模块化之争:为什么 CommonJS 和 ES Modules 无法相互协调...的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: python的知识点运用_程序猿在Pyt
- 下一篇: div和div之间画横线_javascr