開發人員工具架構更新:遷移至 JavaScript 模組

Tim van der Lippe
Tim van der Lippe

如您所知,Chrome 開發人員工具是使用 HTML、CSS 和 JavaScript 編寫的網頁應用程式。多年下來,DevTools 的功能越來越豐富,也越來越聰明,對更廣泛的網路平台瞭若指掌。雖然 DevTools 這幾年來不斷擴充,但其架構仍與 WebKit 時期的原本架構相似。

這篇文章是一系列網誌文章的一部分,說明我們對 DevTools 架構所做的變更,以及如何建構這個架構。我們將說明 DevTools 過往的運作方式、優點和限制,以及我們為緩解這些限制所採取的措施。因此,讓我們深入探討模組系統、如何載入程式碼,以及我們如何使用 JavaScript 模組。

起初,什麼都沒有

雖然目前的前端環境有各種模組系統,以及圍繞這些系統建構的工具,以及現已標準化的 JavaScript 模組格式,但這些都不是開發人員工具首次建構時就存在的。開發人員工具是建構在 12 多年前 WebKit 中最初發布的程式碼之上。

在 DevTools 中首次提及模組系統是在 2012 年:引入模組清單,並與相關的來源清單建立關聯。這是當時用來編譯及建構 DevTools 的 Python 基礎架構的一部分。後續變更在 2013 年將所有模組擷取到個別的 frontend_modules.json 檔案 (commit),然後在 2014 年將這些模組擷取到個別的 module.json 檔案 (commit)。

module.json 檔案範例:

{
  "dependencies": [
    "common"
  ],
  "scripts": [
    "StylePane.js",
    "ElementsPanel.js"
  ]
}

自 2014 年起,開發人員工具就已使用 module.json 模式指定模組和來源檔案。與此同時,網頁生態系統迅速發展,並建立了多種模組格式,包括 UMD、CommonJS 和最終標準化的 JavaScript 模組。不過,開發人員工具仍會使用 module.json 格式。

雖然開發人員工具仍可正常運作,但使用非標準化且獨特的模組系統有幾個缺點:

  1. module.json 格式需要自訂建構工具,類似於新式套件組合器。
  2. 沒有 IDE 整合,因此需要自訂工具來產生現代 IDE 可理解的檔案 (原始指令碼,可為 VS Code 產生 jsconfig.json 檔案)。
  3. 函式、類別和物件都放在全域範圍,以便在模組之間共用。
  4. 檔案會依序排列,也就是說,sources 的列出順序十分重要。除非有人驗證,否則我們無法保證您所依賴的程式碼會載入。

總而言之,在評估 DevTools 中的模組系統和其他 (較常用的) 模組格式的現況後,我們得出結論:module.json 模式帶來的問題比解決的問題還多,因此是時候規劃如何移除這個模式。

標準的優點

我們從現有的模組系統中,選擇 JavaScript 模組作為遷移目標。在做出這項決定時,JavaScript 模組仍是透過 Node.js 中的標記發布,而且 NPM 提供的大量套件並未提供可供使用的 JavaScript 模組套件。儘管如此,我們仍認為 JavaScript 模組是最佳選擇。

JavaScript 模組的主要優點是,它是 JavaScript 的標準化模組格式。當我們列出 module.json 的缺點 (如上所述) 時,我們發現這些缺點幾乎都與使用非標準化且獨特的模組格式有關。

選擇非標準的模組格式,代表我們必須花時間自行建構整合,並與維護人員使用的建構工具和工具整合。

這些整合功能通常不穩定,且缺乏對功能的支援,因此需要額外的維護時間,有時還會導致難以察覺的錯誤,最終會傳送給使用者。

由於 JavaScript 模組是標準,因此 VS Code 等 IDE、Closure Compiler/TypeScript 等型別檢查器,以及 Rollup/minifier 等建構工具,都能夠瞭解我們編寫的原始碼。此外,當新的維護人員加入 DevTools 團隊時,他們不必花時間學習專屬的 module.json 格式,因為他們可能已經熟悉 JavaScript 模組。

當然,在開發人員工具最初建構時,上述優點都還不存在。標準群組、執行階段實作項目和使用 JavaScript 模組的開發人員提供意見回饋,我們才得以歷經多年努力,才有今天的成果。但 JavaScript 模組推出後,我們必須做出選擇:要繼續維護自己的格式,還是要投資遷移至新格式。

全新產品的費用

雖然 JavaScript 模組有許多優點,我們仍決定採用非標準的 module.json 版本。要充分發揮 JavaScript 模組的優點,就必須大幅投資清理技術債務,執行可能會破壞功能並引入回歸錯誤的遷移作業。

此時的問題不是「我們要使用 JavaScript 模組嗎?」,而是「使用 JavaScript 模組的成本有多高?」。在這個情況下,我們必須權衡風險,包括因回歸而導致使用者無法使用服務、工程師花費大量時間遷移的成本,以及我們將要面對的暫時性惡化情況。

