
一、前言

浏览器扩展已然成为提升用户体验与增强浏览器功能的关键利器,从开发者工具(如 Vue.js devtools 和 React Developer Tools 等)到生产力助手(如翻译插件和截图插件等),再到个性化插件(如 Kimi 浏览器助手、Sider 和 Monica 等)。这些扩展通过多样化的交互形式满足不同场景需求,例如侧边栏、悬浮球、弹出式界面等,其中内容脚本(Content Script)技术是实现网页级功能扩展的关键。内容脚本是在网页情境中运行的文件,其能够直接操作 DOM 来读取浏览器访问的网页的详细信息,并对其进行更改,也可以将信息传递给其父级扩展程序。本文将详细介绍如何使用 WXT + React + TS + Tailwind CSS + Ant Design 开发一个浏览器扩展,重点探讨如何通过内容脚本(content script)机制将 React UI 组件动态注入目标页面,并完成对页面 DOM 的交互式操作。
二、技术选型
浏览器扩展开发框架主要有 WXT、Extension、Plasmo 和 Create Chrome Extension 等可以选择。
- 浏览器扩展框架 WXT
- 开发框架 React
- 语言 Typescript
- 样式框架 Tailwind CSS
- 组件库 Ant Design
- 包管理 Pnpm
三、具体实现
3.1 WXT + React 设置
安装 WXT 初始化项目,并选择使用 React 框架和 Pnpm 包管理工具;
bash
$pnpm dlx wxt@latest init

WXT 默认情况下是一个平面文件夹结构,可以在 wxt.config.ts 文件中配置使用 src/ 目录将源代码与配置文件分开;
typescript
export default defineConfig({
srcDir: 'src',
});
浏览器扩展支持不同的入口点,如 Bookmarks、Devtools、Content Scripts、Popup 和 Side Panel 等,我们主要关注 Content Scripts 入口店,即 entrypoints/content.ts 文件;
typescript
export default defineContentScript({
matches: ['*://*.google.com/*'], // 当页面规则匹配时则执行脚本逻辑,例如往页面中注入 UI 内容或其他逻辑
main() {
// 脚本执行主入口
console.log('Hello content.');
},
});
WXT 提供了 3 种内置的方式来通过内容脚本的方式往页面注入新 UI 内容,Integrated、Shadow Root 和 IFrame,我们选择 Shadow Root 的方式来注入内容,其提供了较好的样式隔离。在 entrypoints/content.ts 文件主入口 通过 createShadowRootUi API + React 实现;

typescript
import ReactDOM from "react-dom/client";
import { ContentScriptContext } from "wxt/utils/content-script-context";
import App from "@/App";
const CONTAINER_ID = "WXT-CONTAINER";
const initVisualEditor = async (ctx: ContentScriptContext) => {
const ui = await createShadowRootUi(ctx, {
name: "wxt-content",
position: "inline",
anchor: "body",
onMount: (container, shadowRoot, shadowHost) => {
const wrapper = document.createElement("div");
container.append(wrapper);
const root = ReactDOM.createRoot(wrapper);
root.render(<App />);
shadowHost.id = CONTAINER_ID;
return { root, wrapper };
},
onRemove: (elements) => {
elements?.root.unmount();
elements?.wrapper.remove();
},
});
ui.mount();
return ui;
};
export default defineContentScript({
matches: ["*://*/*"],
cssInjectionMode: "ui",
async main(ctx) {
let ui: null | globalThis.ShadowRootContentScriptUi<{
root: ReactDOM.Root;
wrapper: HTMLDivElement;
}> = null;
try {
ui = await initVisualEditor(ctx);
} catch(e) {
console.log(e);
}
},
});
3.2 Tailwind CSS 设置
WXT 使用 Vite 作为构建工具,因此 Tailwind CSS 安装使用 Vite 的方式;
bash
$pnpm install tailwindcss @tailwindcss/vite
Vite 插件的配置在 vite.config.ts 文件中,而使用 WXT 则在 wxt.config.ts 文件的 vite 字段中;
typescript
import { defineConfig } from "wxt";
import tailwindcss from "@tailwindcss/vite";
export default defineConfig({
// ...
vite: () => ({
plugins: [tailwindcss()],
}),
});
创建 global.css 文件,导入 Tailwind CSS,并且将 global.css 文件导入到 app.tsx 文件即可生效;
css
@import "tailwindcss";

