React 化した共通ヘッダーを kintone の全ページに適用しました!

こんにちは!kintone フロントエンドリアーキテクチャプロジェクト (フロリア) で、エンジニアとして活動している @nissy_dev です。

以前投稿したチーム紹介記事の中で、コンポーネント単位での共通ヘッダー部分の React 化に取り組んでいることを紹介しました。それから約半年の期間を経て、React 化した共通ヘッダーを全ページへ適用することができました。kintone の7月版のアップデート情報にも記載されています。

今回は、このリリースの流れや技術的な詳細について紹介したいと思います。技術的な内容については、昨年の 12 月にマイクロフロントエンドに挑戦しているという記事を公開しているので、マイクロフロントエンドを実際に適用してみた感想などにも触れたいと思います。


目次


React 化した共通ヘッダーの全ページへの適用

kintone では、現在も Closure Tools で表示されているページを React に置き換える作業が進んでおり、React で表示されているページと Closure Tools で表示されているページが存在しています。

チームの紹介記事を書いた時点 (昨年の 11 月) では、React 化した共通ヘッダーを npm パッケージとして配布し、React で表示されているページのみに適用していました。

今回のリリースでは、適用できていなかった Closure Tools で表示されているページにも対応し、React 化した共通ヘッダーを全ページへ導入しました。

React 化した共通ヘッダーが全ページへ適用されました

リリースの詳細な流れ

全ページに React 化した共通ヘッダーを適用していく上で考慮する必要があったのが、kintone のカスタマイズです。kintone のカスタマイズ機能は、JavaScript と CSS を利用して UI や動作を自由に変更することができます。

これまで React 化した共通ヘッダーを適用したページは、カスタマイズが反映されないページになっており、これらの影響はありませんでした。一方で、全ページへの適用となると、カスタマイズ機能への考慮が必要になります。カスタマイズ機能は、共通ヘッダー部分に対しても使われており、UI を大きく変更しているケースがありました。

シンプルな例としては、次の画像のような独自のショートカットボタンを追加するものがあります。

カスタマイズのイメージ

今回の共通ヘッダーの React 化では大きく DOM 構造が変わるため、このようなカスタマイズが正常に動かなくなる可能性が高いです。カスタマイズへの影響を考慮して、リリースについては次のように進めています。

  • 6 月: 全ページへ適用が可能になる (有効化機能を提供)
  • 7 月: デフォルトで全ページへ適用 (無効化機能を提供)
  • 11 月: 無効化機能の廃止

6 月に有効化機能を提供した理由としては、早めにユーザーへの周知を行うためです。この期間に社内でのドッグフーディングも行いました。その後は、デフォルトで全ページに適用しつつも、ユーザーがカスタマイズを修正するのに必要な時間をふまえて、4 ヶ月間の無効化機能を提供する予定です。

これらのリリースの方針に関しては、開発チーム、PM、営業などの多くのメンバーからの意見をふまえて決定しました。ちなみに、今後も React 化による DOM 構造の変更を予定しているため、このような UI に依存したカスタマイズは推奨していません1

Closure Tools による制約をふまえた適用方法

全ページに共通ヘッダーを適用していく上での一番の課題は、Closure Tools で npm のパッケージを利用するのが難しいという点でした。 そのため、共通ヘッダーを npm パッケージとして配布するのではなく、サーバーが返す各ページの HTML に共通ヘッダーをレンダリングするための DOM と JS を埋め込む方法で実装しました。

次のように、マウント用の DOM とレンダリング用の JS を読み込む script タグを HTML に追加します。

<html>
  <head>
    <script type="module" src="https://xxxxxx/0.0.1/common-header.js"></script>
    ...
  </head>
  <body>
    <div id="common-header"></div>
    ...
  </body>
</html>

読み込んでいる common-header.js では、共通ヘッダーのコンポーネントをレンダリングさせます。

import { createRoot } from "react-dom/client";

const domNode = document.getElementById("common-header");
const root = createRoot(domNode);
root.render(<CommonHeader />);

通知アイコンなどに代表される共通ヘッダーとページコンテンツ間でやり取りする必要のある機能については、Custom Event を使って実装を行います。

通知詳細ページにおける Custom Event を利用した共通ヘッダーとページコンテンツとのやり取り

マイクロフロントエンドに関する書籍でよく紹介されている Web Components や iframe などを利用する方法についても調査しましたが、採用には至りませんでした。Web Components を使って共通ヘッダーを提供しようとしたケースでは、CSS in JS のスタイルやモーダルなどに関してランタイムで Shadow DOM の外に要素を作ってしまう問題があり、これらを期待通りの挙動に修正するのが困難でした。

また、React で表示されているページのために npm パッケージの配布を続けることはできますが、ビルド周りの設定が複雑になることを考慮して、このタイミングで完全に廃止しました。このため、React 化されたページでは比較的サイズの大きい react-dom などのライブラリが共通ヘッダーとページコンテンツから重複して読み込まれています。

