WXT浏览器插件开发中文教程(17)----内容脚本详解

前言

大家好,我是倔强青铜三 。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。欢迎点赞、收藏、关注,一键三连!!!

内容脚本

要创建内容脚本,请参阅 入口点类型

上下文

内容脚本的 main 函数的第一个参数是其"上下文"。

ts 复制代码
// entrypoints/example.content.ts
export default defineContentScript({
  main(ctx) {},
});

此对象负责跟踪内容脚本的上下文是否"失效"。大多数浏览器默认情况下不会在扩展程序被卸载、更新或禁用时停止内容脚本。当这种情况发生时,内容脚本会开始报告以下错误:

javascript 复制代码
Error: Extension context invalidated.

ctx 对象提供了几个助手方法,用于在上下文失效后停止异步代码的运行:

ts 复制代码
ctx.addEventListener(...);
ctx.setTimeout(...);
ctx.setInterval(...);
ctx.requestAnimationFrame(...);
// 以及更多

您也可以手动检查上下文是否失效:

ts 复制代码
if (ctx.isValid) {
  // 执行某些操作
}
// 或者
if (ctx.isInvalid) {
  // 执行某些操作
}

CSS

在常规的 Web 扩展中,内容脚本的 CSS 通常是单独的 CSS 文件,添加到清单中的 CSS 数组:

json 复制代码
{
  "content_scripts": [
    {
      "css": ["content/style.css"],
      "js": ["content/index.js"],
      "matches": ["*://*/*"]
    }
  ]
}

在 WXT 中,要为内容脚本添加 CSS,只需在 JS 入口文件中导入 CSS 文件,WXT 将自动将打包后的 CSS 输出添加到 css 数组。

ts 复制代码
// entrypoints/example.content/index.ts
import './style.css';
export default defineContentScript({
  // ...
});

要创建一个仅包含 CSS 文件的独立内容脚本:

  1. 创建 CSS 文件:entrypoints/example.content.css

  2. 使用 build:manifestGenerated 钩子将内容脚本添加到清单中:

    wxt.config.ts

    ts 复制代码
    export default defineConfig({
      hooks: {
        'build:manifestGenerated': (wxt, manifest) => {
          manifest.content_scripts ??= [];
          manifest.content_scripts.push({
            // 构建扩展程序一次,查看 CSS 的写入位置
            css: ['content-scripts/example.css'],
            matches: ['*://*/*'],
          });
        },
      },
    });

UI

WXT 提供了 3 种内置工具,用于从内容脚本向页面添加 UI:

  • 集成 - createIntegratedUi
  • [Shadow Root](#Shadow Root "#shadow-root") - createShadowRootUi
  • IFrame - createIframeUi

每种方法都有其自身的优缺点。

方法 隔离样式 隔离事件 HMR 使用页面上下文
集成
Shadow Root ✅ (默认关闭)
IFrame

集成

集成内容脚本 UI 会注入到页面内容的旁边。这意味着它们会受到该页面 CSS 的影响。 以下是将内容脚本生成的UI插入页面中的示例:

原生JavaScript将UI插入页面的方式

ts 复制代码
// entrypoints/example-ui.content.ts
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 = '...';
        container.append(app);
      },
    });
    // 调用 mount 将 UI 添加到 DOM
    ui.mount();
  },
});

Vue将UI插入页面的方式

ts 复制代码
// entrypoints/example-ui.content/index.ts
import { createApp } from 'vue';
import App from './App.vue';
export default defineContentScript({
  matches: ['<all_urls>'],
  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 创建应用并将其挂载到 UI 容器
        const app = createApp(App);
        app.mount(container);
        return app;
      },
      onRemove: (app) => {
        // 在 UI 移除时卸载应用
        app.unmount();
      },
    });
    // 调用 mount 将 UI 添加到 DOM
    ui.mount();
  },
});

React将UI插入页面的方式

tsx 复制代码
// entrypoints/example-ui.content/index.tsx
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
export default defineContentScript({
  matches: ['<all_urls>'],
  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 在 UI 容器上创建根并渲染组件
        const root = ReactDOM.createRoot(container);
        root.render(<App />);
        return root;
      },
      onRemove: (root) => {
        // 在 UI 移除时卸载根
        root.unmount();
      },
    });
    // 调用 mount 将 UI 添加到 DOM
    ui.mount();
  },
});

