如您所知,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
格式。
雖然開發人員工具仍可正常運作,但使用非標準化且獨特的模組系統有幾個缺點:
module.json
格式需要自訂建構工具,類似於新式套件組合器。- 沒有 IDE 整合,因此需要自訂工具來產生現代 IDE 可理解的檔案 (原始指令碼,可為 VS Code 產生 jsconfig.json 檔案)。
- 函式、類別和物件都放在全域範圍,以便在模組之間共用。
- 檔案會依序排列,也就是說,
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 模組。因此,我們的主要目標如下:
- 確保 JavaScript 模組的使用能盡可能發揮效益。
- 請確認與現有
module.json
系統的整合方式安全無虞,不會對使用者造成負面影響 (回歸錯誤、使用者不滿意)。 - 引導所有開發人員工具維護人員完成遷移作業,並透過內建的檢查和平衡機制,避免發生意外錯誤。
試算表、轉換作業和技術債
雖然目標很明確,但 module.json
格式帶來的限制,讓我們很難找到解決方法。我們經過多次迭代、製作原型和架構變更,才開發出滿意的解決方案。我們根據最終的遷移策略撰寫設計文件。設計文件也列出了我們最初的時間估計值:2 到 4 週。
劇透警告:遷移作業最密集的部分花了 4 個月,從頭到尾則花了 7 個月!
不過,最初的計畫經得起時間考驗:我們會教導 DevTools 執行階段使用舊方法載入 module.json
檔案中 scripts
陣列中列出的所有檔案,同時使用 JavaScript 模組動態匯入功能,將 modules
陣列中列出的所有檔案載入。任何位於 modules
陣列中的檔案都能使用 ES 匯入/匯出功能。
此外,我們會在 2 個階段 (最後階段會分為 2 個子階段,請參閱下文) 執行遷移作業:export
和 import
階段。在大型試算表中追蹤哪個模組會處於哪個階段:
進度表的程式碼片段已公開,請前往這裡查看。
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();
不過,這種做法有一些限制:
- 並非所有符號都命名為
Module.File.symbolName
。部分符號只命名為Module.File
,甚至是Module.CompletelyDifferentName
。由於這兩者不一致,因此我們必須建立從舊全域物件到新匯入物件的內部對應關係。 - 有時模組層級名稱之間會發生衝突。最明顯的例子是,我們使用了宣告特定類型
Events
的模式,其中每個符號都只命名為Events
。這表示如果您在不同檔案中宣告多種事件類型,這些Events
的import
陳述式就會發生名稱衝突。 - 結果發現檔案之間存在循環相依性。在全域範圍的情況下,這並無礙於使用符號,因為符號的使用是在所有程式碼載入後才會發生。不過,如果您需要
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 都附加至此錯誤,但大部分都附加了)。
我們的經驗教訓
- 過去的決策可能會對專案造成長期影響。雖然 JavaScript 模組 (和其他模組格式) 已推出一段時間,但 DevTools 無法證明遷移的必要性。決定何時遷移或不遷移很困難,而且必須根據推測做出判斷。
- 我們最初的預估時間是以週為單位,而非以月為單位。這主要是因為我們在初始成本分析中發現的意外問題比預期多。雖然遷移計畫相當完善,但技術債 (經常) 會造成阻礙。
- JavaScript 模組遷移作業包括大量 (看似不相關的) 技術債清理作業。遷移至現代化標準化模組格式後,我們得以將程式碼最佳做法與現代網頁開發作業保持一致。舉例來說,我們可以用最少的 Rollup 設定取代自訂 Python 束縛程式。
- 儘管對程式碼庫造成的影響很大 (約 20% 的程式碼變更),但我們只回報了少數的回歸。雖然我們在遷移前幾個檔案時遇到許多問題,但過了一陣子,我們終於建立了可靠的部分自動化工作流程。這表示在本次遷移作業中,穩定使用者受到的負面影響降到最低。
- 向其他維護人員說明特定遷移作業的複雜性,有時是相當困難,甚至是不可能的。這種規模的遷移作業難以追蹤,且需要大量領域知識。將該領域的知識轉移給在同一個程式碼庫中工作的其他人,就他們的工作而言,本身並非理想做法。知道該分享哪些內容,以及哪些詳細資料不應分享,是一門藝術,也是必要的藝術。因此,請務必減少大型遷移作業的數量,或至少不要同時執行這些作業。
下載預覽管道
建議您將 Chrome Canary、開發人員版或Beta 版設為預設開發人員版瀏覽器。這些預覽管道可讓您存取最新的 DevTools 功能,測試最新的網路平台 API,並在使用者發現問題前,協助您找出網站的問題!
與 Chrome 開發人員工具團隊聯絡
請使用下列選項討論新功能、更新或任何與開發人員工具相關的內容。
- 請前往 crbug.com 提交意見回饋和功能要求。
- 在開發人員工具中,依序按一下「more_vert」 更多選項 >「Help」 >「Report a DevTools issue」,即可回報開發人員工具的問題。
- 在 Twitter 上傳送訊息給 @ChromeDevTools。
- 在 YouTube 影片「What's new in DevTools」或「DevTools 提示」YouTube 影片中留言。