Framework
Life Cycle

Plasmo CSUIのライフサイクル

PlasmoのCSUI (Content Script User Interface)は、コンテンツスクリプト内でReact、Vue、またはSvelteコンポーネントのマウントとアンマウントを行うライフサイクルをオーケストレートします。UIライブラリ/フレームワークごとにマウントAPIがわずかに異なりますが、最上位のライフサイクルはほぼ同じです。

  1. Anchorを取得する
  2. Root Containerを作成するか、見つける
  3. Rendererを使用してRoot Containerにコンポーネントをレンダリングする

用語

用語説明
AnchorCSUIにコンポーネントのマウント方法と場所を指示します。
Anchor-getterCSUIにアンカーの探し方を指示します。
Overlay最上位(最大のz-index)のオーバーレイ要素にコンポーネントをマウントします。
Inlineターゲット要素の隣に、ウェブページのDOMにコンポーネントをマウントします。
Root ContainerCSUIによって作成された、コンポーネントを分離するためのShadowDOM要素です。
Renderer最上位のライフサイクルランナー(すべてを実行します)

Anchor

CSUI Life Cycle

Plasmo CSUIアンカーは、次の型で定義されます。

export type PlasmoCSUIAnchor = {
  type: "overlay" | "inline"
  element: Element
}

デフォルトでは、CSUIライフサイクルはdocument.body要素を使用してオーバーレイアンカーを作成します。

{
  type: "overlay",
  element: document.body
}

アンカーゲッター関数が定義されてエクスポートされている場合、CSUIライフサイクルは代わりに返された要素と関連するアンカータイプを使用します。アンカーゲッター関数は非同期にすることができるため、Plasmoがコンポーネントをマウントするタイミングを制御することもできます。たとえば、特定の要素がページに表示されるのを待ってからコンポーネントをマウントできます。

アンカーは、アンカープロップを介してCSUIに渡されます。次のようにアクセスできます。

import type { PlasmoCSUIProps } from "plasmo"
 
const AnchorTypePrinter: FC<PlasmoCSUIProps> = ({ anchor }) => {
  return <span>{anchor.type}</span>
}
 
export default AnchorTypePrinter

Overlay

オーバーレイアンカーは、CSUIごとに単一のRoot Container要素に一括でマウントされるCSUI Overlay Containerを生成します。Overlay Containerは、各アンカーの要素に対して絶対的に配置され、z-indexが最大化されます。次に、エクスポートされたCSUI Componentが各Overlay Containerにマウントされます。

Overlay Anchor Mounting

単一のオーバーレイアンカーを指定するには、getOverlayAnchor関数をエクスポートします。

import type { PlasmoGetOverlayAnchor } from "plasmo"
 
export const getOverlayAnchor: PlasmoGetOverlayAnchor = async () =>
  document.querySelector("#pricing")

オーバーレイアンカーのリストを指定するには、getOverlayAnchorList関数をエクスポートします。

import type { PlasmoGetOverlayAnchorList } from "plasmo"
 
export const getOverlayAnchorList: PlasmoGetOverlayAnchorList = async () =>
  document.querySelectorAll("a")
⚠️

getOverlayAnchorListは現時点では動的なケースに対応していません。たとえば、最初のレンダリング後に新しいアンカーがウェブページに追加された場合、CSUIライフサイクルはそれを検出できません。この機能の改善のためのPRは大歓迎です!

位置の更新

デフォルトのOverlay Containerは、ウィンドウのスクロールイベントをリッスンして、アンカー要素と位置を合わせます。watchOverlayAnchor関数をエクスポートすることで、Overlay Containerが絶対位置を更新する方法をカスタマイズできます。次の例では、8472ミリ秒ごとに位置を更新します。

import type { PlasmoWatchOverlayAnchor } from "plasmo"
 
export const watchOverlayAnchor: PlasmoWatchOverlayAnchor = (
  updatePosition
) => {
  const interval = setInterval(() => {
    updatePosition()
  }, 8472)
 
  // アンマウント時に間隔をクリアする
  return () => {
    clearInterval(interval)
  }
}

例については、with-content-scripts-ui/contents/plasmo-overlay-watch.tsx (opens in a new tab)を参照してください。

Inline

インラインアンカーは、CSUI Componentをウェブページに直接埋め込みます。各アンカーは、ターゲット要素の隣に付加されたRoot Containerを生成します。各Root Container内で、エクスポートされたCSUI Componentをマウントするために使用されるInline Containerが作成されます。

CSUI Inline Anchor