Svelte将UI插入页面的方式

ts 复制代码
// entrypoints/example-ui.content/index.ts
import App from './App.svelte';
import { mount, unmount } from 'svelte';
export default defineContentScript({
  matches: ['<all_urls>'],
  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 在 UI 容器中创建 Svelte 应用
        mount(App, {
          target: container,
        });
      },
      onRemove: (app) => {
        // 在 UI 移除时销毁应用
        unmount(app);
      },
    });
    // 调用 mount 将 UI 添加到 DOM
    ui.mount();
  },
});

Solid将UI插入页面的方式

tsx 复制代码
// entrypoints/example-ui.content/index.ts
import { render } from 'solid-js/web';
export default defineContentScript({
  matches: ['<all_urls>'],
  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 将应用渲染到 UI 容器
        const unmount = render(() => <div>...</div>, container);
        return unmount;
      },
      onRemove: (unmount) => {
        // 在 UI 移除时卸载应用
        unmount();
      },
    });
    // 调用 mount 将 UI 添加到 DOM
    ui.mount();
  },
});

有关完整选项列表,请参阅 API 参考

Shadow Root [​](#Shadow Root "#shadow-root")

在 Web 扩展中,通常不希望内容脚本的 CSS 影响页面,反之亦然。ShadowRoot API 非常适合这种情况。

WXT 的 createShadowRootUi 抽象化了所有 ShadowRoot 的设置,便于创建样式与页面隔离的 UI。它还支持可选的 isolateEvents 参数,以进一步隔离用户交互。

要使用 createShadowRootUi,请按照以下步骤操作:

  1. 在内容脚本顶部导入 CSS 文件
  2. defineContentScript 中设置 cssInjectionMode: "ui"
  3. 使用 createShadowRootUi() 定义您的 UI
  4. 挂载 UI 使其对用户可见

原生JavaScript导入CSS方式

ts 复制代码
// 1. 导入样式
import './style.css';
export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. 设置 cssInjectionMode
  cssInjectionMode: 'ui',
  async main(ctx) {
    // 3. 定义您的 UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount(container) {
        // 定义如何在容器内挂载您的 UI
        const app = document.createElement('p');
        app.textContent = 'Hello world!';
        container.append(app);
      },
    });
    // 4. 挂载 UI
    ui.mount();
  },
});

Vue导入CSS方式

ts 复制代码
// 1. 导入样式
import './style.css';
import { createApp } from 'vue';
import App from './App.vue';
export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. 设置 cssInjectionMode
  cssInjectionMode: 'ui',
  async main(ctx) {
    // 3. 定义您的 UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 定义如何在容器内挂载您的 UI
        const app = createApp(App);
        app.mount(container);
        return app;
      },
      onRemove: (app) => {
        // 在 UI 移除时卸载应用
        app?.unmount();
      },
    });
    // 4. 挂载 UI
    ui.mount();
  },
});

React导入CSS方式

tsx 复制代码
// 1. 导入样式
import './style.css';
import ReactDOM from 'react-dom/client';
import App from './App.tsx';
export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. 设置 cssInjectionMode
  cssInjectionMode: 'ui',
  async main(ctx) {
    // 3. 定义您的 UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 容器是 body,React 在 body 上创建根时会发出警告,因此创建一个包装 div
        const app = document.createElement('div');
        container.append(app);
        // 在 UI 容器上创建根并渲染组件
        const root = ReactDOM.createRoot(app);
        root.render(<App />);
        return root;
      },
      onRemove: (root) => {
        // 在 UI 移除时卸载根
        root?.unmount();
      },
    });
    // 4. 挂载 UI
    ui.mount();
  },
});

Svelte导入CSS方式

