を使用して埋め込みウィジェットをビルドする


我々のチームCompanyCam 我々のユーザーが彼らのウェブサイトに埋め込むことができた装置を構築することを頼みました.ウィジェットは、応答をインストールし、簡単にフルスクリーンのアプリケーション体験を提供する必要があります.ここでは,技術的意思決定をどのようにして得たかを紹介し,解説した.

ディスカバリー
コードに飛び込む前に、発見の間、我々のチームが学んだいくつかのことをすぐに議論したい.うまくいけば、これはあなたのプロジェクトのための正しい意思決定を支援します.
製品の詳細を学習した後、コードベースは2つの要件を持っていた.

カプセル化
我々のチームは、外部CSSが我々のコードにカスケーディングするのを防ぐ必要がありました.加えて、我々のスタイリング我々のアプリケーションにスコープする必要があります.私たちはiframeでウィジェットをラップすることを調査しましたa nested browsing context .これは我々が必要とするカプセル化を提供しました、しかし、我々は良質なフルスクリーン経験を提供するためにiframeを制御するのが難しいとわかりました.The Fullscreen API 可能性のある解決策であったが、必要としなかったbrowser support . IFrameを使用して小さな製品をカプセル化することは大きな解決策であるかもしれませんが、我々のユースケースに合わなかったでしょう.
我々は、我々に注意を向けましたShadow DOM API . シャドウDOMは、隠されたDOMツリーを任意の要素にアタッチする方法を提供します.これはカプセル化を作成しますが、アプリケーションのコントロールを持つ能力を制限しません.さらに、シャドウDOM APIはgood browser support .

小さい束
ウィジェットが急速にロードされることは不可欠です.チームが適切な戦略を持っていたので、アプリケーションを分割するのは難しいということが明らかになった.で、ユーザインタフェースを書くReact したがって、それに固執するのは意味がありました.
サードパーティのライブラリを追加すると、我々のバンドルのサイズが大きくなった.私たちはPreact この問題の良い解決策だった.これは、はるかに小さいパッケージではなく、反応と同じ機能を提供します.あなたの未パックのサイズを比較することができますPreact 合わすReact and React-DOM そして、重要な違いを見てください!
さて、いくつかのコードにジャンプしましょう!クローン無料this starter repo 働く例があなたのために役に立つならば.

影DOM層でアプリをマウント
準備は簡単ですintegrate into an existing project . 私たちの前提を取り付けるApp コンポーネントは反応に似ているはずです.
/* @jsx h */
import { h, render } from "preact";

import App from "./components/App.jsx";

const appRoot = document.querySelector("#app-root");

render(<App />, appRoot);
シャドウDOM層を追加しましょう.
/* @jsx h */
import { h, render } from "preact";

import App from "./components/App.jsx";

// app shadow root

const appRoot = document.querySelector("#app-root");
appRoot.attachShadow({
  mode: "open",
});

render(<App />, appRoot.shadowRoot);
シャドウホストと呼ばれる通常のDOMノードにシャドウDOM層を取り付けることができます.これを呼び出すことによってattachShadow メソッドoptions パラメータとして.通過mode 値付きでopen シャドウDOMがshadowRoot プロパティ.その他の値mode is closed , これはshadowRoot 帰着null .
物事が正常に動作していることを確認するために、私たちは
ブラウザの開発ツールとDOMツリーを見てください.ここでは、私たちの影DOM層を見ることができます.


シャドウDOM
スタイルは、レンダリングするためにシャドウDOMの中にスコープされなければなりません.
/* @jsx h */
import { h, render } from "preact";

import App from "./components/App.jsx";

import styles from "./styles.css";

// app shadow root

const appRoot = document.querySelector("#app-root");
appRoot.attachShadow({
  mode: "open",
});

// inject styles

const styleTag = document.createElement("style");
styleTag.innerHTML = styles;
appRoot.appendChild(styleTag);

render(<App />, appRoot.shadowRoot);
あなたがWebpackを使っているならば、あなたが必要とすることを心に留めておいてくださいcss-loader このアプローチが働くために.クリエイトアstyle タグと設定innerHTML インポートされたスタイルシートに.

我々のアプリケーションが育ったので、我々のスタイルを管理することは厄介になりました、そして、我々のチームは別の解決を見つけたかったです.Companycamでは、当社のデザイナーはstyled-components . With styled-components , エーgenerated stylesheet is injected の終わりにhead ドキュメントの.私たちの影DOM層のため、これはいくつかの設定なしで動作しません.
/* @jsx h */
import { h } from "preact";
import styled, { StyleSheetManager } from "styled-components";

const Heading = styled.h3`
  color: #e155f5;
  font-family: sans-serif;
`;

const App = () => {
  return (
    <StyleSheetManager target={document.querySelector("#app-root").shadowRoot}>
        <Heading>Hey, Shadow DOM!</Heading>
    </StyleSheetManager>
  );
};

