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 チームに参加した場合、(おそらく)すでに 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 のインポート/エクスポートを使用できます。

また、移行は 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 はファイルの名前です。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.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. 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 件前後で、主に 2 人のエンジニアが実施すると見込まれます。変更された変更の規模に関する明確な統計情報はありませんが、変更された行数(各 CL の挿入と削除の絶対差の合計として計算)は、約 30,000 行(DevTools フロントエンド コードの約 20%)と慎重に推定されています。

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

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

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

振り返り

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

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

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

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

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