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

Tim van der Lippe
Tim van der Lippe

如您所知,Chrome 開發人員工具是使用 HTML、CSS 和 JavaScript 編寫的網頁應用程式。 多年來,開發人員工具在更廣泛的網路平台方面變得更加豐富、聰明,也更加瞭解。 雖然開發人員工具多年來不斷擴張,但當時仍在 WebKit 的一部分,其架構大部分與原始架構類似。

本文是一系列網誌文章之一,旨在說明開發人員工具架構的異動及建構方式。我們會說明開發人員工具的過往運作方式、優點和限制,以及我們採取了哪些措施來減輕這些限制。 因此,讓我們深入瞭解模組系統、如何載入程式碼,以及最終如何使用 JavaScript 模組。

一開始

雖然目前的前端環境具有各種內建工具的模組系統,以及現在標準化的 JavaScript 模組格式,但上述功能在首次建構 DevTools 時都不適用。開發人員工具是以程式碼為基礎,這些程式碼最初在 WebKit 中推出至今已超過 12 年。

開發人員工具中首次提到模組系統,起源於 2012 年:推出包含相關來源清單的模組清單。這是用於編譯及建構開發人員工具的 Python 基礎架構的一部分。後續變更作業會從 2013 年將所有模組擷取到個別 frontend_modules.json 檔案 (修訂版本),然後在 2014 年轉換為個別 module.json 檔案 (「修訂」)。

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 的排列順序相當重要。Google 無法保證您仰賴的程式碼會載入,除非已經經過真人驗證。

總而言之,在在開發人員工具和其他 (更廣泛使用) 模組格式中評估模組系統的目前狀態時,我們推論 module.json 模式產生的問題比解決的問題多,且是時候應考慮走走遠離這個模式了。

標準優點

我們從現有的模組系統中,選擇做為要遷移的 JavaScript 模組。當時,決策 JavaScript 模組目前仍在 Node.js 中透過旗標傳送,而 NPM 上提供的大量套件則沒有我們可以使用的 JavaScript 模組組合。儘管如此,我們做出的結論是 JavaScript 模組是最佳選擇。

JavaScript 模組的主要優點在於 JavaScript 的標準化模組格式。列出 module.json 的缺點 (如上所示) 時,我們發現幾乎所有這些問題都與非標準化的專屬模組格式有關。

選擇非標準化的模組格式時,我們必須投入時間自行設計與維護人員使用的建構工具和工具整合。

這些整合工作通常很脆弱,而且不支援功能,需要額外的維護時間。有時可能會產生細微的錯誤,而且最終會向使用者推送。

由於 JavaScript 模組是標準功能,因此讓 VS Code 等 IDE 可以解讀 Closure Compiler/TypeScript 和建構工具 (例如 Rollup/minifiers) 等建構工具。此外,當有一位新的維護人員加入開發人員工具團隊時,他們就不必花時間學習專屬的 module.json 格式,反而會 (或許) 已經熟悉 JavaScript 模組。

當然,開發者最初開發時並沒有上述好處。他們花費多年時間,針對標準群組、執行階段實作,以及使用 JavaScript 模組的開發人員提供相關意見,協助他們達成目前目標。 但是當 JavaScript 模組推出時,我們有選擇選擇:繼續自行保留格式,或投資遷移至新模組。

嶄新的代價

即使 JavaScript 模組有許多我們想使用的益處,我們還是會繼續處於非標準的 module.json 環境中。取消 JavaScript 模組的優點後,我們必須投入大量資源來清理技術債,執行可能破壞功能並產生迴歸錯誤的遷移作業。

現階段,這不是「要使用 JavaScript 模組嗎?」的問題,而是想瞭解「使用 JavaScript 模組的費用有多高?」的問題。然而,我們必須在破壞使用者的風險與迴歸、工程師花費大量時間 (大量) 遷移,以及我們面臨的暫時性惡劣狀態之間取得平衡。

我們最後發現這一點非常重要。即使理論上我們可以找到 JavaScript 模組,但在遷移作業期間,最後我們會將程式碼「同時」納入考量 module.json 和 JavaScript 模組。 這不僅在技術上困難重重,也代表所有使用開發人員工具的工程師,也都有必要瞭解如何在這個環境中工作。 他們必須不斷自問:「針對這部分的程式碼集,它是 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 為檔案名稱。在 sourcetree 中,會是 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 陳述式約一週多次嘗試重新進入

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. 有時候,moduleScoped 的名稱之間會有衝突。最值得注意的是,我們使用某種模式來宣告特定類型的 Events,其中每個符號的名稱都只有 Events。也就是說,如果您監聽不同檔案中宣告的多種類型事件,這些 Eventsimport 陳述式會出現名稱限制。
  3. 如前所述,檔案之間存在循環依附元件。這對全域範圍而言沒有問題,因為符號是在所有程式碼載入後才使用。然而,如果您需要 import,循環依附元件則是明確設定。除非您的全域範圍程式碼中有連帶影響的函式呼叫,否則這不會立即造成問題。總而言之,為確保轉換作業安全無虞,這一切都得仰賴手術與重構。