最後一點非常重要。雖然理論上可以使用 JavaScript 模組,但在遷移期間,程式碼必須考量 module.json 和 JavaScript 模組。這不僅在技術上難以實現,也意味著所有在 DevTools 上工作的工程師都必須瞭解如何在這個環境中作業。他們必須不斷問自己:「這個程式碼庫的部分是 module.json 還是 JavaScript 模組,我該如何進行變更?」

搶先看:協助維護人員遷移的隱藏成本比我們預期的還要高。

經過成本分析後,我們認為仍值得遷移至 JavaScript 模組。因此,我們的主要目標如下:

  1. 確保 JavaScript 模組的使用能盡可能發揮效益。
  2. 請確認與現有 module.json 系統的整合方式安全無虞,不會對使用者造成負面影響 (回歸錯誤、使用者不滿意)。
  3. 引導所有開發人員工具維護人員完成遷移作業,並透過內建的檢查和平衡機制,避免發生意外錯誤。

試算表、轉換作業和技術債

雖然目標很明確,但 module.json 格式帶來的限制,讓我們很難找到解決方法。我們經過多次迭代、製作原型和架構變更,才開發出滿意的解決方案。我們根據最終的遷移策略撰寫設計文件。設計文件也列出了我們最初的時間估計值:2 到 4 週。

劇透警告:遷移作業最密集的部分花了 4 個月,從頭到尾則花了 7 個月!

不過,最初的計畫經得起時間考驗:我們會教導 DevTools 執行階段使用舊方法載入 module.json 檔案中 scripts 陣列中列出的所有檔案,同時使用 JavaScript 模組動態匯入功能,將 modules 陣列中列出的所有檔案載入。任何位於 modules 陣列中的檔案都能使用 ES 匯入/匯出功能。

此外,我們會在 2 個階段 (最後階段會分為 2 個子階段,請參閱下文) 執行遷移作業:exportimport 階段。在大型試算表中追蹤哪個模組會處於哪個階段:

JavaScript 模組遷移試算表

進度表的程式碼片段已公開,請前往這裡查看。

export 階段

第一階段是針對所有應在模組/檔案之間共用的符號新增 export 陳述式。轉換作業會自動執行,每個資料夾都會執行指令碼。假設 module.json 世界中存在下列符號:

Module.File1.exported = function() {
  console.log('exported');
  Module.File1.localFunctionInFile();
};
Module.File1.localFunctionInFile = function() {
  console.log('Local');
};

(此處的 Module 是模組名稱,File1 是檔案名稱。在我們的來源樹狀結構中,則為 front_end/module/file1.js)。

這會轉換為以下內容:

export function exported() {
  console.log('exported');
  Module.File1.localFunctionInFile();
}
export function localFunctionInFile() {
  console.log('Local');
}

/** Legacy export object */
Module.File1 = {
  exported,
  localFunctionInFile,
};

我們最初的計畫是在這階段重寫相同檔案的匯入作業。舉例來說,在上述範例中,我們會將 Module.File1.localFunctionInFile 重寫為 localFunctionInFile。不過,我們發現如果將這兩種轉換分開,就能更輕鬆地自動化並安全地套用。因此,「遷移同一個檔案中的所有符號」會成為 import 階段的第二個子階段。

由於在檔案中加入 export 關鍵字會將檔案從「指令碼」轉換為「模組」,因此必須相應更新許多開發人員工具基礎架構。這包括執行階段 (含動態匯入),以及 ESLint 等工具,可在模組模式下執行。

在解決這些問題的過程中,我們發現測試是在「鬆散」模式下執行。由於 JavaScript 模組會讓檔案以 "use strict" 模式執行,因此這也會影響我們的測試。結果發現,有不少測試都依賴這種鬆散的做法,包括使用 with 陳述式的測試 😱?。

最後,更新第一個資料夾以納入 export 陳述式花了約一週的時間,並多次嘗試使用 relands

import 階段

在使用 export 陳述式匯出所有符號並保留在全域範圍 (舊版) 後,我們必須更新所有跨檔案符號的參照,才能使用 ES 匯入功能。最終目標是移除所有「舊版匯出物件」,清理全域範圍。轉換作業會自動執行,每個資料夾執行一個指令碼

舉例來說,以下是 module.json 世界中的符號:

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
SameModule.AnotherFile.moduleScoped();

會轉換為:

import * as Module from '../module/Module.js';
import * as AnotherModule from '../another_module/AnotherModule.js';

import {moduleScoped} from './AnotherFile.js';

Module.File1.exported();
AnotherModule.AnotherFile.alsoExported();
moduleScoped();