単一のインラインアンカーを指定するには、getInlineAnchor関数をエクスポートします。

import type { PlasmoGetInlineAnchor } from "plasmo"
 
export const getInlineAnchor: PlasmoGetInlineAnchor = async () =>
  document.querySelector("#pricing")

挿入位置を指定した単一のインラインアンカーを指定するには:

import type { PlasmoGetInlineAnchor } from "plasmo"
 
export const getInlineAnchor: PlasmoGetInlineAnchor = async () => ({
      element: document.querySelector("#pricing"),
      insertPosition: "afterend"
})

インラインアンカーのリストを指定するには、getInlineAnchorList関数をエクスポートします。

import type { PlasmoGetInlineAnchorList } from "plasmo"
 
export const getInlineAnchorList: PlasmoGetInlineAnchorList = async () =>
  document.querySelectorAll("a")

挿入位置を指定したインラインアンカーのリストを指定するには:

import type { PlasmoGetInlineAnchorList } from "plasmo"
 
export const getInlineAnchorList: PlasmoGetInlineAnchorList = async () => {
  const anchors = document.querySelectorAll("a")
  return Array.from(anchors).map((element) => ({
    element,
    insertPosition: "afterend"
  }))
}

例については、with-content-scripts-ui/contents/plasmo-inline.tsx (opens in a new tab)を参照してください。

Root Container

Root container creation

Root Containerは、CSUI Componentがマウントされる場所です。組み込みのRoot Containerは、plasmo-csuiカスタムタグを持つShadowDOM要素です。これにより、ウェブページのスタイルの影響を受けることなく、Root Containerとそのエクスポートされたコンポーネントをスタイル設定できます。

カスタムDOMマウント

Root Containerは、ウェブページのDOMツリーに挿入されるshadowHostを作成します。デフォルトでは、Plasmoはインラインアンカーの要素の後にshadowHostを挿入し、オーバーレイアンカーの場合はdocument.bodyの前に挿入します。この動作をカスタマイズするには、mountShadowHost関数をエクスポートします。

import type { PlasmoMountShadowHost } from "plasmo"
 
export const mountShadowHost: PlasmoMountShadowHost = ({
  shadowHost,
  anchor,
  mountState
}) => {
  anchor.element.appendChild(shadowHost)
  mountState.observer.disconnect() // OPTIONAL DEMO: 必要に応じてオブザーバーを停止する
}

クローズドシャドウルート

デフォルトでは、シャドウルートは「オープン」になっており、誰でも(開発者と拡張機能のユーザー)がShadowDOMの階層を検査できます。この動作をオーバーライドするには、createShadowRoot関数をエクスポートします。

import type { PlasmoCreateShadowRoot } from "plasmo"
 
export const createShadowRoot: PlasmoCreateShadowRoot = (shadowHost) =>
  shadowHost.attachShadow({ mode: "closed" })

カスタムスタイル

組み込みのShadowDOMは、HTML style要素 (opens in a new tab)を返すgetStyle関数をエクスポートすることで、拡張機能開発者がコンポーネントを安全にスタイル設定するための便利なメカニズムを提供します。

CSUIのスタイル設定に関する詳細なガイダンスについては、Plasmo CSUIのスタイル設定をお読みください。

カスタムルートコンテナ

ニーズに合わせてPlasmoのShadow DOMコンテナの実装を完全に置き換えたい場合があります。たとえば、新しいDOM要素を作成する代わりに、ウェブページ内の要素自体を利用したい場合があります。そのためには、getRootContainer関数をエクスポートします。

import type { PlasmoGetRootContainer } from "plasmo"
 
export const getRootContainer = () => document.getElementById("itero")

これを行う理由のいくつかを次に示します。

⚠️

getRootContainer関数をエクスポートする場合、getStylegetShadowHostIdなど、組み込みのShadowDOMを拡張する関数はすべて無視されます。必要に応じて、カスタムgetRootContainerロジック内でこれらの関数を呼び出してください!

例については、with-content-scripts-ui (opens in a new tab)を参照してください。

Renderer

Renderer mounting Component

Rendererは、各Root Containerの存在を検出し、各アンカーの要素とそのRoot Container間のリンクを追跡するために、ウェブサイトのDOMを観察する役割を担っています。安定したRoot Containerが決定されると、RendererAnchorのタイプに応じてInline ContainerまたはOverlay Containerを使用して、エクスポートされたCSUI ComponentRoot Containerにマウントします。

Root Containerの削除の検出と最適化