ts 复制代码
// 1. 导入样式
import './style.css';
import App from './App.svelte';
import { mount, unmount } from 'svelte';
export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. 设置 cssInjectionMode
  cssInjectionMode: 'ui',
  async main(ctx) {
    // 3. 定义您的 UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 在 UI 容器中创建 Svelte 应用
        mount(App, {
          target: container,
        });
      },
      onRemove: () => {
        // 在 UI 移除时销毁应用
        unmount(app);
      },
    });
    // 4. 挂载 UI
    ui.mount();
  },
});

Solid导入CSS方式

tsx 复制代码
// 1. 导入样式
import './style.css';
import { render } from 'solid-js/web';
export default defineContentScript({
  matches: ['<all_urls>'],
  // 2. 设置 cssInjectionMode
  cssInjectionMode: 'ui',
  async main(ctx) {
    // 3. 定义您的 UI
    const ui = await createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 将应用渲染到 UI 容器
        const unmount = render(() => <div>...</div>, container);
      },
      onRemove: (unmount) => {
        // 在 UI 移除时卸载应用
        unmount?.();
      },
    });
    // 4. 挂载 UI
    ui.mount();
  },
});

有关完整选项列表,请参阅 API 参考

完整示例:

IFrame

如果您不需要在与内容脚本相同的框架中运行您的 UI,则可以使用 IFrame 来托管您的 UI。由于 IFrame 只托管一个 HTML 页面,支持 HMR

WXT 提供了一个辅助函数,createIframeUi,简化了 IFrame 的设置。

  1. 创建一个将被加载到您的 IFrame 中的 HTML 页面:

    html 复制代码
    <!-- entrypoints/example-iframe.html -->
    <!doctype html>
    <html lang="en">
      <head>
        <meta charset="UTF-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Your Content Script IFrame</title>
      </head>
      <body>
        <!-- ... -->
      </body>
    </html>
  2. 将页面添加到清单的 web_accessible_resources 中:

    wxt.config.ts

    ts 复制代码
    export default defineConfig({
      manifest: {
        web_accessible_resources: [
          {
            resources: ['example-iframe.html'],
            matches: [...],
          },
        ],
      },
    });
  3. 创建并挂载 IFrame:

    ts 复制代码
    export default defineContentScript({
      matches: ['<all_urls>'],
      main(ctx) {
        // 定义 UI
        const ui = createIframeUi(ctx, {
          page: '/example-iframe.html',
          position: 'inline',
          anchor: 'body',
          onMount: (wrapper, iframe) => {
            // 为 iframe 添加样式,如宽度
            iframe.width = '123';
          },
        });
        // 向用户显示 UI
        ui.mount();
      },
    });

有关完整选项列表,请参阅 API 参考

隔离世界与主世界

默认情况下,所有内容脚本都在一个隔离的上下文中运行,其中只有 DOM 与它运行的网页共享------一个"隔离世界"。在 MV3 中,Chromium 引入了在"主"世界中运行内容脚本的能力------在这里,不仅仅是 DOM,所有内容都对内容脚本可用,就像脚本是由网页加载的一样。

您可以通过设置 world 选项为内容脚本启用此功能:

ts 复制代码
export default defineContentScript({
  world: 'MAIN',
});

然而,这种方法有几个显著的缺点:

  • 不支持 MV2
  • world: "MAIN" 仅由 Chromium 浏览器支持
  • 主世界内容脚本无法访问扩展程序 API

相反,WXT 建议使用 injectScript 函数手动将脚本注入到主世界中。这将解决前面提到的缺点。

  • injectScript 支持 MV2 和 MV3
  • injectScript 支持所有浏览器
  • 有一个"父"内容脚本意味着您可以发送消息来回通信,从而可以访问扩展程序 API

要使用 injectScript,我们需要两个入口点,一个内容脚本和一个未列出的脚本:

html 复制代码
📂 entrypoints/
   📄 example.content.ts
   📄 example-main-world.ts
ts 复制代码
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
  console.log('Hello from the main world');
});
ts 复制代码
// entrypoints/example.content.ts
export default defineContentScript({
  matches: ['*://*/*'],
  async main() {
    console.log('Injecting script...');
    await injectScript('/example-main-world.js', {
      keepInDom: true,
    });
    console.log('Done!');
  },
});
JavaScript 复制代码
export default defineConfig({
  manifest: {
    // ...
    web_accessible_resources: [
      {
        resources: ["example-main-world.js"],
        matches: ["*://*/*"],
      }
    ]
  }
});

