首页 / WXT 浏览器扩展框架教程 / 往网页里长 UI:内容脚本界面注入

WXT 浏览器扩展框架教程

往网页里长 UI:内容脚本界面注入

本教程共 45 篇 · 第 15 篇 · 更新于 2026-08-13 · 约 3 分钟阅读

WXT内容脚本UI注入Shadow DOMiframeSPA

本节目标:学会往网页里注入界面。三种方式(集成式、Shadow DOM、iframe)各有什么优缺点,CSS 怎么隔离,页面元素是动态出现的怎么办,以及单页应用(SPA)下内容脚本不重跑怎么处理。

内容脚本不只是改改文字,很多时候我们要在页面上「长」出自己的界面:评分徽章、翻译浮层、侧边助手。WXT 提供了三个现成的工具函数。

先解决 CSS

普通扩展里,内容脚本的 CSS 要单独写进 manifest 的 css 数组。WXT 简化了这一步:在入口文件里 import './style.css',构建时 WXT 会自动把打包后的 CSS 加进 manifest 的 css 数组

如果你想要一个纯 CSS 的内容脚本(不注入 JS),可以建 entrypoints/example.content.css,再用 build:manifestGenerated 钩子把它手动加进 content_scripts,如 §30 所述。

三种 UI 注入方式

WXT 提供三个工具函数,选哪个取决于你要不要隔离:

方式样式隔离事件隔离HMR能用页面上下文
createIntegratedUi
createShadowRootUi✅(默认关)
createIframeUi

集成式:createIntegratedUi

界面直接插进页面 DOM,会被页面 CSS 影响,也会影响页面。适合样式简单、不追求隔离的小组件:

export default defineContentScript({
  matches: ["<all_urls>"],
  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: "inline",
      anchor: "body",
      onMount: (container) => {
        const app = document.createElement("p");
        app.textContent = "Hello!";
        container.append(app);
      },
    });
    ui.mount();
  },
});

position 还有 overlaymodal 等取值,完整列表见官方 API 参考。onMount 的返回值会传给 onRemove 做卸载,接 React/Vue 时很有用。

Shadow DOM:createShadowRootUi

Shadow DOM 是天然的样式隔离罩:组件内部样式和页面样式互不干扰。步骤比集成式多两步:

  1. 顶部 import './style.css'
  2. defineContentScript 里设 cssInjectionMode: "ui",让 CSS 只注入到 UI 的 shadow root 里,而不是整页
  3. createShadowRootUi 定义界面(需要 name
  4. ui.mount() 挂载
import "./style.css";

export default defineContentScript({
  matches: ["<all_urls>"],
  cssInjectionMode: "ui",
  async main(ctx) {
    const ui = await createShadowRootUi(ctx, {
      name: "example-ui",
      position: "inline",
      anchor: "body",
      onMount: (container) => {
        const app = document.createElement("p");
        app.textContent = "Hello world!";
        container.append(app);
      },
    });
    ui.mount();
  },
});

WXT 会通过 all: initial 重置继承样式,但**rem 单位没法完全隔离**:<html> 的字号由页面决定,用了 Tailwind 这类 rem 体系时,你的界面在不同网站会忽大忽小。这是官方 FAQ 里的已知问题。

Note

mkext 项目的域名评级徽章用了原生 attachShadow({ mode: "closed" }) 手动建封闭 Shadow DOM:既挡住 Google 的样式污染徽章,也让页面的 JavaScript 碰不到徽章内部。

iframe:createIframeUi

iframe 是三重隔离的终极方案,而且支持 HMR——开发时改界面不用整页刷新。代价是它只显示一个 HTML 页面,无法直接操作宿主页面上下文。

<!-- entrypoints/example-iframe.html -->
<!doctype html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <title>Content Script IFrame</title>
  </head>
  <body></body>
</html>

页面要能被网页加载,得加进 web_accessible_resources

export default defineConfig({
  manifest: {
    web_accessible_resources: [
      { resources: ["example-iframe.html"], matches: ["<all_urls>"] },
    ],
  },
});
export default defineContentScript({
  matches: ["<all_urls>"],
  main(ctx) {
    const ui = createIframeUi(ctx, {
      page: "/example-iframe.html",
      position: "inline",
      anchor: "body",
      onMount: (_wrapper, iframe) => {
        iframe.width = "300";
      },
    });
    ui.mount();
  },
});

挂到动态出现的元素上

很多页面的目标元素是异步渲染的,脚本注入时它还不存在。把 anchor 写成选择器字符串,再调用 ui.autoMount(),WXT 就会用 MutationObserver 盯着它:出现时自动挂载,消失时自动卸载:

const ui = createIntegratedUi(ctx, {
  position: "inline",
  anchor: "#your-target-dynamic-element", // 选择器 => 自动观察
  onMount: (container) => { /* ... */ },
});

ui.autoMount(); // 而不是 ui.mount()
Tip

调用了 ui.remove() 之后,autoMount 的观察也会一起停止。

对付 SPA

单页应用用 history 模式切换路由时不会整页刷新,而内容脚本只在整页加载时注入一次。你在 YouTube 的视频页刷新能看到脚本,从首页点进去就看不到了。

解法是监听 WXT 提供的 wxt:locationchange 事件,URL 变化时自己判断要不要干活:

const watchPattern = new MatchPattern("*://*.youtube.com/watch*");

export default defineContentScript({
  matches: ["*://*.youtube.com/*"],
  main(ctx) {
    ctx.addEventListener(window, "wxt:locationchange", ({ newUrl }) => {
      if (watchPattern.includes(newUrl)) mountUi(ctx);
    });
  },
});

隔离世界与主世界

默认内容脚本跑在「隔离世界」(Isolated World)里,和页面共享 DOM 但不共享 JavaScript 环境。把 world: "MAIN" 可以进入主世界,但代价不小:仅 MV3 支持(Chrome 111 起、Firefox 128 起)、拿不到扩展 API。

WXT 更推荐 injectScript 手动注入主世界:它兼容所有浏览器,配合一个「父内容脚本」还能来回发消息、间接使用扩展 API。具体做法需要 unlisted 脚本 + web_accessible_resources,在 §13 讲过,这里不再展开。

小结

  • 三个 UI 工具按需选:要页面上下文选集成式,要样式隔离选 Shadow DOM,要 HMR 选 iframe。
  • 动态元素用 anchor 选择器 + autoMount
  • SPA 用 wxt:locationchange 事件补上「路由变了」的触发时机。
  • 主世界需求优先考虑 injectScript,而不是 world: "MAIN"