ウェブページがDOM構造を変更すると、Root Containerが削除される場合があります。たとえば、受信トレイアイテムでいっぱいのメールクライアントと、各アイテムの横にインラインで挿入されたCSUIがあるとします。アイテムが削除されると、ルートコンテナも削除されます。

Root Containerの削除を検出するために、CSUI Rendererは、マウントされた各コンテナのルートとwindow.documentオブジェクトを比較します。このチェックは、getShadowHostId関数をエクスポートすることでO(1)に最適化できます。

import type { PlasmoGetShadowHostId } from "plasmo"
 
export const getShadowHostId: PlasmoGetShadowHostId = () => `adonais`

この関数は、検出された各アンカーのIDをカスタマイズすることもできます。

import type { PlasmoGetShadowHostId } from "plasmo"
 
export const getShadowHostId: PlasmoGetShadowHostId = ({ element }) =>
  element.getAttribute("data-custom-id") + `-pollax-iv`

カスタムレンダラー

開発者は、デフォルトのレンダラーをオーバーライドするためにrender関数をエクスポートできます。この機能が必要になる場合があります。

  • カスタムInline containerまたはOverlay containerを提供する
  • マウントロジックをカスタマイズする
  • カスタムMutationObserverを提供する

たとえば、既存の要素をカスタムコンテナとして使用するには、次のようにします。

import type { PlasmoRender } from "plasmo"
 
import { CustomContainer } from "~components/custom-container"
 
const EngageOverlay = () => <span>ENGAGE</span>
 
// この関数は、デフォルトの`createRootContainer`をオーバーライドします
export const getRootContainer = () =>
  new Promise((resolve) => {
    const checkInterval = setInterval(() => {
      const rootContainer = document.getElementById("itero")
      if (rootContainer) {
        clearInterval(checkInterval)
        resolve(rootContainer)
      }
    }, 137)
  })
 
export const render: PlasmoRender = async ({
  anchor, // 観察されたアンカー、またはdocument.body。
  createRootContainer // これはデフォルトのルートコンテナを作成します
}) => {
  const rootContainer = await createRootContainer()
 
  const root = createRoot(rootContainer) // 任意のルート
  root.render(
    <CustomContainer>
      <EngageOverlay />
    </CustomContainer>
  )
}

カスタムコンテナを動的に作成する方法:

import type { PlasmoRender } from "plasmo"
 
import { CustomContainer } from "~components/custom-container"
 
const EngageOverlay = () => <span>ENGAGE</span>
 
// この関数は、デフォルトの`createRootContainer`をオーバーライドします
export const getRootContainer = ({ anchor, mountState }) =>
  new Promise((resolve) => {
    const checkInterval = setInterval(() => {
      let { element, insertPosition } = anchor
      if (element) {
        const rootContainer = document.createElement("div")
        mountState.hostSet.add(rootContainer)
        mountState.hostMap.set(rootContainer, anchor)
        element.insertAdjacentElement(insertPosition, rootContainer)
        clearInterval(checkInterval)
        resolve(rootContainer)
      }
    }, 137)
  })
 
export const render: PlasmoRender = async ({
  anchor, // 観察されたアンカー、またはdocument.body。
  createRootContainer // これはデフォルトのルートコンテナを作成します
}) => {
  const rootContainer = await createRootContainer(anchor)
 
  const root = createRoot(rootContainer) // 任意のルート
  root.render(
    <CustomContainer>
      <EngageOverlay />
    </CustomContainer>
  )
}

組み込みのInline ContainerまたはOverlay Containerを利用するには、次のようにします。

import type { PlasmoRender } from "plasmo"
 
const AnchorOverlay = ({ anchor }) => <span>{anchor.innerText}</span>
 
export const render: PlasmoRender = async (
  {
    anchor, // 観察されたアンカー、またはdocument.body。
    createRootContainer // これはデフォルトのルートコンテナを作成します
  },
  _,
  OverlayCSUIContainer
) => {
  const rootContainer = await createRootContainer()
 
  const root = createRoot(rootContainer) // 任意のルート
  root.render(
    // デフォルトのコンテナをマウントするには、アンカーを渡す必要があります。ここではデフォルトのアンカーを渡します
    <OverlayCSUIContainer anchor={anchor}>
      <AnchorOverlay anchor={anchor} />
    </OverlayCSUIContainer>
  )
}

MutationObserverをカスタマイズする必要がある場合は、アンカーゲッター関数をエクスポートしないでください。それ以外の場合は、組み込みのMutationObserverが引き続き生成されます。