injectScript 通过在页面上创建一个指向您的脚本的 script 元素来工作。这会将脚本加载到页面上下文中,从而使其在主世界中运行。

injectScript 返回一个承诺,当它被解析时,意味着脚本已被浏览器评估,您可以开始与它通信。

警告:run_at 注意事项

对于 MV3,injectScript 是同步的,注入的脚本将与内容脚本的 run_at 同时评估。

然而对于 MV2,injectScript 必须 fetch 脚本的文本内容并创建一个内联 <script> 块。这意味着对于 MV2,您的脚本是异步注入的,它不会与内容脚本的 run_at 同时评估。

将 UI 挂载到动态元素 [​](#将 UI 挂载到动态元素)

在许多情况下,您可能需要将 UI 挂载到在网页最初加载时不存在的 DOM 元素上。为此,请使用 autoMount API 自动在目标元素动态出现时挂载 UI,并在元素消失时卸载它。在 WXT 中,anchor 选项用于定位元素,根据其出现和移除自动进行挂载和卸载。

ts 复制代码
export default defineContentScript({
  matches: ['<all_urls>'],
  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      // 它观察锚点
      anchor: '#your-target-dynamic-element',
      onMount: (container) => {
        // 将子元素附加到容器
        const app = document.createElement('p');
        app.textContent = '...';
        container.append(app);
      },
    });
    // 调用 autoMount 观察锚点元素的添加/移除。
    ui.autoMount();
  },
});

提示

当调用 ui.remove 时,autoMount 也会停止。

有关完整选项列表,请参阅 API 参考

处理单页应用

为单页应用 (SPA) 和使用 HTML5 历史模式进行导航的网站编写内容脚本较为困难,因为内容脚本仅在全页重新加载时运行。SPA 和利用 HTML5 历史模式的网站在更改路径时不会进行全页重新加载,因此您的内容脚本不会在您期望的时候运行。

让我们来看一个示例。假设您希望在 YouTube 上观看视频时添加一个 UI:

ts 复制代码
export default defineContentScript({
  matches: ['*://*.youtube.com/watch*'],
  main(ctx) {
    console.log('YouTube content script loaded');
    mountUi(ctx);
  },
});
function mountUi(ctx: ContentScriptContext): void {
  // ...
}

您只会看到"YouTube content script loaded"在重新加载观看页面或直接从其他网站导航到该页面时。

要解决这个问题,您需要手动监听路径的变化,并在 URL 匹配您期望的值时运行您的内容脚本。

ts 复制代码
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)) mainWatch(ctx);
    });
  },
});
function mainWatch(ctx: ContentScriptContext) {
  mountUi(ctx);
}

最后感谢阅读!欢迎关注我,微信公众号倔强青铜三。欢迎点赞收藏关注,一键三连!!!

相关推荐
祯民4 分钟前
《生成式 AI 应用开发:基于 OpenAI API 开发》实体书上架
前端·aigc·openai
工业互联网专业5 分钟前
基于springboot+vue的校园数字化图书馆系统
java·vue.js·spring boot·毕业设计·源码·课程设计·校园数字图书馆系统
bigyoung9 分钟前
ts在运行时校验数据类型的探索
前端·javascript·typescript
独立开阀者_FwtCoder13 分钟前
深入解密Node共享内存:这个原生模块让你的多进程应用性能翻倍
前端·javascript·后端
Json_14 分钟前
使用JS写一个用鼠标拖动DIV到任意地方
前端·javascript·深度学习
祯民19 分钟前
阿民解锁了"入职 30 天跑路"新成就
前端·面试
昌平第一王昭君20 分钟前
一个简单的虚拟滚动
前端
Json_22 分钟前
jQuery选项卡小练习
前端·深度学习·jquery
王sir万岁26 分钟前
普通前端工程师如何入门 Web3 开发?
前端
Json_28 分钟前
2017-06-05 20:33:39发布第一篇博客 坚持写博客时间统计代码(某个时间到当前时间的统计)
前端·深度学习