すべてのWebデベロッパーは、管理されたステージング環境でアプリケーションが完璧にレンダリングされる様子を見て、安心する感覚を知っています。しかし、埋め込みウィジェットをリリースすると、その安心感は完全に打ち砕かれます。あなたはもはや、ページの設計者ではありません。あなたは招かれざる客となり、自分が所有していないDOM、自分が作成していないCSSのカスケード、そして自分に不利に働くかもしれないランタイム環境へと、Reactアプリケーションを注入することになるのです。Clanker Supportウィジェットの構築とリリースを通じて、私たちは、誰かのテーマ内でコードが実行された瞬間に、標準的なWeb開発の前提が崩壊することを学びました。ホストサイトがフォントサイズをリセットしたり、空のdivを非表示にしたり、あるいは設定を読み込む前に構成を無効化してしまうようなスクリプトのライフサイクルを強制したりすることがあります。以下は、私たちが実戦での痛い経験から導き出した、防御のためのルールです。

1つのファイル、1つの失敗モード

モダンなバンドラーは、コード分割やダイナミックインポートの誘惑を仕掛けてきます。それに抗ってください。埋め込みウィジェットは、単一の即時実行関数式(IIFE)として配信する必要があります。顧客があなたのスクリプトタグをテンプレートにコピーするとき、彼らは1回のネットワークリクエストを期待しています。もしあなたのバンドルが、重いパースライブラリや言語モデルのチャンクを遅延読み込み(lazy-load)しようとすれば、そのフェッチは静かに失敗する可能性があります。ホスト側が厳格なContent Security Policyを設定していたり、強力な広告ブロッカーが動いていたり、あるいはあなたのpublicPathの想定と一致しないCDNパスを使用していたりするかもしれません。すべてを1つのIIFEに強制することで、二次的なチャンク読み込みに伴う未知の要素を排除できます。もし依存関係が内部の遅延読み込みを強行しようとするなら、ビルド時に軽量なスタブへとエイリアスを貼ってください。これにより、アーティファクトは単一になり、失敗モードも単一になります。また、顧客のサイト管理者が壊れたチャットバブルのスクリーンショットをメールで送ってきた際も、デバッグが格段に容易になります。

Shadow DOMからも漏れ出す

デベロッパーはしばしば、Shadow DOMを難攻不落の要塞のように扱います。Shadow DOMは確かにセレクターをホストページのCSSから隔離してくれますが、継承(inheritance)までは隔離してくれません。font-familyline-heightcolortext-alignといったプロパティは、境界が存在しないかのように、あなたのシャドウツリーへと下方向に流れてきます。font-family: "Comic Sans MS" というグローバルな宣言を持つShopifyストアは、ルート要素で継承可能なすべてのプロパティを明示的に固定しない限り、あなたが精巧にデザインしたサポートウィジェットを汚染してしまいます。ホストレベルで、独自のタイポグラフィ、スペーシング、テキスト配置を具体的な値で設定してください。親ページは敵対的であると想定し、重要な要素はすべてリセットしてください。Shadow DOMが守るのはクラス名であって、デザイン(美学)ではありません。

消える空のdiv

これは、私たちの不意を完全に突いた問題でした。Shopify Dawnを含む多くの人気テーマには、一見無害に見える次のようなCSSルールが含まれています:div:empty { display: none; }。ウィジェットがマウントされるとき、通常は空の状態で始まるホストのdivをターゲットにします。JavaScriptが実行され、Reactがノードをハイドレーションするまでの間、そのdivは文字通り「空」です。すると、テーマのスタイルシートがそれを非表示にしてしまいます。スクリプトが実行され、ReactDOM.createRootを呼び出しても、何も表示されません。コンソールにエラーすら出ません。要素がレイアウト上から単に消滅してしまったのです。解決策は、力技かつ明示的なものです。マウントポイントに display: block !important というインラインスタイルを適用してください。後でCSS-in-JSライブラリが対処してくれるのを期待してはいけません。スタイルシートが適用される頃には、ホストテーマの勝利が決まっているのです。

remを捨ててpxを使う

