框架
Life Cycle

Plasmo CSUI 生命周期

Plasmo 的 CSUI 负责在内容脚本中挂载和卸载你的 React、Vue 或 Svelte 组件。尽管每个 UI 库/框架的挂载 API 略有不同,但顶层生命周期大致相同:

  1. 获取一个 Anchor
  2. 创建或定位一个 Root Container
  3. 将组件 渲染 到 Root Container

术语

术语描述
Anchor告诉 CSUI 如何以及在哪里挂载你的组件
Anchor-getter告诉 CSUI 如何找到你的锚点
Overlay在顶级(最大 z-index)覆盖元素上挂载你的组件
Inline将你的组件嵌入到网页的 DOM 中,靠近目标元素
Root ContainerCSUI 创建的 ShadowDOM 元素,用于隔离你的组件
Renderer顶层生命周期运行器(它负责所有操作)

Anchor

CSUI 生命周期

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

Overlay 锚点生成 CSUI Overlay Containers,它们批量挂载到每个 CSUI 的单个 Root Container 元素上。Overlay Containers 相对于每个锚点元素绝对定位,并具有最大的 z-index。然后,你的导出的 CSUI Component 将挂载到每个 Overlay Container 上:

Overlay 锚点挂载

要指定单个 overlay 锚点,导出一个 getOverlayAnchor 函数:

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

要指定多个 overlay 锚点,导出一个 getOverlayAnchorList 函数:

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

getOverlayAnchorList 当前不支持动态情况。例如, 如果在初始渲染后向网页添加新的锚点,CSUI 生命周期将无法检测到它们。欢迎提交 PR 来改进此功能!

更新位置

默认的 Overlay Container 监听窗口滚动事件以对齐锚点元素。你可以通过导出一个 watchOverlayAnchor 函数来自定义 Overlay Container 刷新其绝对位置的方式。下面的例子每 8472ms 刷新一次位置:

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

Inline 锚点将你的 CSUI Component 直接嵌入到网页中。每个锚点生成一个 Root Container,追加到其目标元素旁边。在每个 Root Container 内,创建一个 Inline Container,然后用于挂载导出的 CSUI Component:

CSUI Inline 锚点

要指定单个 inline 锚点,导出一个 getInlineAnchor 函数:

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

要指定带有插入位置的单个 inline 锚点:

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

要指定多个 inline 锚点,导出一个 getInlineAnchorList 函数:

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

要指定带有插入位置的多个 inline 锚点:

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 容器创建

Root Container 是挂载你的 CSUI Component 的地方。内置的 Root Container 是一个带有 plasmo-csui 自定义标签的 ShadowDOM 元素。这允许你为 Root Container 和它们导出的组件安全地设置样式,而不受网页样式的影响。

自定义 DOM 挂载

Root Container 创建一个 shadowHost,它被注入到网页的 DOM 树中。默认情况下,Plasmo 在 inline 锚点之后注入 shadowHost,并在 document.body 之前注入 overlay 锚点。要自定义此行为,导出一个 mountShadowHost 函数:

import type { PlasmoMountShadowHost } from "plasmo"
 
export const mountShadowHost: PlasmoMountShadowHost = ({
  shadowHost,
  anchor,
  mountState
}) => {
  anchor.element.appendChild(shadowHost)
  mountState.observer.disconnect() // 可选演示:根据需要停止观察者
}

Closed Shadow Root

默认情况下,shadow root 是“开放”的,允许任何人(开发者和扩展用户)检查 ShadowDOM 的层次结构。要覆盖此行为,导出一个 createShadowRoot 函数:

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

自定义样式

内置的 ShadowDOM 提供了一种方便的机制,允许扩展开发者通过导出一个 getStyle 函数来返回一个 HTML style 元素 (opens in a new tab) 来安全地设置组件样式。

有关设置 CSUI 样式的进一步指导,请阅读 Styling Plasmo CSUI。

自定义 Root Container

有时,你可能需要完全替换 Plasmo 的 Shadow DOM 容器实现以满足需求。例如,你可能希望利用网页中的某个元素,而不是创建一个新的 DOM 元素。为此,导出一个 getRootContainer 函数:

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

你可能需要这样做的原因包括:

⚠️

如果你导出了一个 getRootContainer 函数,任何扩展内置 ShadowDOM 的函数,如 getStyle 或 getShadowHostId 都将被忽略。 根据需要在自定义 getRootContainer 逻辑中调用这些函数!

请查看 with-content-scripts-ui (opens in a new tab) 示例。

Renderer

Renderer 挂载组件

Renderer 负责观察网站的 DOM 以检测每个 Root Container 的存在,并跟踪每个锚点元素与其 Root Container 之间的链接。一旦确定了稳定的 Root Container,Renderer 将导出的 CSUI Component 挂载到 Root Container 上,具体使用 Inline Container 或 Overlay Container 取决于 Anchor 的类型。

检测和优化 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`

自定义 Renderer

开发人员可以导出一个 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 仍然会被触发。