運用 JavaScript 模組的全新世界

2020 年 2 月,在 2019 年 9 月開始的 6 個月後,系統會在 ui/ 資料夾中執行上次清理作業。 這就是遷移作業的非官方程序。等待灰塵後,我們正式將遷移作業標示為 2020 年 3 月 5 日完成。🎉

現在,開發人員工具中的所有模組都會使用 JavaScript 模組來共用程式碼。針對舊版測試或與開發人員工具架構的其他部分整合,我們仍會在全域範圍 (位於 module-legacy.js 檔案中) 放置一些符號。我們會逐漸移除這些問題,但我們認為這些限制在未來開發時不會造成阻礙。我們也提供 JavaScript 模組的使用樣式指南

統計資料

針對本次遷移作業所涉及的 CL 數量 (變更清單縮寫,與 GitHub 提取要求類似) 的保守估計值大約是 250 個 CL,由 2 位工程師執行。 我們沒有確切的執行變更大小統計資料,但是經過保守的行預估數據 (計算方式為每個 CL 插入與刪除作業之間的絕對差異總和) 約為 30,000 (佔所有 DevTools 前端程式碼的 20%)

使用 export 的第一個檔案在 Chrome 79 中發布,並於 2019 年 12 月發布並保持穩定。 Chrome 第 83 版新增了遷移至 import 的最終變更,並於 2020 年 5 月發布穩定版本。

我們知道有一項迴歸問題會送至 Chrome 穩定版,並且在這項遷移作業中導入。 由於發生大量的 default 匯出項目,指令選單中的自動完成建議功能會毀損。我們還有好幾種迴歸問題,但我們的自動化測試套件和 Chrome Canary 使用者都回報了這些問題,並在他們觸及 Chrome 穩定使用者前修正。

您可以在 crbug.com/1006759 中查看完整歷程 (並非所有 CL 附加到這項錯誤,但大部分 CL 會保留在記錄中)。

我們的經驗教訓

  1. 過去所做的決策可能會對您的專案造成深遠的影響。雖然 JavaScript 模組 (和其他模組格式) 已有一段時間使用,但開發人員工具無法合理說明遷移作業。憑藉著合理的猜測結果,您很難判斷何時該遷移,以及何時不應遷移。
  2. 最初的預估時間是數週,而非數月。 這主要是出於我們發現比初步費用分析中預期的更多意外問題。 雖然遷移計畫相當堅實,但技術債卻是阻礙者的因素。
  3. JavaScript 模組遷移作業包含大量 (似乎不相關) 技術債清除。我們遷移至現代化的標準化模組格式,讓我們能重新調整程式設計最佳做法,與現代化的網頁開發技術保持同步。例如,我們得以用最少的 Rollup 設定來取代自訂 Python 軟體包。
  4. 儘管對程式碼集的影響很大 (變更約 20% 的程式碼),但只會回報極少數的迴歸問題。 雖然在遷移前幾個檔案時確實遭遇不少問題,但後來我們有部分自動化的工作流程,而且只能自動處理部分工作。 這意味著,本次遷移作業對穩定使用者的影響微乎其微。
  5. 教導將特定資料遷移到其他維護人員的複雜作業並不容易,有時是不可能的任務。這種規模的遷移作業並不容易,也需要大量的網域知識。我們不希望將網域知識轉移給使用同一程式碼集的其他工作,也無意為對方正設法完成工作。瞭解哪些事情要分享、哪些要分享的細節是一件美術工作,但這是不可或缺的一環。 因此,減少大量遷移作業十分重要,或者至少不會同時執行遷移作業。

下載預覽管道

考慮使用 Chrome Canary 版開發人員版Beta 版做為預設開發瀏覽器。這些預覽管道可讓您存取開發人員工具的最新功能、測試最先進的網路平台 API,並在使用者使用之前就在網站上發現問題!

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

使用下列選項,討論文章的新功能和異動,以及其他與開發人員工具相關的事項。

  • 請透過 crbug.com 提交建議或意見回饋。
  • 如要回報開發人員工具的問題,請在開發人員工具中依序點選「更多選項」圖示 更多   >「說明」 >「回報開發人員工具的問題」
  • @ChromeDevTools 張貼推文。
  • 歡迎對開發人員工具的 YouTube 影片或開發人員工具秘訣 (YouTube 影片) 提供意見。