通常のアプリケーションでは、remのような相対単位は責任ある(適切な)選択です。しかし、埋め込みにおいては、それらはリスクとなります。remの値は、ウィジェットではなく、ホストドキュメントのルートhtmlフォントサイズに基づいて解決されます。もしホストページが html { font-size: 10px; } と設定していたり、古い62.5%のテクニックを使用していたりすると、あなたのタイポグラフィやスペーシングのスケール全体が予告なく変化してしまいます。快適な 1.6rem の行間が 16px に縮小したり、パディングが判読不能なほど細い隙間になったりすることもあります。ホストのルートサイズを予測したり制御したりすることはできないため、埋め込みウィジェットにとって唯一「誠実な」単位はピクセル(px)なのです。ピクセルは、周囲のページの想定に関わらず、同じ物理的なサイズでレンダリングされます。他サイトのカスケードの中で生きるなら、remが持つ理論的なアクセシビリティの柔軟性を捨て、pxが持つ実用的な信頼性を取るべきです。

設定が消える前に読み込む

script tagのdata属性を通じてウィジェットに設定を渡す場合、それらを同期的に読み取る必要があります。ブラウザはdocument.currentScriptを提供しており、スクリプト自身が自身のタグを検査できますが、この参照は一時的なものです。DOMContentLoadedや何らかの非同期境界を待ってしまうと、document.currentScriptnullになります。設定が消えてしまいます。これらの属性は、スクリプト実行の最上位レベルですぐに読み取ってください。APIキー、ウィジェットID、カラーテーマをその場でキャプチャしてクロージャまたはモジュール変数に格納し、それからReactの起動を進めてください。

スクリプトURLにAPIのオリジンを選択させる

本番環境のAPI URLをバンドルにハードコードすることは、環境ごとに問題が倍増する間違いです。代わりに、スクリプト要素自身のsrc属性からAPIのオリジンを導き出してください。もしウィジェットがhttps://cdn.staging.example.com/widget.jsからロードされるなら、そのAPIコールはデフォルトでhttps://api.staging.example.comになるべきです。開発者がlocalhost:3000から配信されるローカルのHTMLファイルにスクリプトタグを配置した場合、ローカルビルドはリクエストをローカルサーバーにルーティングする必要があります。この慣習により、環境固有のビルド、フィーチャーフラグ、あるいは埋め込みユーザーによる手動設定の必要性がなくなります。インフラストラクチャの場所が配信場所によって暗示されるため、ただ「動く」のです。

キャッシュヘッダーをホットフィックスの命綱として扱う

ユーザーは一度フッターテンプレートにスクリプトタグをコピーしたら、あとは忘れてしまいます。5,000人のマーチャントにメールを送って、バージョン用のクエリパラメータを更新してもらうことなど不可能です。つまり、キャッシュヘッダーはインシデント対応戦略の一部なのです。ウィジェットのバンドルに短いmax-ageを設定し、重要な修正をリリースした際に、数週間ではなく数時間以内に反映されるようにしてください。キャッシュの保持期間が長いことの利便性は、回収不可能な壊れたバージョンが数千のサイトで稼働し続けているという麻痺状態のリスクに見合うものではありません。CDNのトラフィックコストを受け入れてください。あなたの精神衛生はそれにかかっています。

iframe埋め込みのためにCSPを反転させる

iframeベースの埋め込みオプションを提供する場合、Content Security Policy(CSP)は標準的なWebアプリケーションの考え方とは逆転させる必要があります。通常、クリックジャッキングを防ぐためにフレーミングを禁止するかもしれません。しかし、ウィジェットの場合は、それを許可しなければなりません。frame-ancestors *を設定して、どのサイトでもあなたのiframeをホストできるようにします。その上で、それ以外のすべてに対して厳格になってください。そのiframeポリシー内で、script-srcstyle-srcconnect-srcを厳しく制限します。フレーミングというベクトルを通じて、意図的にWeb全体に対して自分自身をさらしているため、ホストページが操作を試みたとしても、iframe内で実行されるコードが悪挙を起こす余地がないようにしなければなりません。

ゲストのマインドセット

埋め込み(embed)の構築は、標準的なWebアプリケーションの構築とは異なる姿勢を要求します。自身のアプリでは、コンテナ、ルーティング、ビルドパイプライン、グローバルスタイルを管理しています。しかし、埋め込みにおいては、何も所有していません。ホストページは任意のものであり、しばしば古く、時には敵対的であり、常にあなたの制御外にあります。あらゆる仮定は防御的である必要があります。意図することを明示的に指定し、環境を積極的に検証し、目に見えない破損を想定して設計してください。Clanker Supportウィジェットが今日機能しているのは、Webが予測可能だからではなく、私たちがWebが予測可能であると信じるのをやめたからです。