export default App;
The StyleSheetManager ヘルパーコンポーネントでは、スタイルの処理方法を変更できます.それを包んでくださいApp コンポーネントchildren とパスshadowRoot シャドウホストの値target . これは代替のDOMノードにスタイルを注入します.
前のテクニックと同様に、我々は我々のスタイルを影DOMの範囲内で見ることができます.


避ける
シャドウDOMは、外部のCSSセレクタが任意のマークアップに到達するのを防ぎます.しかし、それは影DOMの要素のために可能ですinherit CSS values . 私たちはreset properties to their default values プロパティの宣言によってall 値にinitial アプリケーションの親要素です.
/* @jsx h */
import { h } from "preact";
import styled, { StyleSheetManager } from "styled-components";

const Heading = styled.h3`
  color: #e155f5;
  font-family: sans-serif;
`;

const WidgetContainer = styled.div`
  all: initial;
`;

const App = () => {
  return (
    <StyleSheetManager target={document.querySelector("#app-root").shadowRoot}>
      <WidgetContainer>
        <Heading>Hey, Shadow DOM!</Heading>
      </WidgetContainer>
    </StyleSheetManager>
  );
};

export default App;

スタッキングの順序での戦いの戦い
それはWordPress、squarespace、Wix、またはゼロから何かであるかどうか、我々のウィジェットは、任意のウェブサイトに住んでいる必要があります.以来stacking order depends on the DOM tree hierarchy , 我々はすぐに見たz-index 私たちのフルスクリーンコンポーネントの問題.

Portals レンダリングする方法を提供するchildren アプリケーションのコンテキストの外部に存在するDOMノードに.マウントできますPortal を返します.私たちのケースでは、これらのフルスクリーンコンポーネントを可能な限りDOMツリーで高く表示する必要がありました.したがって、我々はPortalbody ドキュメントの中にウィジェットをインストールします.

をつくりましょうPortal アプリケーションのルートを起動します.
// index.js

/* @jsx h */
import { h, render } from "preact";

import App from "./components/App.jsx";

// shadow portal root

const portalRoot = document.createElement("div");
portalRoot.setAttribute("id", "portal-root");
portalRoot.attachShadow({
  mode: "open",
});
document.body.appendChild(portalRoot);

// app shadow root

const appRoot = document.querySelector("#app-root");
appRoot.attachShadow({
  mode: "open",
});

render(<App />, appRoot.shadowRoot);
シャドウホストを作成するPortal コンポーネントとそれを与えるid . それから私たちがしたようにappRoot , 新しい影DOM層をアタッチします.
/* @jsx h */

import { h, Fragment } from "preact";
import { useLayoutEffect, useRef } from "preact/hooks";
import { createPortal } from "preact/compat";
import styled, { StyleSheetManager } from "styled-components";

const Portal = ({ children, ...props }) => {
  const PortalContainer = styled.div`
    all: initial;
  `;

  const node = useRef();
  const portalRoot = document.querySelector("#portal-root");

  useLayoutEffect(() => {
    const { current } = node;
    if (current) {
      portalRoot.appendChild(current);
    }
  }, [node, portalRoot]);

  return (
    <Fragment ref={node} {...props}>
      {createPortal(
        <StyleSheetManager target={portalRoot.shadowRoot}>
          <PortalContainer>{children}</PortalContainer>
        </StyleSheetManager>,
        portalRoot.shadowRoot
      )}
    </Fragment>
  );
};

export default Portal;
次に、Portal コンポーネント.追加効果を追加するportalRoot コンポーネントの親要素に.そこからchildren and portalRoot.shadowRoot to createPortal .
あなたのスタイルをPortal シャドウDOMレイヤーStyleSheetManager と子要素のスタイルをデフォルト値にリセットします.
/* @jsx h */
import { h } from "preact";
import styled, { StyleSheetManager } from "styled-components";

import Portal from "./Portal.jsx";

const Heading = styled.h3`
  color: #e155f5;
  font-family: sans-serif;
`;

const WidgetContainer = styled.div`
  all: initial;
`;

const App = () => {
  return (
    <StyleSheetManager target={document.querySelector("#app-root").shadowRoot}>
      <WidgetContainer>
        <Heading>Hey, Shadow DOM!</Heading>
        <Portal>
          <Heading>Hey, Shadow Portal!</Heading>
        </Portal>
      </WidgetContainer>
    </StyleSheetManager>
  );
};

export default App;
今、我々はすべてのフルスクリーンコンポーネントをラップすることができますPortal .


結論
最近チームはreleased the widget to GA . 上で概説されたテクニックは、我々がそうである小さいCodeBaseで豊かなアプリケーション経験を構築するのを許しました.ほとんどカプセル化されます.我々はまだ時折z-index 問題やJavaScriptのイベントの競合は、Webサイトビルダーのテーマによって提供される.全体的に、ウィジェットのインストールは成功している.