こんにちは、フロントエンドエキスパートチームの麦島(@mugi_uno)です!
2021年5月に新しくメンバーとして加わり、富山からフルリモートで働いています。
最近はチームメンバーに誕生日を祝ってもらって嬉しかったです🎉
さて、以前に "kintoneのフロントエンド刷新に向けた取り組み"*1 というエントリでもご紹介しましたが、現在サイボウズ社内では kintone で利用するフロントエンドの技術スタックを刷新する取り組みを進めています。
その一環として、 "Closure Tools DevTools" という Google Chrome 向け拡張機能を作成しました。
作成した DevTools は kintone に限らず利用することができるため、Chrome ウェブストアで公開しています。
ソースコードも次のリポジトリでご確認いただけます。
フロントエンドエキスパートチームでは、活動内容の一貫として「探究」も推進されているため、 その時間を利用してコツコツ開発し、期間としてはだいたい1ヶ月ほどで形になりました。
今回は DevTools を作成するに至った背景と、作成時に工夫した点について紹介します。
Closure Tools 製既存コード読解の難解さ
kintone は歴史も長いためコードベースもとても大きく、新たに触れるメンバーは既存コードの理解に苦労しがちです。 自分も最初は正面からコードとにらめっこしていましたが、例に漏れずなかなか捗りませんでした。
なぜ理解が大変なのかを一度立ち止まって考えてみたところ、次のような要因に気付きました。
画面上の表示要素と対応コードの突き合わせが難しい
kintone のコードでは、画面要素の多くが Closure Tools の goog.ui.Component
*2 を継承するコンポーネントとして作成されています。
現在では React や Vue.js によって一般的になっていますが、Closure Tools を導入した当時としてはコンポーネントの親子関係を構造化して定義できることや、DOMのライフサイクルを管理できることは画期的でした。
しかし、画面の要素から「これはどのコンポーネントなんだろう?」とコードを辿るのは容易ではありません。
たとえば、次のように kintone の画面に表示されているテキストがどのコンポーネントで描画されているか、該当コードを特定したいケースを考えてみます。
この場合、多くは次のような方法を取る必要があります。
- DOMのクラス名やIDからコード全体をgrepしてそれっぽい部分を探す
- 画面上の文言からコード全体をgrepしてそれっぽい部分を探す
- ベテランエンジニアのエスパー能力で見つけてもらう 🌞
つまり、基本的には "grepして探す or 経験と勘で見つける" という状態です。 慣れるまでは1ファイルを特定するだけでもそれなりに苦労します。
Closure Tools によるイベント制御でのコード上のジャンプ
goog.ui.Component
の継承コンポーネントでは、自動的に goog.events.EventTarget
*3 というクラスも継承しており、独自のイベント制御機構が利用可能になります。
これを利用すると、DOMとは切り離したレイヤーでのカスタムイベントによる Pub/Sub や、親子コンポーネント間でのイベントバブリングを実現できます。 しかしkintone上では、この機構による "イベントをPublish → Subscribe 箇所で再度別のイベントをPublish" という処理が多く存在します。 これはコードを読んでいる際には全くの別ファイルにジャンプすることになるため、「この処理は一体どこで実行されてるんだ……??」と迷子になりがちです。
コンポーネントの保持するデータが見えない
goog.ui.Component
の継承コンポーネントでは、依存するデータを this.xxx
の形でコンポーネントのインスタンスに紐付ける形で保持しています。
これは外部から参照することはできず、内部データを確認したい時には、debugger を仕込んで止めるか console.log でログを出力する必要があります。
仕様理解のために動かしながらコードリーディングする際にも、頻度の高い操作のため回数が多く煩わしさを感じることもあります。
根本から解決を試みる
これらの問題を抱えたまま闇雲にコード理解を進めようとしても効率が悪く、今後新たに参加するメンバーも全員同じ問題を抱えることが容易に予想できます。
根本から解決できる仕組みを考えてみることにしました。
コンポーネントごとに可視化してみる
画面上の表示要素と対応コードの突き合わせが難しい
この問題の対処のため「画面にコンポーネント名を描画した資料があれば理解できるのでは?」と思い立ちます。
幸いにもコンポーネントはすべて goog.ui.Component
を継承しているという共通点がありました。
そこで、 goog.ui.Component
が提供するライフライクルメソッドを乗っ取り、画面の初期描画タイミングで強制的にアウトライン要素を差し込むことでコンポーネント境界を可視化してみました。
実験的にコンポーネント名として'hoge'を表示してみたものが次の画像です。
それっぽく要素がアウトラインで囲われてはいますが、しっかり見ていくと色々と問題が出てきました。
- コンポーネント名が長く他コンポーネントと被って見づらい
- 親子関係にあるコンポーネントが見づらい
- 何か操作をトリガーに描画される要素だと見えない
- 総じて見づらい
どうやら一気に表現するのは無理があるようです。
DevTools が欲しいことに気付く
作業の様子は適宜 Slack 上の分報に流していましたが、そこで @koba04 と話していて 「自分たちが欲しいのは、React や Vue.js にあるような DevTools では?」という気付きを得ます。
確かに、やりたいことが何かを考え直してみると、
- 画面上から特定要素がどのコンポーネントか知りたい
- コンポーネントから発火したイベントを知りたい
- コンポーネントが持っているデータを知りたい
と考えると、それはもう DevTools です。
Closure Tools DevTools を作る
というわけで、Closure Tools DevTools を Google Chrome 向け拡張として実際に作成することにしました。
普段当たり前のように利用している React や Vue.js の DevTools ですが、実際にそれ相当のものを作るとなると、色々と考慮すべきことが多いことに気付きます。
Message Passing
今回のような DevTools を作成する際、Google Chrome 拡張機能における次の3つのレイヤーを意識する必要があります。 *4
- Inspected Window → 実際にコンテンツが描画されるwindow
- Background page → extensions API へフルアクセス可能なバックグランドスクリプト。Inspected Window と DevTools page の橋渡しも行う。
- DevTools page → DevTools のパネル
これらのレイヤー間で連携を取るために、頻繁に Message Passing が発生します。
「ホバーで画面上の要素を選択し、該当コンポーネントを DevTools のパネルに表示」という、よくある Inspect 操作で考えてみても
- DevTools page で Inspect を有効化
- Inspect が有効化された情報を Background page 経由で Inspected Window に通知
- Inspected Window でホバーによるイベントハンドリングを有効化
- ホバーのタイミングで要素情報を取得
- 要素情報を Background page 経由で DevTools page に通知
- 通知を元に DevTools page 上でコンポーネント情報を表示
- Inspect の終了を Background page 経由で Inspected Window に通知
- Inspected Window でホバーによるイベントハンドリングを無効化
と、多くのメッセージのやり取りが必要なことがわかります。
幸いにも現代には TypeScript という味方がいるため、飛び交う Message の情報をきっちり型定義して各レイヤー間で共有することで、スムーズに書くことができました。
また、設計検討時には Vue DevTools のコードをかなり参考にさせていただきました。
初期化処理
Closure Tools のライフサイクルイベントに処理を割り込ませるため、画面上でアプリケーションのコードが実行されるよりも早いタイミングで、 DevTools 向けのコードを差し込む必要があります。
しかし、同時に次のような点はカバーしておきたいです。
- 必要がない場合 (DevTools がインストールされていない or Productionビルド時など) には実行してほしくない
- DevTools 側に必要なコードは、できる限り DevTools 側で保持してほしい (アプリケーション側のコード変更量は最小限にしたい)
最終的には次のような流れで処理をすることで対処しました。
- DevTools 読み込み時点で、スクリプトファイルを画面に差し込み … ①
- ① のスクリプト内で初期化用フック関数を window に定義 … ②
※ DevTools の Content Script から直接 window オブジェクトに破壊的変更はできないため、このような手順を取る必要があります - アプリケーションコード側で、Development時 かつ ② の関数が存在する場合は ② を実行して初期化
これにより、DevTools が存在し、かつアプリケーション側に初期化用フック関数の呼び出しが仕込まれていない場合には実行されず、 本当に必要なときにだけ DevTools の初期化処理が実行されることになります。
Closure Tools DevTools でできること
画面要素から対応するコンポーネントの確認
Inspectモード状態から要素上でマウスホバーすると、アウトラインと共にコンポーネント名が表示されます。 また、DevTools の Components パネル上ではコンポーネントツリーが表示されるため、コンポーネントの親子関係も把握できます。 選択したコンポーネント内部で保持するデータをパネル右側で確認することもできます。
発火したイベントの確認
DevTools の Events パネル上では、 goog.events.EventTarget
を継承したクラスで dispatchEvent
(イベントの発火) の実行履歴が表示されます。
コンポーネント名に加え、ペイロードとして渡された情報も確認できます。
社内でのシェア
完成・公開後に少しだけ時間をもらい、一部 kintone 開発メンバーに Closure Tools DevTools の紹介をさせてもらいました。 その後、社内エンジニアから便利に使っている旨の声も聞こえており、嬉しい限りです。
まとめ
今回作成した Google Chrome 拡張は、フロントエンド刷新を終えれば社内ではお役御免となるため、 作り始めた時点で将来的な利用終了が確定しているツールです。
「あったら便利そうだけど、いまさら作るのもな……」と考えたくなりますが、しかし、kintoneのフロントエンド刷新は一朝一夕で終わるものではなく、ある程度腰を据えて立ち向かう必要があるテーマです。 今後新たに関わるメンバーも多いことが予想されるため、刷新前のコード理解を加速させるための仕組み・ツールは、 全体的な作業効率向上のためにも重要だと考えています。
理想に描くフロントエンド環境に1歩でも近づくため、これからも積極的にやっていきたいと思います!