常見問題
為何 ES 模組比 CommonJS 模組好?
ES 模組是 JavaScript 程式碼結構的官方標準和明確前進方向,而 CommonJS 模組是一種特殊遺留格式,在 ES 模組提出之前作為權宜解決方案。ES 模組允許靜態分析,有助於執行樹狀搖晃和範圍提升等最佳化,並提供循環參考和動態繫結等進階功能。
什麼是「樹狀搖晃?」
樹狀搖晃,也稱為「動態程式碼包含」,是 Rollup 的程序,用於消除特定專案中實際上未使用的程式碼。它是 一種形式的死程式碼消除,但相較於其他方法,在輸出大小方面可能更有效率。這個名稱源自模組的 抽象語法樹(而非模組圖表)。此演算法會先標記所有相關陳述,然後「搖晃語法樹」以移除所有死程式碼。其概念類似於 標記清除垃圾回收演算法。雖然此演算法不限於 ES 模組,但它們讓 Rollup 能將所有模組視為一個具有共用繫結的大型抽象語法樹,因此讓演算法更有效率。
如何在 Node.js 中使用 Rollup 搭配 CommonJS 模組?
Rollup 致力於實作 ES 模組的規範,而不一定實作 Node.js、NPM、require()
和 CommonJS 的行為。因此,CommonJS 模組的載入和使用 Node 的模組位置解析邏輯都實作為選用外掛程式,未預設包含在 Rollup 核心。只要使用 npm install
安裝 commonjs 和 node-resolve 外掛程式,然後使用 rollup.config.js
檔案啟用它們,即可設定完成。如果模組匯入 JSON 檔案,您還需要 json 外掛程式。
為何 node-resolve 不是內建功能?
主要有兩個原因
從哲學角度來看,這是因為 Rollup 本質上是一種 polyfill,用於 Node 和瀏覽器中的原生模組載入器。在瀏覽器中,
import foo from 'foo'
無法運作,因為瀏覽器不使用 Node 的解析演算法。從實務角度來看,如果這些問題能透過良好的 API 清楚區分,開發軟體會容易得多。Rollup 的核心相當龐大,任何能避免它變更大的做法都是好事。同時,這樣也能更容易修正錯誤並新增功能。透過保持 Rollup 精簡,技術負債的潛在風險會很小。
請參閱 此議題 以取得更詳細的說明。
當進行程式碼分割時,為何會在入口區塊中出現額外的匯入?
預設情況下,在建立多個區塊時,入口區塊的依賴項匯入會新增為入口區塊本身的空匯入。 範例
// input
// main.js
import value from './other-entry.js';
console.log(value);
// other-entry.js
import externalValue from 'external';
export default 2 * externalValue;
// output
// main.js
import 'external'; // this import has been hoisted from other-entry.js
import value from './other-entry.js';
console.log(value);
// other-entry.js
import externalValue from 'external';
var value = 2 * externalValue;
export default value;
這不會影響程式碼執行順序或行為,但會加快程式碼載入和解析的速度。沒有這個最佳化,JavaScript 引擎需要執行以下步驟來執行 main.js
- 載入並解析
main.js
。最後,會發現匯入other-entry.js
。 - 載入並解析
other-entry.js
。最後,會發現匯入external
。 - 載入並解析
external
。 - 執行
main.js
。
透過這個最佳化,JavaScript 引擎會在解析一個入口模組後發現所有遞移相依性,避免瀑布
- 載入並解析
main.js
。最後,會發現匯入other-entry.js
和external
。 - 載入並解析
other-entry.js
和external
。other-entry.js
中匯入的external
已經載入並解析。 - 執行
main.js
。
在某些情況下,這個最佳化是不需要的,在這種情況下,你可以透過 output.hoistTransitiveImports
選項將其關閉。當使用 output.preserveModules
選項時,這個最佳化也永遠不會套用。
如何將 polyfill 新增到 Rollup 捆綁?
即使 Rollup 通常會在捆綁時盡量維持精確的模組執行順序,但在以下兩種情況下,並非總是如此:程式碼分割和外部相依性。這個問題在外部相依性中是最明顯的,請參閱以下 範例
// main.js
import './polyfill.js';
import 'external';
console.log('main');
// polyfill.js
console.log('polyfill');
在此執行順序為 polyfill.js
→ external
→ main.js
。現在當你打包程式碼時,你會得到
import 'external';
console.log('polyfill');
console.log('main');
執行順序為 external
→ polyfill.js
→ main.js
。這不是 Rollup 將 import
放在程式碼最上方所造成的問題,不論它們位於檔案中的何處,匯入總是會先執行。這個問題可以透過建立更多區塊來解決:如果 polyfill.js
出現在與 main.js
不同的區塊中,正確的執行順序將會被保留。然而,在 Rollup 中還沒有自動執行此操作的方法。對於程式碼分割,情況類似,因為 Rollup 嘗試建立儘可能少的區塊,同時確保不會執行不需要的程式碼。
對於大多數程式碼,這不是問題,因為 Rollup 可以保證
如果模組 A 匯入模組 B,且沒有循環匯入,則 B 將會永遠在 A 之前執行。
然而,這對於 polyfill 來說是個問題,因為它們通常需要先執行,但通常不希望在每個模組中都放置 polyfill 的匯入。幸運的是,這並不需要
- 如果沒有依賴於 polyfill 的外部依賴項,則將 polyfill 的匯入新增為每個靜態進入點的第一個陳述就足夠了。
- 否則,另外將 polyfill 設為一個獨立的進入點或 手動區塊 將會永遠確保它先執行。
Rollup 是用於建置函式庫還是應用程式?
Rollup 已被許多主要的 JavaScript 函式庫使用,也可以用來建構絕大多數的應用程式。不過,如果你想在較舊的瀏覽器中使用程式碼分割或動態匯入,你需要額外的執行時間來處理載入遺失的區塊。我們建議使用 SystemJS 生產建置,因為它與 Rollup 的系統格式輸出整合得很好,並且能夠適當地處理所有 ES 模組即時繫結和重新匯出的邊緣案例。或者,也可以使用 AMD 載入器。
如何在瀏覽器中執行 Rollup 本身
雖然一般的 Rollup 建置依賴於一些 NodeJS 功能,但也有瀏覽器建置可以使用,它只使用瀏覽器 API。你可以透過以下方式安裝
npm install @rollup/browser
在你的指令碼中,透過以下方式匯入
import { rollup } from '@rollup/browser';
或者,你可以從 CDN 匯入,例如 ESM 建置
import * as rollup from 'https://unpkg.com/@rollup/browser/dist/es/rollup.browser.js';
以及 UMD 建置
<script src="https://unpkg.com/@rollup/browser/dist/rollup.browser.js"></script>
這將建立一個全域變數 window.rollup
。由於瀏覽器建置無法存取檔案系統,你需要提供外掛程式來解析和載入所有你想要組合的模組。以下是一個這樣做的虛構範例
const modules = {
'main.js': "import foo from 'foo.js'; console.log(foo);",
'foo.js': 'export default 42;'
};
rollup
.rollup({
input: 'main.js',
plugins: [
{
name: 'loader',
resolveId(source) {
if (modules.hasOwnProperty(source)) {
return source;
}
},
load(id) {
if (modules.hasOwnProperty(id)) {
return modules[id];
}
}
}
]
})
.then(bundle => bundle.generate({ format: 'es' }))
.then(({ output }) => console.log(output[0].code));
這個範例只支援兩個匯入,"main.js"
和 "foo.js"
,沒有相對匯入。以下是另一個使用絕對 URL 作為進入點並支援相對匯入的範例。在這種情況下,我們只是重新組合 Rollup 本身,但它可以用於任何公開 ES 模組的其他 URL
rollup
.rollup({
input: 'https://unpkg.com/rollup/dist/es/rollup.js',
plugins: [
{
name: 'url-resolver',
resolveId(source, importer) {
if (source[0] !== '.') {
try {
new URL(source);
// If it is a valid URL, return it
return source;
} catch {
// Otherwise make it external
return { id: source, external: true };
}
}
return new URL(source, importer).href;
},
async load(id) {
const response = await fetch(id);
return response.text();
}
}
]
})
.then(bundle => bundle.generate({ format: 'es' }))
.then(({ output }) => console.log(output));