DevTools アーキテクチャの更新: JavaScript モジュールへの移行

Tim van der Lippe
Tim van der Lippe

ご存じのとおり、Chrome DevTools は、HTML、CSS、JavaScript を使用して記述されたウェブ アプリケーションです。長年にわたり、DevTools はより多くの機能を備え、よりスマートになり、幅広いウェブ プラットフォームに関する知識を蓄積してきました。DevTools は年々拡張されていますが、そのアーキテクチャは、まだ WebKit の一部だったときの元のアーキテクチャとほぼ同じです。

この投稿は、DevTools のアーキテクチャとその構築方法に対する変更について説明する一連のブログ投稿の一部です。ここでは、DevTools のこれまでの機能、メリットと制限、制限を緩和するために Google が行った取り組みについて説明します。そのため、モジュールシステム、コードの読み込み方法、JavaScript モジュールの使用方法について詳しく見ていきましょう。

はじめに、何もなかった

現在のフロントエンドには、さまざまなモジュール システムとそれらを基盤とするツール、標準化された JavaScript モジュール形式が存在しますが、DevTools が最初に構築された時点では、これらのどれも存在しませんでした。DevTools は、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 年以降、DevTools では module.json パターンを使用してモジュールとソースファイルを指定しています。一方、ウェブ エコシステムは急速に進化し、UMD、CommonJS、最終的に標準化された JavaScript モジュールなど、複数のモジュール形式が作成されました。ただし、DevTools は module.json 形式のままでした。

DevTools は引き続き機能していましたが、標準化されていない独自のモジュール システムを使用すると、いくつかの欠点がありました。

  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 の欠点を挙げてみると(上記を参照)、そのほとんどが標準化されていない独自のモジュール形式の使用に関連していることがわかりました。

標準化されていないモジュール形式を選択すると、Google はメンテナンス担当者が使用しているビルドツールやツールとの統合の構築に時間を費やす必要があります。

これらの統合は脆弱で、機能のサポートが不足しているため、メンテナンスに追加の時間がかかり、最終的にユーザーに配布される微妙なバグにつながることもあります。

JavaScript モジュールが標準であるため、VS Code などの IDE、Closure Compiler/TypeScript などの型チェッカー、Rollup/圧縮ツールなどのビルドツールは、作成したソースコードを理解できます。さらに、新しいメンテナンス担当者が DevTools チームに加わった場合、独自の module.json 形式を学習する必要はありません。JavaScript モジュールにはすでに精通しているはずです。

もちろん、DevTools が最初に構築された時点では、上記のメリットはありませんでした。現在の状態に至るまでには、標準グループ、ランタイム実装、JavaScript モジュールを使用するデベロッパーによるフィードバック提供と、長年にわたる作業が必要でした。しかし、JavaScript モジュールが利用可能になったとき、独自の形式を維持するか、新しい形式への移行に投資するかの選択を迫られました。

新品の費用

JavaScript モジュールには、利用したいメリットが多数ありましたが、非標準の module.json の世界にとどまっていました。JavaScript モジュールのメリットを享受するには、技術的負債のクリーンアップに多大な投資を行い、機能を破壊したり、回帰バグを導入したりする可能性のある移行を行う必要がありました。

この時点では、「JavaScript モジュールを使用するか」ではなく、「JavaScript モジュールを使用できると費用はどのくらいかかるか」という問題でした。ここでは、リグレッションによってユーザーに問題が発生するリスク、エンジニアが移行に費やす(大量の)時間のコスト、一時的に悪化する状態とのバランスを取る必要がありました。

この最後のポイントが非常に重要であることがわかりました。理論的には JavaScript モジュールに移行できますが、移行中に module.json モジュールと JavaScript モジュールの両方を考慮する必要があるコードが作成されます。これは技術的に実現するのが困難なだけでなく、DevTools で働くすべてのエンジニアがこの環境で作業する方法を理解する必要がありました。コードベースのこの部分は module.json モジュールか JavaScript モジュールか、変更するにはどうすればよいか、といったことを常に自問自答する必要があります。

プレビュー: 他のメンテナンス担当者を移行に誘導する際の隠れた費用は、予想よりも大きかった。

費用分析の結果、JavaScript モジュールに移行する価値があると判断しました。そのため、主な目標は次のとおりです。

  1. JavaScript モジュールの使用によって、可能な限りメリットを享受できるようにします。
  2. 既存の module.json ベースのシステムとの統合が安全であり、ユーザーに悪影響を及ぼさないこと(回帰バグ、ユーザーの不満)を確認します。
  3. すべての DevTools メンテナンス担当者に移行を案内します。主に、誤ってミスを防ぐためのチェック アンド バランスが組み込まれています。

スプレッドシート、変換、技術的負債

目標は明確でしたが、module.json 形式によって課せられる制限を回避するのは困難でした。満足のいくソリューションを開発するまでに、何度かの反復処理、プロトタイプ、アーキテクチャの変更が必要でした。最終的な移行戦略を記載した設計ドキュメントを作成しました。設計ドキュメントには、最初の所要時間として 2 ~ 4 週間と記載されています。

ネタバレ注意: 移行の最も負荷の高い部分には 4 か月、最初から最後までには 7 か月かかりました。

しかし、当初の計画は長い間検証され、module.json ファイルの scripts 配列にリストされているすべてのファイルを古い方法で読み込み、modules 配列にリストされているすべてのファイルを JavaScript モジュールの動的インポートで読み込むように DevTools ランタイムに指示することになりました。modules 配列に存在するファイルはすべて、ES のインポートとエクスポートを使用できます。

