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

Tim van der Lippe
Tim van der Lippe

如您所知,Chrome 開發人員工具是使用 HTML、CSS 和 JavaScript 編寫的網頁應用程式。這些年來,開發人員工具的功能變得更強大、更聰明,也更加瞭解這個廣大的網路平台。 雖然 DevTools 這些年來不斷擴充,但其架構仍與 WebKit 時期的原始架構相似。

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

起初,什麼都沒有

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

在 DevTools 中首次提及模組系統是在 2012 年:引入模組清單,並與相關的來源清單建立關聯。這是 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 等型別檢查工具,以及綜覽/薄荷器等建構工具,都能瞭解我們編寫的原始碼。此外,當新的維護人員加入 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 日完成」。🎉

現在,開發人員工具中的所有模組都會使用 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 模組 (和其他模組格式) 已可使用一段時間,但開發人員工具無法證明遷移作業的必要性。 決定何時遷移或不遷移,需要根據明智的推測做出判斷,這項決定相當困難。
  2. 我們最初的預估時間是以週為單位,而非以月為單位。這主要原因在於,我們發現在初期費用分析中,發現的意外問題數量超出預期。雖然遷移計畫相當完善,但技術債 (經常) 會造成阻礙。
  3. JavaScript 模組遷移作業包含大量 (看似不相關的) 技術債清理作業。遷移至現代化的標準化模組格式之後,我們得以重新調整程式設計最佳做法,並打造現代化的網頁開發。舉例來說,我們可以用最少的 Rollup 設定取代自訂 Python 套件組合器。
  4. 雖然對程式碼集的影響較大 (程式碼變更約 20%),但回報結果很少。雖然我們在遷移前幾個檔案時遇到許多問題,但過了一陣子,我們終於建立了可靠的部分自動化工作流程。這表示在本次遷移作業中,穩定使用者受到的負面影響降到最低。
  5. 向其他維護人員說明特定遷移作業的複雜性,有時是相當困難,甚至是不可能的。這類規模的遷移作業難以追蹤,且需要大量領域知識。不需要將該項領域知識轉移給在相同程式碼集中工作的其他人處理工作。知道該分享哪些內容,以及哪些詳細資料不該分享,是一門藝術,也是必要的藝術。因此,您需要減少大量遷移作業,或至少不要同時執行遷移作業。

下載預覽頻道

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

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

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