Skip to Content
LaunchExt | Chrome 扩展开发平台 (Next.js + Plasmo) 🚀 Read more → 
文档Framework内容脚本UIPlasmo CSUI 生命周期详解

Plasmo CSUI 生命周期详解

Plasmo 的内容脚本 UI(CSUI)功能精心设计了一套完整的生命周期流程,专门用于在内容脚本中优雅地挂载和卸载 React、Vue 或 Svelte 组件。虽然不同 UI 框架的挂载 API 存在细微差异,但顶层的生命周期流程保持了高度一致性:

  1. 获取一个 锚点元素
  2. 创建或定位一个 根容器
  3. 将组件渲染根容器

核心概念解析

术语功能描述
锚点元素指示 CSUI 如何以及在何处挂载您的组件
锚点获取器告诉 CSUI 如何找到锚点元素的函数
覆盖层在顶层(最高 z-index)覆盖元素上挂载组件的模式
内联挂载将组件直接嵌入到网页 DOM 中,位于目标元素旁边的模式
根容器CSUI 创建的 ShadowDOM 元素,用于隔离和保护您的组件样式
渲染器顶层的生命周期协调器,负责管理整个挂载流程

锚点元素机制

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

覆盖层挂载模式

覆盖层锚点会生成 CSUI 覆盖层容器,这些容器会批量挂载到每个 CSUI 的单个 根容器 元素上。覆盖层容器 相对于每个锚点元素进行绝对定位,并具有最高的 z-index 值。随后,您导出的 CSUI 组件 会挂载到每个 覆盖层容器 中:

覆盖层锚点挂载示意图

要指定单个覆盖层锚点,导出一个 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 来改进此 功能!

位置更新机制

默认的 覆盖层容器 会监听窗口滚动事件以保持与锚点元素的精准对齐。您可以通过导出一个 watchOverlayAnchor 函数来自定义 覆盖层容器 刷新其绝对定位的方式。以下示例展示了每 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 

内联挂载模式

内联锚点将您的 CSUI 组件 直接嵌入到网页 DOM 结构中。每个锚点会生成一个 根容器,附加到其目标元素旁边。在每个 根容器 内,会创建一个 内联容器,然后用于挂载导出的 CSUI 组件

CSUI 内联锚点示意图

要指定单个内联锚点,导出一个 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 

根容器机制

根容器创建流程

根容器 是您的 CSUI 组件 挂载的核心位置。内置的 根容器 是一个带有 plasmo-csui 自定义标签的 ShadowDOM 元素。这种设计让您可以自由地样式化 根容器 及其导出的组件,完全不受网页样式的影响。

自定义 DOM 挂载

根容器 会创建一个 shadowHost,它会注入到网页的 DOM 树中。默认情况下,Plasmo 会将 shadowHost 注入到内联锚点元素之后,以及覆盖层锚点的 document.body 之前。要自定义此行为,导出一个 mountShadowHost 函数:

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

关闭式 Shadow Root

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

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

自定义样式方案

内置的 ShadowDOM 提供了一个便捷的机制,允许扩展开发者通过导出一个返回 HTML 样式元素 getStyle 函数来安全地样式化他们的组件。

有关 CSUI 样式化的详细指导,请阅读 样式化 Plasmo CSUI 文档。

自定义根容器实现

在某些特定场景下,您可能需要完全替换 Plasmo 的 Shadow DOM 容器实现。例如,您可能希望利用网页本身的现有元素,而不是创建新的 DOM 元素。为此,导出一个 getRootContainer 函数:

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

您可能需要这样做的典型场景:

  • 扩展需要继承宿主网页的样式 
  • 扩展需要将组件直接挂载到网页 DOM 中,而不是使用 shadow DOM
  • 扩展需要使用 iframe  来实现更复杂的隔离需求
⚠️

如果您导出了 getRootContainer 函数,任何扩展内置 ShadowDOM 的函数(如 getStylegetShadowHostId)都将被忽略。 请务必在您的自定义 getRootContainer 逻辑中根据需要调用这些函数!

具体实现示例请参考 with-content-scripts-ui 

渲染器机制

渲染器挂载组件流程

渲染器 是整个生命周期的核心协调器,负责观察网站的 DOM 结构以检测每个 根容器 的存在,并跟踪每个锚点元素与其 根容器 之间的关联关系。一旦确定了稳定的 根容器渲染器 就会使用 内联容器覆盖层容器 将导出的 CSUI 组件 挂载到 根容器 中,具体模式取决于 锚点 的类型。

检测和优化根容器移除

当网页动态更改其 DOM 结构时,根容器 可能会被意外移除。例如,在一个充满收件箱项目的电子邮件客户端中,如果每个项目旁边都内联注入了 CSUI,当用户删除某个项目时,对应的根容器也会被一同移除。

要检测 根容器 移除,CSUI 渲染器 会将每个已挂载容器的根与 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 函数来完全覆盖默认的渲染器逻辑。您可能需要此功能来实现:

  • 提供自定义的 内联容器覆盖层容器 实现
  • 定制化的挂载逻辑需求
  • 特殊的 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> ) }

要使用内置的 内联容器覆盖层容器

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 仍将继续运行,可能会与您的自定义实现产生冲突。

最后更新于