浏览器扩展开发指南:WXT + React + TS + TailwindCSS + AntDesign

一、前言

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

二、技术选型

浏览器扩展开发框架主要有 WXTExtensionPlasmoCreate Chrome Extension 等可以选择。

三、具体实现

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 内容:

  1. 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;
  1. 内容脚本实现
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 作为连接这些技术的核心纽带,极大地简化了开发流程,降低了开发难度,使得浏览器扩展的开发变得更加高效、便捷。

相关推荐
拖孩几秒前
微信群太多,管理麻烦?那试试接入AI助手吧~
前端·后端·微信
乌兰麦朵17 分钟前
Vue吹的颅内高潮,全靠选择性失明和 .value 的PUA!
前端·vue.js
Code季风17 分钟前
Gin Web 层集成 Viper 配置文件和 Zap 日志文件指南(下)
前端·微服务·架构·go·gin
蓝倾18 分钟前
如何使用API接口实现淘宝商品上下架监控?
前端·后端·api
舂春儿19 分钟前
如何快速统计项目代码行数
前端·后端
毛茸茸19 分钟前
⚡ 从浏览器到编辑器只需1秒,这个React定位工具改变了我的开发方式
前端
Pedantic20 分钟前
我们什么时候应该使用协议继承?——Swift 协议继承的应用与思
前端·后端
Software攻城狮21 分钟前
vite打包的简单配置
前端
Codebee21 分钟前
如何利用OneCode注解驱动,快速训练一个私有的AI代码助手
前端·后端·面试
流星稍逝22 分钟前
用vue3的写法结合uniapp在微信小程序中实现图片压缩、调整分辨率、做缩略图功能
前端·vue.js