不過,這種做法有一些限制:

  1. 並非所有符號都命名為 Module.File.symbolName。部分符號只命名為 Module.File,甚至是 Module.CompletelyDifferentName。由於這兩者不一致,因此我們必須建立從舊全域物件到新匯入物件的內部對應關係。
  2. 有時模組層級名稱之間會發生衝突。最明顯的例子是,我們使用了宣告特定類型 Events 的模式,其中每個符號都只命名為 Events。這表示如果您在不同檔案中宣告多種事件類型,這些 Eventsimport 陳述式就會發生名稱衝突。
  3. 結果發現檔案之間存在循環相依性。在全域範圍的情況下,這並無礙於使用符號,因為符號的使用是在所有程式碼載入後才會發生。不過,如果您需要 import,循環相依性會變得明確。除非全域範圍程式碼中含有副作用函式呼叫,否則這並非立即的問題,而 DevTools 也曾出現這種情況。總而言之,您需要進行一些手術和重構,才能確保轉換作業安全無虞。

全新的 JavaScript 模組世界

在 2019 年 9 月開始後的 6 個月,也就是 2020 年 2 月,我們在 ui/ 資料夾中執行最後一次清理作業。這標誌著遷移作業非正式結束。在一切塵埃落定後,我們正式將遷移作業標示為「2020 年 3 月 5 日完成」。🎉

如今,DevTools 中的所有模組都會使用 JavaScript 模組來共用程式碼。我們仍會在全域範圍 (module-legacy.js 檔案中) 放置部分符號,以便進行舊版測試或整合 DevTools 架構的其他部分。這些功能會隨著時間移除,但我們不認為這會阻礙日後的開發作業。我們也提供JavaScript 模組使用方式的樣式指南

統計資料

根據保守估計,這項遷移作業涉及的 CL 數量 (CL 是變更清單的縮寫,在 Gerrit 中用來代表變更,類似於 GitHub 提取要求) 約為 250 個,主要由 2 位工程師執行。我們沒有確切的統計資料,無法得知變更的規模,但根據保守估計,變更的資料列數 (計算方式為各 CL 的插入和刪除值之間的絕對差異總和) 大約為 30,000 (約佔 DevTools 前端程式碼的 20%)

第一個使用 export 的檔案是在 Chrome 79 版中提供,並於 2019 年 12 月發布穩定版。最後一次變更遷移至 import 是在 Chrome 83 版,並在 2020 年 5 月發布至穩定版。

我們發現在遷移過程中,Chrome 穩定版中出現了一個回歸問題。由於額外的 default 匯出,指令選單中的程式碼片段自動完成功能中斷。我們也發現其他幾個迴歸問題,但自動化測試套件和 Chrome Canary 使用者已回報這些問題,我們在 Chrome 穩定版使用者遇到這些問題前就已修正。

您可以在 crbug.com/1006759 中查看完整記錄 (並非所有 CL 都附加至此錯誤,但大部分都附加了)。

我們的經驗教訓

  1. 過去的決策可能會對專案造成長期影響。雖然 JavaScript 模組 (和其他模組格式) 已推出一段時間,但 DevTools 無法證明遷移的必要性。決定何時遷移或不遷移很困難,而且必須根據推測做出判斷。
  2. 我們最初的預估時間是以週為單位,而非以月為單位。這主要是因為我們在初始成本分析中發現的意外問題比預期多。雖然遷移計畫相當完善,但技術債 (經常) 會造成阻礙。
  3. JavaScript 模組遷移作業包括大量 (看似不相關的) 技術債清理作業。遷移至現代化標準化模組格式後,我們得以將程式碼最佳做法與現代網頁開發作業保持一致。舉例來說,我們可以用最少的 Rollup 設定取代自訂 Python 束縛程式。
  4. 儘管對程式碼庫造成的影響很大 (約 20% 的程式碼變更),但我們只回報了少數的回歸。雖然我們在遷移前幾個檔案時遇到許多問題,但過了一陣子,我們終於建立了可靠的部分自動化工作流程。這表示在本次遷移作業中,穩定使用者受到的負面影響降到最低。
  5. 向其他維護人員說明特定遷移作業的複雜性,有時是相當困難,甚至是不可能的。這種規模的遷移作業難以追蹤,且需要大量領域知識。將該領域的知識轉移給在同一個程式碼庫中工作的其他人,就他們的工作而言,本身並非理想做法。知道該分享哪些內容,以及哪些詳細資料不應分享,是一門藝術,也是必要的藝術。因此,請務必減少大型遷移作業的數量,或至少不要同時執行這些作業。

下載預覽管道

建議您將 Chrome Canary開發人員版Beta 版設為預設開發人員版瀏覽器。這些預覽管道可讓您存取最新的 DevTools 功能,測試最新的網路平台 API,並在使用者發現問題前,協助您找出網站的問題!

與 Chrome 開發人員工具團隊聯絡

請使用下列選項討論新功能、更新或任何與開發人員工具相關的內容。