3.3 Ant Design 设置
安装 Ant Design 组件库;
bash
$pnpm install antd --save
在 WXT 中通过 Shadow DOM 注入的 UI 组件需要注意样式隔离问题。由于 Shadow DOM 的 标签插入机制与常规 DOM 不同,因此必须使用 @ant-design/cssinjs 提供的 StyleProvider 组件,并通过其 container 属性显式指定样式规则的插入位置。
对于 Select、Dialog、Tooltip、Cascader、AutoComplete 和 Dropdown 等带有浮层元素的组件,若需要将浮层定位基准从宿主页面切换为浏览器扩展内容,可通过 ConfigProvider 的 getPopupContainer 属性进行配置,将浮层容器指定为 Shadow Root 节点。
typescript
import ReactDOM from "react-dom/client";
import { StyleProvider } from "@ant-design/cssinjs";
import { ConfigProvider } from "antd";
import { ContentScriptContext } from "wxt/utils/content-script-context";
import App from "@/App";
const CONTAINER_ID = "WXT-CONTAINER";
const initVisualEditor = async (ctx: ContentScriptContext) => {
const ui = await createShadowRootUi(ctx, {
name: "wxt-content",
position: "inline",
anchor: "body",
onMount: (container, shadowRoot, shadowHost) => {
const wrapper = document.createElement("div");
container.append(wrapper);
const root = ReactDOM.createRoot(wrapper);
root.render(
<StyleProvider container={shadowRoot}>
<ConfigProvider getPopupContainer={() => shadowRoot as any}>
<App />
</ConfigProvider>
</StyleProvider>
);
shadowHost.id = CONTAINER_ID;
return { root, wrapper };
},
onRemove: (elements) => {
elements?.root.unmount();
elements?.wrapper.remove();
},
});
ui.mount();
return ui;
};
3.4 内容开发

经过上述配置,所有前置准备工作均已就绪。现在,在 App.tsx 中开发时,您将获得与原生 React 框架完全一致的开发体验。我们简单尝试一下:"使用一个AntDesign 组件,并且监听点击事件,获取到宿主页面的元素,元素类型显示在右上角悬浮卡片,并对这个元素加一个蓝色边框"。
typescript
import { useState } from "react";
import { FloatButton } from "antd";
import "./styles/global.css";
function App() {
const [enable, setEnable] = useState(true);
const [clickTag, setClickTag] = useState("");
useEffect(() => {
const handleClick = (e: MouseEvent) => {
if (e.target instanceof HTMLElement) {
const tagName = e.target.tagName;
e.target.style.border = "1px solid blue"; // 对这个点击的元素加一个边框
setClickTag(tagName || "");
}
};
if (enable) {
window.addEventListener("click", handleClick);
} else {
window.removeEventListener("click", handleClick);
}
return () => {
window.removeEventListener("click", handleClick);
};
}, [enable]);
return (
<>
{enable && (
<div className="fixed top-4 right-4 z-50">
<div className="w-64 p-6 bg-white rounded-lg shadow-xl">
<h3 className="text-lg font-semibold text-gray-800">悬浮卡片</h3>
<p className="mt-2 text-gray-600">点击元素:{clickTag ?? "--"}</p>
</div>
</div>
)}
<FloatButton
onClick={() => setEnable((val) => !val)}
tooltip={<div className="text-xl font-bold">hello, sherwin</div>}
/>
</>
);
}
export default App;
3.5 消息通信
内容脚本还可以与浏览器扩展的不同部分之间通信,例如可以通过 popup 中的按钮点击控制内容脚本是否注入 UI 内容:
- popup 实现
typescript
import { useState } from "react";
function App() {
const handleClick = async () => {
const [tab] = await browser.tabs.query({
active: true,
currentWindow: true,
});
if (tab.id) {
await browser.tabs.sendMessage(tab.id, {
action: "TOGGLE",
});
}
};
return (
<>
<button onClick={handleClick}>click toggle content</button>
</>
);
}
export default App;
- 内容脚本实现
typescript
export default defineContentScript({
matches: ["*://*/*"],
cssInjectionMode: "ui",
async main(ctx) {
let ui: null | globalThis.ShadowRootContentScriptUi<{
root: ReactDOM.Root;
wrapper: HTMLDivElement;
}> = null;
try {
ui = await initVisualEditor(ctx);
} catch (e) {
console.log(e);
}
browser.runtime.onMessage.addListener(async (event) => {
if (event.action === "TOGGLE") {
if (!ui) {
ui = await initVisualEditor(ctx);
} else {
ui.remove();
ui = null;
}
}
});
},
});
四、总结
本文源码地址:wxt-learn
在本文的深入探讨中,我们全面展示了如何借助 WXT + React + TS + Tailwind CSS + Ant Design 这一强大技术栈来开发浏览器扩展。这一技术组合为开发者提供了一套高效且现代化的开发流程,不仅能够满足基础功能的构建需求,还为实现更复杂的扩展功能奠定了坚实基础。而 WXT 作为连接这些技术的核心纽带,极大地简化了开发流程,降低了开发难度,使得浏览器扩展的开发变得更加高效、便捷。