また、移行は 2 つのフェーズ(最終的に最後のフェーズを 2 つのサブフェーズに分割しました。下記を参照)で実施します。export フェーズと import フェーズです。どのモジュールがどのフェーズにあるかのステータスは、大きなスプレッドシートで追跡されていました。

JavaScript モジュールの移行スプレッドシート

進捗状況シートのスニペットはこちらで公開されています。

export-phase

最初のフェーズでは、モジュール/ファイル間で共有されるはずのすべてのシンボルに 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.localFunctionInFilelocalFunctionInFile に書き換えます。ただし、これらの 2 つの変換を分離すると、自動化が容易になり、適用が安全になることがわかりました。したがって、「同じファイル内のすべてのシンボルを移行する」は、import フェーズの 2 番目のサブフェーズになります。

ファイルに export キーワードを追加すると、ファイルが「スクリプト」から「モジュール」に変換されるため、DevTools インフラストラクチャの多くを適宜更新する必要がありました。これには、ランタイム(動的インポートあり)だけでなく、モジュール モードで実行する ESLint などのツールも含まれています。

これらの問題の解決中に、テストが「ずさんな」モードで実行されていることが判明しました。JavaScript モジュールは、ファイルが "use strict" モードで実行されることを前提としているため、テストにも影響します。結果として、with ステートメントを使用したテストなど、かなりの数のテストがこの不注意に依存していました。😱?

結局、最初のフォルダを更新して export ステートメントを含めるまでに、約 1 週間複数回の再ビルドが必要でした。

import-phase

すべてのシンボルが 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 モジュールを使用してコードを共有するようになりました。以前のテストや DevTools アーキテクチャの他の部分との統合のために、一部のシンボルは引き続きグローバル スコープ(module-legacy.js ファイル内)に配置します。これらの機能は今後削除される予定ですが、今後の開発の妨げになるものではありません。また、JavaScript モジュールの使用に関するスタイルガイドもあります。

統計情報

この移行に関連する CL(変更リストの略称 - Gerrit で変更を表す用語 - GitHub の pull リクエストに似ています)の数は、250 件程度で、主に 2 人のエンジニアが実施します。変更された変更の規模に関する明確な統計情報はありませんが、変更された行数(各 CL の挿入と削除の絶対差の合計として計算)は、約 30,000 行(DevTools フロントエンド コードの約 20%)と慎重に推定されています。

export を使用した最初のファイルは、2019 年 12 月に安定版としてリリースされた Chrome 79 で出荷されました。import への移行に関する最後の変更は、2020 年 5 月に安定版としてリリースされた Chrome 83 で行われました。

この移行の一環として、Chrome Stable にリリースされたリグレッションが 1 件確認されています。不要な default エクスポートが原因で、コマンド メニューのスニペットの自動補完が機能しなくなった。他にもいくつかの回帰が発生しましたが、自動テストスイートと Chrome Canary のユーザーから報告があり、Chrome Stable のユーザーに影響する前に修正しました。

crbug.com/1006759 にログに記録されている完全な経路(すべての CL がこのバグに関連付けられているわけではありませんが、ほとんどの CL が関連付けられています)を確認できます。

振り返り

  1. 過去に下した決定は、プロジェクトに長期的な影響を及ぼす可能性があります。JavaScript モジュール(および他のモジュール形式)は長い間利用可能でしたが、DevTools では移行を正当化する立場にありませんでした。移行するタイミングと移行しないタイミングを判断するのは難しい作業であり、経験に基づく推測に基づいています。
  2. 当初の所要時間の見積もりは、月ではなく週単位でした。これは主に、最初の費用分析で想定していたよりも多くの予期しない問題が見つかったことに起因しています。移行計画は堅固でしたが、技術的な負債が(望ましい頻度よりもはるかに多く)障害となっていました。
  3. JavaScript モジュールの移行には、(関連性がないように見える)技術的負債の大量のクリーンアップが含まれていました。最新の標準化されたモジュール形式に移行したことで、コーディングのベスト プラクティスを最新のウェブ開発に合わせて調整できました。たとえば、カスタム Python バンドルを最小限の Rollup 構成に置き換えることができました。
  4. コードベースに大きな影響(コードの 20% が変更)があったにもかかわらず、報告されたリグレッションはごくわずかでした。最初の数ファイルの移行では多くの問題が発生しましたが、しばらくすると、部分的に自動化された堅牢なワークフローが確立されました。つまり、この移行では、安定したユーザーへの悪影響は最小限に抑えられました。
  5. 特定の移行の複雑さを他のメンテナンス担当者に教えるのは難しい場合があり、不可能なことさえあります。この規模の移行は追跡が難しく、多くのドメイン知識が必要です。そのドメイン知識を同じコードベースで働く他の人に移行することは、その人が行っている仕事にとってそれ自体望ましいことではありません。何を共有し、何を共有しないかを判断するのは、技術的な問題ですが、必要なものです。そのため、大規模な移行の量を減らすか、少なくとも同時に実行しないようにすることが重要です。

プレビュー チャネルをダウンロードする

デフォルトの開発用ブラウザとして Chrome の CanaryDevBeta のいずれかを使用することを検討してください。これらのプレビュー チャンネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたりできます。また、ユーザーよりも早くサイトの問題を見つけることもできます。

Chrome DevTools チームに問い合わせる

次のオプションを使用して、DevTools の新機能、アップデート、その他のトピックについて話し合います。