Skip to content

Content Scripts

Outline

深度内容脚本

CSS

在常规网页扩展中,内容脚本的CSS通常是一个单独的CSS文件,添加到 manifest 中:

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

在WXT中,为了将CSS注入内容脚本,请导入CSS文件到JS入口,并通过wxt自动添加编译后的CSS输出:

typescript
// entrypoints/example.content/index.ts
import './style.css';

export default defineContentScript({
  // ...
});

要为内容脚本添加CSS,请检查 manifest 中的 css 数组:

json
{
  "content_scripts": [
    {
      "css": ["content/style.css"],
      "matches": ["*://*/*"]
    }
  ]
}

UI

WXT提供了三种构建UI的方法:集成阴影根IFrame。每种方法都有其优势和适用场景。

集成

集成内容脚本UI是将UI与内容一起渲染到页面,这意味着它们会受到页面的CSS影响:

typescript
// entrypoints/example-ui.content.ts
export default defineContentScript({
  matches: ['<all_urls>'],

  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 在容器中添加UI组件
        const app = document.createElement('p');
        app.textContent = 'Hello world!';
        container.append(app);
      },
    });

    ui.mount();
  },
});

阴影根

阴影根是一种隔离CSS的技术,使得UI的样式不受页面影响:

typescript
// entrypoints/example-shadow-root.content.ts
export default defineContentScript({
  matches: ['<all_urls>'],
  cssInjectionMode: 'ui',

  main(ctx) {
    const ui = createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 在容器中渲染UI组件
        const app = document.createElement('div');
        container.append(app);
      },
      onRemove: () => {
        // 从容器中移除UI组件
        container?.remove();
      },
    });

    ui.mount();
  },
});

IFrame

使用IFrame可以将UI嵌入到另一个页面中,支持HMR:

typescript
// entrypoints/example-iframe.content.ts
export default defineContentScript({
  matches: ['<all_urls>'],

  main(ctx) {
    const ui = createIframeUi(ctx, {
      page: '/example-iframe.html',
      position: 'inline',
      anchor: 'body',
      onMount: (wrapper, iframe) => {
        // 设置IFrame的样式
        iframe.width = '123';
      },
    });

    ui.mount();
  },
});

隔离世界 vs 主世界

默认情况下,所有内容脚本运行在隔离环境中。MV3中,内容脚本可以通过world: 'MAIN'配置选项运行在主世界。

typescript
export default defineContentScript({
  world: 'MAIN',
});

嵌入式UI

通过createIntegratedUi函数可以将UI嵌入到页面:

typescript
// entrypoints/example-content.content.ts
import './style.css';

export default defineContentScript({
  matches: ['<all_urls>'],
  cssInjectionMode: 'ui',

  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 在容器中添加UI组件
        const app = document.createElement('p');
        app.textContent = 'Hello world!';
        container.append(app);
      },
    });

    ui.mount();
  },
});

阴影根

通过createShadowRootUi函数可以创建隔离的UI:

typescript
// entrypoints/example-shadow-root.content.ts
import './style.css';

export default defineContentScript({
  matches: ['<all_urls>'],
  cssInjectionMode: 'ui',

  main(ctx) {
    const ui = createShadowRootUi(ctx, {
      name: 'example-ui',
      position: 'inline',
      anchor: 'body',
      onMount: (container) => {
        // 在容器中渲染UI组件
        const app = document.createElement('div');
        container.append(app);
      },
      onRemove: () => {
        // 从容器中移除UI组件
        container?.remove();
      },
    });

    ui.mount();
  },
});

IFrame

通过createIframeUi函数可以创建嵌入式的UI:

typescript
// entrypoints/example-iframe.content.ts
import './style.css';

export default defineContentScript({
  matches: ['<all_urls>'],
  cssInjectionMode: 'ui',

  main(ctx) {
    const ui = createIframeUi(ctx, {
      page: '/example-iframe.html',
      position: 'inline',
      anchor: 'body',
      onMount: (wrapper, iframe) => {
        // 设置IFrame的样式
        iframe.width = '123';
      },
    });

    ui.mount();
  },
});

隔离世界与主世界的比较

隔离世界:所有内容脚本共享页面DOM。

主世界:支持更复杂的上下文交互,但不支持MV2。

要启用主世界,需要手动注入脚本:

typescript
// entrypoints/example-main-world.ts
export default defineUnlistedScript(() => {
  console.log('Hello from the main world');
});
typescript
// entrypoints/example.content.ts
export default defineContentScript({
  matches: ['*://*/*'],
  async main() {
    await injectScript('/example-main-world.js', {
      keepInDom: true,
    });
  },
});
json
export default defineConfig({
  manifest: {
    web_accessible_resources: [
      {
        resources: ["example-main-world.js"],
        matches: ["*://*/*"],
      }
    ]
  }
});

动态目标UI的挂载

在某些情况下,需要动态添加目标元素。可以通过autoMount函数自动挂载和移除UI:

typescript
export default defineContentScript({
  matches: ['<all_urls>'],
  main(ctx) {
    const ui = createIntegratedUi(ctx, {
      position: 'inline',
      anchor: '#your-target-dynamic-element',
      onMount: (container) => {
        // 添加UI组件到容器
        const app = document.createElement('p');
        app.textContent = '...';
        container.append(app);
      },
    });

    ui.autoMount();
  },
});

SPAs的处理

对于SPAs,由于内容脚本只在全页面刷新时运行,无法自动加载。需要手动监听路径变化:

typescript
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) {
  const ui = createIntegratedUi(ctx, {
    position: 'inline',
    anchor: '#your-target-element',
    onMount: () => {
      // 添加UI组件到容器
      const app = document.createElement('p');
      app.textContent = '...';
      container.append(app);
    },
  });

  ui.mount();
}

结论

通过以上方法,可以有效地创建和配置内容脚本,支持动态目标、隔离样式和主世界交互。