サイズの大きいライブラリの重複読み込みについては、パフォーマンスの観点からすると良くない状況です。一方で、共通ヘッダーとページコンテンツの間でライブラリを共通化すると、チーム間の結合度が高まるという新たな課題も発生します。現時点では、パフォーマンスよりもチーム間の独立性を重視し、重複読み込みを許容しています。

今回の適用方法で良かったところ

今回の適用方法で良かったところとしては、チームで独立してデプロイできる点が挙げられます。npm パッケージを配布する場合におけるデプロイでは、パッケージを利用しているすべてのチームにバージョンの更新を依頼し、その依頼が完了するのを待つ必要がありました。一方で、今回の適用方法では、基本的にチーム外への依頼は不要であり、自由にデプロイすることが可能です。

「基本的に」と書いたのは、共通ヘッダーとページコンテンツ間で利用しているインターフェースを変更する場合にはチーム間の調整が必要になるためです。現状では、共通ヘッダーとページコンテンツ間は疎結合になっており、インターフェースの変更はほとんど行われません。

今回の適用方法で困ったところ

今回の適用方法で困ったところとしては、主に次の2つの点がありました。

  • 共通ヘッダーの要素に依存しないように既存の機能を修正する必要があった
  • React で表示されているページで CSS in JS のスタイルの読み込み順の問題が発生した

共通ヘッダーの要素に依存している機能の修正

Closure Tools で表示されているページでは、共通ヘッダーの要素に依存した機能がありました。具体的には、共通ヘッダー内の要素を getElementById を使って取得し、その要素の高さを利用するものなどになります。これらについては、既存のコードを調査したり、React 化した共通ヘッダーを有効にした状態で既存の E2E のテストを流してみることで気づくことができました。

共通ヘッダーの要素に依存している機能の問題点は、getElementById で要素が必ず取得できることを前提にロジックが組まれていることです。今回の適用方法だと、共通ヘッダーを表示させるためのスクリプトは、type="module" を付与した読み込みにしています。これは、パフォーマンス観点で有利なことに加えて、通常の script タグによって読み込まれるカスタマイズコードへのグローバル変数空間に対する影響をなくすためです。この読み込み方法の場合、共通ヘッダーはページコンテンツと独立して非同期にレンダリングされるため、ページコンテンツ側で必ず要素を取得できる保証はありません。

理想としてはヘッダーの高さに依存しない方法で機能の再実装を試みることですが、実装コストなども鑑みて今回の場合は次のような関数を用意して対処することにしました。

// 実際は Closure Tools のコードになるので、実装方法や文法は異なります
export const getCommonHeaderHeight = () => {
  const element = document.getElementById("common-header");
  if (element !== null) {
    return element.clientHeight;
  }
  // 取れなかった時のためのフォールバック値
  return 48;
};

要素が取得できなかった場合のフォールバック値を定義しなければならず、共通ヘッダーとページコンテンツでお互いの実装を意識しなければならないのが課題になっています。

CSS in JS によるスタイルの読み込み順の問題

社内には、kintone のデザインシステムを開発しているチームがあります。React で表示されているページに含まれる共通ヘッダーとページコンテンツは、このチームが配布しているコンポーネントライブラリを利用しています。コンポーネントライブラリでは、スタイル定義に styled-components が採用されており、ランタイムで動的に style タグを埋め込むことでスタイルを適用します。

このとき、共通ヘッダーの方でデザインシステムのコンポーネントのスタイルを一部上書きすることがあったのですが、スタイルの読み込み順によってその上書きが打ち消されてしまう問題が発生しました。

CSS in JS によるスタイルの読み込み順の問題

図のように上書き用の .abc-override を共通ヘッダー側で定義していたのですが、詳細度が同じなため後から読み込まれる .abc によって打ち消されてしまいます。

この問題については、次の issue でも議論されています。issue では、ビルド時にクラスに prefix をつけるなどの解決方法が出されていますが、今回問題になったスタイルはすでにビルドされているコンポーネントライブラリ内のクラスで定義されていることから、issue で挙げられている方法での対応は困難でした。

今回のケースでは、デザインシステムのコンポーネントを参考にしつつ、チームで独自のコンポーネントを実装することで対応しました。これは、スタイルの上書きが数箇所でしか行われておらず、対象のコンポーネントも機能の少ないプリミティブなものであったためです。

さいごに

今回の記事では、React 化した kintone の共通ヘッダーを全ページに適用した際のリリースの流れや技術的な詳細について紹介しました。今後は、React 化した共通ヘッダーを無効化している割合なども見ながら、11 月の無効化機能の廃止に向けて進んでいく予定です。

移行期間を設けるなどのカスタマイズ機能を考慮した今回のリリースは、OSS でのリリースにも通ずるものがあると個人的に感じました。普段の OSS 開発でも、破壊的変更の扱いには気をつけていきたいところです。

現在フロリアでは、今回リリースした共通ヘッダー部分以外にも多くのページで刷新が進んでいます!もしこの記事を読んでサイボウズやフロリアに興味を持った方がいれば、次のリンクからご応募お待ちしています!


  1. kintone コーディングガイドラインにも記載されています。