Plasmo CSUI 生命周期详解
Plasmo 的内容脚本 UI(CSUI)功能精心设计了一套完整的生命周期流程,专门用于在内容脚本中优雅地挂载和卸载 React、Vue 或 Svelte 组件。虽然不同 UI 框架的挂载 API 存在细微差异,但顶层的生命周期流程保持了高度一致性:
核心概念解析
| 术语 | 功能描述 |
|---|---|
| 锚点元素 | 指示 CSUI 如何以及在何处挂载您的组件 |
| 锚点获取器 | 告诉 CSUI 如何找到锚点元素的函数 |
| 覆盖层 | 在顶层(最高 z-index)覆盖元素上挂载组件的模式 |
| 内联挂载 | 将组件直接嵌入到网页 DOM 中,位于目标元素旁边的模式 |
| 根容器 | CSUI 创建的 ShadowDOM 元素,用于隔离和保护您的组件样式 |
| 渲染器 | 顶层的生命周期协调器,负责管理整个挂载流程 |
锚点元素机制

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 组件:

要指定单个内联锚点,导出一个 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")您可能需要这样做的典型场景:
如果您导出了 getRootContainer 函数,任何扩展内置 ShadowDOM 的函数(如 getStyle 或 getShadowHostId)都将被忽略。
请务必在您的自定义 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 仍将继续运行,可能会与您的自定义实现产生冲突。