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

標準化されていないモジュール形式を選択すると、メンテナンス担当者が使用するビルドツールやツールとのインテグレーションの構築に時間を費やさなければなりません。

こうした統合は脆弱であることが多く、機能のサポートが不足していたため、メンテナンスにさらなる時間が必要でした。場合によっては、微妙なバグが発生し、それが最終的にユーザーに配布されることがありました。

JavaScript モジュールが標準だったため、VS Code などの IDE、Closure Compiler/TypeScript などの型チェッカー、Rollup/Miniifier などのビルドツールが、記述したソースコードを理解できるようになりました。さらに、新しいメンテナンス担当者が DevTools チームに参加したとき、JavaScript モジュールに(おそらく)すでに精通しているにもかかわらず、独自の module.json 形式について時間を費やす必要はありません。

もちろん、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 のインポート/エクスポートを使用できるようになります。

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

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.localFunctionInFilelocalFunctionInFile に書き換えます。しかし、この 2 つの変換を分けることで、自動化が容易になり、より安全に適用できることがわかりました。 したがって、「同じファイル内のすべてのシンボルを移行する」が、import フェーズの 2 番目のサブフェーズになります。

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

これらの問題に取り組む中で発見した 1 つの発見は、テストが「ずさんな」モードで実行されていたことです。JavaScript モジュールではファイルが "use strict" モードで実行されるため、これもテストに影響します。結局、with ステートメントを使用したテストなど、かなりの量のテストがこのスロッピーに依存していました。

最終的に、最初のフォルダを更新して export ステートメントを含めるまでには、約 1 週間かかり、再リリースによる複数回の試行が必要でした。

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 が必要な場合は、循環依存関係が明示的に指定されます。 これはすぐには問題にはなりません。ただし、グローバル スコープのコードに副作用の関数呼び出しがある場合を除きます。これは DevTools でも同じでした。全体として、変換を安全に行うには手術とリファクタリングが必要でした。

JavaScript モジュールによるまったく新しい世界

2019 年 9 月の開始から 6 か月後の 2020 年 2 月、最後のクリーンアップui/ フォルダで行われました。これにより、移行は非公式に終了となりました。状況が緩和された後、移行は 2020 年 3 月 5 日に完了したことを正式に決定しました。🎉

現在、DevTools のすべてのモジュールが JavaScript モジュールを使用してコードを共有します。以前のテストや DevTools アーキテクチャの他の部分との統合では、引き続き一部のシンボルをグローバル スコープ(module-legacy.js ファイル内)に配置します。時間の経過とともに削除されますが、今後の開発の妨げとはみなされません。JavaScript モジュールの使用方法に関するスタイルガイドも用意されています。

統計

この移行に関連する CL(変更リストの略語。Gerrit で使用される用語で、GitHub pull リクエストと同様)は控えめに見積もってみると、250 の CL(主に 2 人のエンジニアが実行)です。行われた変更の規模に関する明確な統計はありませんが、変更された行の数(各 CL の挿入と削除の絶対差の合計として計算)は、約 30,000(DevTools フロントエンド コード全体の約 20%)です。

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

Chrome Stable 版では、今回の移行の一環として導入されたリグレッションが 1 つ確認されています。 不要な default エクスポートが原因で、コマンド メニュー内のスニペットのオートコンプリートが中断しました。他にも不具合がいくつかありますが、自動テストスイートと Chrome Canary ユーザーから報告された問題は、Chrome の安定版ユーザーに提供される前に修正されました。

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

振り返り

  1. 過去の決定は、プロジェクトに長期的な影響を与える可能性があります。JavaScript モジュール(およびその他のモジュール形式)はかなり前から利用可能でしたが、DevTools は移行を正当化する立場にはありませんでした。移行のタイミングとしないタイミングは、経験に基づく推測に基づいて判断するのは困難です。
  2. 当初の推定期間は数か月ではなく数週間でした。これは主に、最初のコスト分析で想定していた以上の予期しない問題が見つかったことが原因です。移行計画は堅実でしたが、技術的負債が(私たちが望む以上の頻度で)阻害要因でした。
  3. JavaScript モジュールの移行には、(一見無関係と思われる)大量の技術的負債のクリーンアップが含まれていました。最新の標準化されたモジュール形式に移行することで、コーディングのベスト プラクティスを現代のウェブ開発に整合させることができました。 たとえば、カスタムの Python バンドラを最小限の Rollup 構成に置き換えることができました。
  4. Google のコードベースに大きな影響があったにもかかわらず(コードの約 20% を変更)、回帰はほとんど報告されていません。最初の 2 つのファイルの移行では多くの問題が発生しましたが、しばらくすると、部分的に自動化されたしっかりしたワークフローが確立されました。 つまり、この移行では、安定したユーザーに対するユーザーへの影響は最小限に抑えられていました。
  5. 特定の移行の複雑さを他のメンテナンス担当者に教えるのは難しく、ときには不可能なこともあります。このような規模の移行は難しく、多くのドメイン知識が必要です。同じコードベースで作業する他のユーザーにそのドメインの知識を転送することは、仕事にとっては望ましくありません。共有すべき内容と共有すべきでない詳細事項を把握することは、アートではありますが、必要なものです。そのため、大規模な移行の量を減らすか、少なくとも同時には実行しないことが重要です。

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

Chrome CanaryDevBeta を既定の開発ブラウザとして使用することをご検討ください。これらのプレビュー チャンネルでは、最新の DevTools 機能にアクセスしたり、最先端のウェブ プラットフォーム API をテストしたり、ユーザーが実際に体験する前にサイト上の問題を検出したりできます。

Chrome DevTools チームへのお問い合わせ

投稿内の新機能や変更点、または DevTools に関するその他のことについて話し合うには、次のオプションを使用します。

  • crbug.com からご提案やフィードバックをお送りください。
  • DevTools の問題を報告するには、DevTools でその他のオプション アイコン その他   > [ヘルプ] > [DevTools の問題を報告する] を選択します。
  • @ChromeDevTools にツイートします。
  • 「DevTools の新機能」の YouTube 動画または DevTools のヒントの YouTube 動画でコメントを残してください。