一、使用 Vite 创建 React 项目

 1npm create vite@latest # npm
 2yarn create vite			 # yarn
 3pnpm create vite			 # pnpm

选择 ReactTS

进入项目,并进行 pnpm i 安装 node_modules

 1pnpm i # 安装 node_modules

此时项目文件夹目录为:

 1.
 2├── README.md
 3├── index.html
 4├── package.json
 5├── pnpm-lock.yaml
 6├── public
 7│   └── vite.svg
 8├── src
 9│   ├── App.css
10│   ├── App.tsx
11│   ├── assets
12│   │   └── react.svg
13│   ├── index.css
14│   ├── main.tsx
15│   └── vite-env.d.ts
16├── tsconfig.json
17├── tsconfig.node.json
18└── vite.config.ts

二、修改 React 项目

因为我们是开发 Chrome 插件,需要 manifest.json、service-worker、content、popup 页面等文件,所以需要对之前的项目进行删除,并添加我们自己的配置

1. 项目修改

  1. 删除项目根目录下的 index.html 文件
  2. 删除 src 目录下的 App.tsx、main.tsx、App.css、index.css
  3. 删除根目录下的 public 文件夹
  4. 在根目录下创建 manifest.json 文件,此乃插件入口文件
  5. 创建 popup 页面:在 src 目录下创建 popup 文件夹,popup 文件夹中创建 App.tsx、main.tsx、App.css、index.html、index.css、components 文件夹,components 文件夹下创建 TestPopup.tsx 文件(这些新建的文件内容可以参考刚刚删除的文件,但要注意修改 index.html 文件中 main.tsx 的引入路径)在 TestPopup.tsx 文件中写入 Popup Page 文案
  6. 创建 content 页面:在 src 目录下创建 contentPage 文件夹,contentPage 文件夹中创建 App.tsx、main.tsx、App.css、index.html、index.css、components 文件夹,components 文件夹下创建 TestContent.tsx 文件(这些新建的文件内容可以参考刚刚删除的文件,但要注意修改 index.html 文件中 main.tsx 的引入路径)在 TestContent.tsx 文件中写入 Content Page 文案
  7. 创建 background:在 src 目录下创建 background 文件夹,background 文件夹中创建 service-worker.ts,文件里面写入 console.log('background service-worker file')
  8. 创建 content:在 src 目录下创建 content 文件夹,content 文件夹下创建 content.ts,文件写入 console.log('content file')
  9. src 目录下新建 icons 文件夹,用于放置插件 icon,可以网上找个 icon.png

2. 步骤解析

  • 前三步就是删除
  • 第四步是创建插件的入口文件,此文件必须有,在根目录和 src 目录都行,但一般习惯放在根目录中
  • 第五步是创建 popup 弹框页面,如果你的插件不需要可以忽略这一步
  • 第六步是创建 content 页面,和第八步的 content 的区别是这个最终打包为 index.html 文件,通过 iframe 的形式插入对应域名的页面中
  • 第七步是创建 service-worker 页面,V3 虽然也叫 background,但是这个文件一般都写成 service-worker
  • 第八步就是创建注入对应域名的 content.ts 文件
  • 第九步是放置插件的 16、32、48、128 的 png 图片,可以用一张 128 的也行

3. 文件夹目录

 1.
 2├── README.md
 3├── manifest.json
 4├── package.json
 5├── pnpm-lock.yaml
 6├── src
 7│   ├── assets
 8│   │   └── react.svg
 9│   ├── background
10│   │   └── service-worker.ts
11│   ├── content
12│   │   └── content.ts
13│   ├── contentPage
14│   │   ├── App.css
15│   │   ├── App.tsx
16│   │   ├── components
17│   │   │   └── TestContent.tsx
18│   │   ├── index.css
19│   │   ├── index.html
20│   │   └── main.tsx
21│   ├── icons
22│   │   └── icon.png
23│   ├── popup
24│   │   ├── App.css
25│   │   ├── App.tsx
26│   │   ├── components
27│   │   │   └── TestPopup.tsx
28│   │   ├── index.css
29│   │   ├── index.html
30│   │   └── main.tsx
31│   └── vite-env.d.ts
32├── tsconfig.json
33├── tsconfig.node.json
34└── vite.config.ts

三、配置项目

1. 配置 manifest.json 文件

1.1. 写入以下内容

 1{
 2  "manifest_version": 3,
 3  "name": "My React Chrome Ext",
 4  "version": "0.0.1",
 5  "description": "Chrome 插件",
 6  "icons": {
 7    "16": "icons/icon.png",
 8    "19": "icons/icon.png",
 9    "38": "icons/icon.png",
10    "48": "icons/icon.png",
11    "128": "icons/icon.png"
12  },
13  "action": {
14    "default_title": "React Chrome Ext",
15    "default_icon": "icons/icon.png",
16    "default_popup": "popup/index.html"
17  },
18  "background": {
19    "service_worker": "background/service-worker.js"
20  },
21  "permissions": [],
22  "host_permissions": [],
23  "content_scripts": [
24    {
25      "js": [
26        "content/content.js"
27      ],
28      "matches": [
29        "http://127.0.0.1:5500/*"
30      ],
31      "all_frames": true,
32      "run_at": "document_end",
33      "match_about_blank": true
34    }
35  ]
36}

1.2. 解析

  1. manifest_version 字段一定得是 3
  2. 因为只有一张图,所以 iconsaction/default_icon 就用同一张图片
  3. 可以看到 action/default_popup 配置的值为 popup/index.html,是因为我想把项目 build 成这种路径
  4. background/service_worker 也是同理,要 buildbackground/service-worker.js 这种路径
  5. 通了 content_scripts 配置也是一样,buildcontent/content.js
  6. match 配置了一个本地的路径,方便调试(使用 vscodeLive Server 插件或者 node 包启动一个服务)
    1. 如果你想在哪个页面显示就配置哪个页面的域名即可

2. 配置 Chrome 插件的 Types

因为我们使用的是 TypeScripts 来进行开发 Chrome 插件,所以需要配置一个 Chrome 插件 APITypes

2.1. 安装 chrome-types

 1pnpm i chrome-types -D

2.2. 配置 Types

src/vite-env.d.ts 文件中写入

 1/// <reference types="chrome-types/index" />

这样的话,就可以在 popup、content、background 中使用 chrome,并且有类型等提示

service-worker.ts

content.ts

popup/main.ts

3. 配置 vite.config.ts

配置构建文件,需要按照我们写入的 manifest.json 文件进行配置

3.1. 复制文件,使用 rollup-plugin-copy 复制 icons 以及 manifest.json 文件

通过复制可以直接把需要的文件复制到对应的目录中,这些复制的文件不需要构建,不需要压缩

3.1.1. 安装 rollup-plugin-copy
 1pnpm i rollup-plugin-copy -D
3.1.2. 配置 vite.config.ts
 1import { defineConfig } from 'vite'
 2import react from '@vitejs/plugin-react-swc'
 3import copy from 'rollup-plugin-copy' 
 4
 5
 6export default defineConfig({
 7  root: 'src/',
 8  plugins: [
 9    react(),
10    copy({
11      targets: [
12        { src: 'manifest.json', dest: 'dist' }, 
13        { src: "src/icons/**", dest: 'dist/icons' } 
14      ]
15    })
16  ],
17})

3.2. 配置 build 选项

build 构建,需要按照我们的 manifest.json 引入的配置

3.2.1. 需要引入 @types/node
 1pnpm i @types/node -D
3.2.2. 配置 build
 1build: {
 2  outDir: path.resolve(__dirname, 'dist'),
 3    rollupOptions: {
 4    input: {
 5      popup: path.resolve(__dirname, 'src/popup/index.html'),
 6        contentPage: path.resolve(__dirname, 'src/contentPage/index.html'),
 7        content: path.resolve(__dirname, 'src/content/content.ts'),
 8        background: path.resolve(__dirname, 'src/background/service-worker.ts'),
 9        },
10    output: {
11      assetFileNames: 'assets/[name]-[hash].[ext]', 
12        chunkFileNames: 'js/[name]-[hash].js', 
13        entryFileNames: (chunkInfo) => { 
14        const baseName = path.basename(chunkInfo.facadeModuleId, path.extname(chunkInfo.facadeModuleId))
15        const saveArr = ['content', 'service-worker']
16        return `[name]/${saveArr.includes(baseName) ? baseName : chunkInfo.name}.js`;
17      },
18        name: '[name].js'
19    }
20  },
21},
3.2.2.1. 解析
  • input 模块配四个文件,两个是页面,两个是 ts 文件
  • output/entryFileNames 配置,是判断如果传入的是 content.tsservice-worker.ts,也用这两个当生成的文件名称
3.2.3. 配置 root

因为我们引入的页面是从 src 下面的引入的,所以需要配置下 root 字段

 1root: 'src/',

3.3. 完整的 vite.config.ts 文件

 1import { defineConfig } from 'vite'
 2import react from '@vitejs/plugin-react-swc'
 3import path from 'path'
 4import copy from 'rollup-plugin-copy'
 5
 6
 7export default defineConfig({
 8  root: 'src/',
 9  plugins: [
10    react(),
11    copy({
12      targets: [
13        { src: 'manifest.json', dest: 'dist' },
14        { src: "src/icons/**", dest: 'dist/icons' }
15      ]
16    })
17  ],
18  build: {
19    outDir: path.resolve(__dirname, 'dist'),
20    rollupOptions: {
21      input: {
22        popup: path.resolve(__dirname, 'src/popup/index.html'),
23        contentPage: path.resolve(__dirname, 'src/contentPage/index.html'),
24        content: path.resolve(__dirname, 'src/content/content.ts'),
25        background: path.resolve(__dirname, 'src/background/service-worker.ts'),
26      },
27      output: {
28        assetFileNames: 'assets/[name]-[hash].[ext]', 
29        chunkFileNames: 'js/[name]-[hash].js', 
30        entryFileNames: (chunkInfo) => { 
31          const baseName = path.basename(chunkInfo.facadeModuleId, path.extname(chunkInfo.facadeModuleId))
32          const saveArr = ['content', 'service-worker']
33          return `[name]/${saveArr.includes(baseName) ? baseName : chunkInfo.name}.js`;
34        },
35        name: '[name].js'
36      }
37    },
38  },
39})
40

四、构建项目

1. 运行 build 命令

运行 pnpm run build,生成 dist 文件夹

 1pnpm run build

2. dist 文件夹目录

 1dist
 2├── assets
 3│   └── popup-05NROAPV.css
 4├── background
 5│   └── service-worker.js
 6├── content
 7│   └── content.js
 8├── contentPage
 9│   ├── contentPage.js
10│   └── index.html
11├── icons
12│   └── icon.png
13├── js
14│   └── client-37I-Sspp.js
15├── manifest.json
16└── popup
17    ├── index.html
18    └── popup.js

3. 加载已解压的扩展程序

chrome://extensions/ 页面点击【加载已解压的扩展程序】选择 dist 目录

选择完之后,可以看到我们的插件已经出现在扩展程序列表中了

4. 打开本地页面

4.1. 控制台输出 content 内容

可以看到控制台输出了我们的 content.ts 文件的内容

4.2. Popup action

把插件固定,点击插件 action 按钮,弹出 popup 页面 popup 中的 app.css 加个宽高

 1#app{
 2  width: 400px;
 3  height: 400px;
 4}

5. 打开插件控制台

可以看到 service-worker.ts 中的内容

6. Content page 如何注入页面?

我们的 vite.config.ts 中的 build 选项中还构建了一个 contentPage 页面呢,这个页面要怎么注入呢?

对于普通的注入 js 的文件,我们直接写在 content.ts 中,打包构建之后就可以注入了,这个 contentPage 存在的意义是向页面注入页面,一般是嵌在 iframe

6.1. Contnet.ts 中注入 iframe

contnet.ts 中写入以下代码

 1console.log('content file')
 2const init = () => {
 3  const addIframe = (id: string, pagePath: string) => {
 4    const contentIframe = document.createElement("iframe");
 5    contentIframe.id = id;
 6    contentIframe.style.cssText = "width: 100%; height: 100%; position: fixed; inset: 0px; margin: 0px auto; z-index: 10000002; border: none;";
 7    const getContentPage = chrome.runtime.getURL(pagePath);
 8    contentIframe.src = getContentPage;
 9    document.body.appendChild(contentIframe);
10  }
11
12  addIframe('content-start-iframe', 'contentPage/index.html')
13}
14
15init()

解析:

  • 通过 content.ts 代码,生成 iframe 元素,src 为我们的 contentPage/index.html,这个路径获取需要通过 chrome.runtime.getURL 获取
  • 为什么还要包裹一层 init 函数?
    • 因为我们的 manifest.jsoncontent_scriptall_framestrue,这个代表着我们的 content.ts 会注入所有的 frames 中,加一个这个是在判断 topself 相等的时候在注入
 1
 2if (window.top === window.self) {
 3  init();
 4}

6.2. 重新构建项目

重新 build 项目,然后刷新插件,再刷新下我们的本地项目 可以发现:“此页面已被屏蔽”

但是我们打开控制台,可以发现我们的 iframe 已经注入到页面了,屏蔽的页面正是我们的 iframe

这样可不行啊,被屏蔽了怎么能行…

6.3. 配置 manifest.json 中的 web_accessible_resources 字段

web_accessible_resources:网络可访问的资源

 1"web_accessible_resources": [
 2  {
 3    "resources": ["popup/*", "contentPage/*", "assets/*", "js/*"],
 4    "matches": ["http://127.0.0.1:5500/*"],
 5    "use_dynamic_url": true
 6  }
 7]

我们需要把我们插件的资源允许访问才行

  • 匹配的 matches 还是我们本地的域名,要和 content_scripts 中一致
  • resources 是我们打包构建之后的 dist 里面的目录
  • 需要哪些写哪些,"popup/*" 可以删除不写

6.4 再次重新构建项目

重新 build 项目,刷新插件,刷新本地项目 contentPage 中的 app.css 加个宽高和背景色

 1#app{
 2  width: 400px;
 3  height: 400px;
 4  background: gray;
 5}

可以看到我们的 iframe 已经加载了

但是这个时候 iframe 把我们的项目挡住了,那其实我们可以先把 iframe 设置为 width: 0px,然后在某些需要展示 iframe 的时候在设置宽度即可

五、项目开发

1. 图片资源

图片资源使用比较简单,比如我们的 assets 文件夹放入一个图片

 1src/assets
 2├── Vite_React_Chrome_Ext.jpg
 3└── react.svg

1.1. Popup 页面使用图片

  1. 直接引入,TestPopup.tsx 内容
 1import reactViteImg from '../../assets/Vite_React_Chrome_Ext.jpg'
 2
 3export const TestPopup = () => {
 4  return (
 5    <>
 6      <span>Popup Page</span>
 7      <img src={reactViteImg} width="270px" height="170px" />
 8    </>
 9  )
10}
  1. 重新 build 项目,刷新插件,刷新页面,点击 popup action,弹出 popup 页面

1.2. Content 页面使用图片

  1. 直接引入,TestContent.tsx 内容
 1import reactViteImg from '../../assets/Vite_React_Chrome_Ext.jpg'
 2
 3export const TestContent = () => {
 4  return (
 5    <>
 6      <span>Content Page</span>
 7      <img src={reactViteImg} width="270px" height="170px" />
 8    </>
 9  )
10}
  1. 重新 build 项目,刷新插件,刷新页面

2. 使用 UI

Ant Design 为例

2.1. 安装 Ant Design

 1pnpm i antd

2.2. Popup 页面使用

  1. 直接引入,TestPopup.tsx 内容:
 1import { Button } from 'antd'
 2
 3import reactViteImg from '../../assets/Vite_React_Chrome_Ext.jpg'
 4
 5export const TestPopup = () => {
 6  return (
 7    <>
 8      <span>Popup Page</span>
 9      <img src={reactViteImg} width="270px" height="170px" />
10      <hr />
11      <Button type="primary">Primary Button</Button>
12      <Button>Default Button</Button>
13      <Button type="dashed">Dashed Button</Button>
14      <Button type="text">Text Button</Button>
15      <Button type="link">Link Button</Button>
16    </>
17  )
18}
19
  1. 重新 build 项目,刷新插件,刷新页面,点击 popup action,弹出 popup 页面

2.3. Content 页面使用

  1. 直接引入,TestContent.tsx 内容
 1import { Button } from 'antd'
 2
 3import reactViteImg from '../../assets/Vite_React_Chrome_Ext.jpg'
 4
 5export const TestContent = () => {
 6  return (
 7    <>
 8      <span>Content Page</span>
 9      <img src={reactViteImg} width="270px" height="170px" />
10      <hr />
11      <Button type="primary">Primary Button</Button>
12      <Button>Default Button</Button>
13      <Button type="dashed">Dashed Button</Button>
14      <Button type="text">Text Button</Button>
15      <Button type="link">Link Button</Button>
16    </>
17  )
18}
19
  1. 重新 build 项目,刷新插件,刷新页面

3. 状态管理 Zustand

Zustand 是一个简单而强大的状态管理库,它提供了一个小巧的 API,可以让你轻松地管理组件的状态。 Zustand 的 API 简单而直观,适用于小型到中型的应用。

3.1. 安装 zustand

 1pnpm i zustand

3.2. Popup 页面使用

  1. src/popup 中新建 store 文件夹,新建 store.ts 文件

popup 文件夹目录

 1src/popup
 2├── App.css
 3├── App.tsx
 4├── components
 5│   └── TestPopup.tsx
 6├── index.css
 7├── index.html
 8├── main.tsx
 9└── store
10    └── store.ts
  1. counter.ts 文件写入以下内容
 1import { create } from 'zustand';
 2
 3interface ICountStoreState {
 4  count: number
 5  increment: (countNum: number) => void
 6  decrement: (countNum: number) => void
 7}
 8
 9const useStore = create<ICountStoreState>((set) => ({
10  count: 0,
11  increment: (countNum: number) => set((state) => ({ count: state.count + countNum })),
12  decrement: (countNum: number) => set((state) => ({ count: state.count - countNum })),
13}));
14
15export default useStore;
16
  1. TestPopup.tsx 页面引入和使用
 1import { Button } from 'antd'
 2
 3import useStore from '../store/store';
 4import reactViteImg from '../../assets/Vite_React_Chrome_Ext.jpg'
 5
 6export const TestPopup = () => {
 7  const { count, increment, decrement } = useStore();
 8  return (
 9    <>
10      <span>Popup Page</span>
11      <div>
12        <span>count is {count}</span>
13        <button onClick={() => increment(1)}>
14          increment 1
15        </button>
16        <button onClick={() => decrement(1)}>
17          decrement 1
18        </button>
19      </div>
20      <img src={reactViteImg} width="270px" height="170px" />
21      <hr />
22      <Button type="primary">Primary Button</Button>
23      <Button>Default Button</Button>
24      <Button type="dashed">Dashed Button</Button>
25      <Button type="text">Text Button</Button>
26      <Button type="link">Link Button</Button>
27    </>
28  )
29}
30
  1. 重新 build,刷新插件,刷新页面,点击 popup action 弹出页面,点击按钮操作

3.3. Content 页面使用

  1. src/contentPage 中新建 store 文件夹,新建 store.ts 文件

文件夹目录

 1src/contentPage
 2├── App.css
 3├── App.tsx
 4├── components
 5│   ├── TestContent.tsx
 6├── index.css
 7├── index.html
 8├── main.tsx
 9└── store
10    └── store.ts
  1. counter.tspopup 中的 store.ts 一样即可
 1import { create } from 'zustand';
 2
 3interface ICountStoreState {
 4  count: number
 5  increment: (countNum: number) => void
 6  decrement: (countNum: number) => void
 7}
 8
 9const useStore = create<ICountStoreState>((set) => ({
10  count: 0,
11  increment: (countNum: number) => set((state) => ({ count: state.count + countNum })),
12  decrement: (countNum: number) => set((state) => ({ count: state.count - countNum })),
13}));
14
15export default useStore;
16
17
  1. TestContent.tsx 页面引入和使用
 1import { Button } from 'antd'
 2
 3import useStore from '../store/store';
 4import reactViteImg from '../../assets/Vite_React_Chrome_Ext.jpg'
 5
 6export const TestContent = () => {
 7  const { count, increment, decrement } = useStore();
 8  return (
 9    <>
10      <span>Content Page</span>
11      <div>
12        <span>count is {count}</span>
13        <button onClick={() => increment(1)}>
14          increment 1
15        </button>
16        <button onClick={() => decrement(1)}>
17          decrement 1
18        </button>
19      </div>
20      <img src={reactViteImg} width="270px" height="170px" />
21      <hr />
22      <Button type="primary">Primary Button</Button>
23      <Button>Default Button</Button>
24      <Button type="dashed">Dashed Button</Button>
25      <Button type="text">Text Button</Button>
26      <Button type="link">Link Button</Button>
27    </>
28  )
29}
30
  1. 重新 build,刷新插件,刷新页面

4. 使用 CSS 预处理器

Vite 同时提供了对 .scss, .sass, .less, .styl.stylus 文件的内置支持

因为 vite 内置支持,所以只需要安装依赖就行

4.1. 安装 less

 1pnpm i less -D

4.2. Popup 页面使用

  1. TestPopup.tsx 中加入以下代码
 1<div className="test-popup">
 2  <ul>
 3    <li>popup</li>
 4  </ul>
 5</div>
  1. components 新建 index.less 文件
 1src/popup/components
 2├── TestPopup.tsx
 3└── index.less
  1. index.less 写入以下内容
 1.test-popup{
 2  background: red;
 3  padding: 20px;
 4  ul{
 5    padding: 20px;
 6    background: black;
 7    li{
 8      background: green;
 9      padding: 20px;
10    }
11  }
12}
  1. TestPopup.tsx 中引入 index.less
 1import './index.less'
  1. Popup 页面展示

4.3. Content 页面使用

  1. TestContent.tsx 中加入以下代码
 1<div className="test-content">
 2  <ul>
 3    <li>content</li>
 4  </ul>
 5</div>
  1. components 新建 index.less 文件
 1src/contentPage/components
 2├── TestContent.tsx
 3└── index.less
  1. index.less 写入以下内容
 1.test-content{
 2  background: red;
 3  padding: 20px;
 4  ul{
 5    padding: 20px;
 6    background: black;
 7    li{
 8      background: green;
 9      padding: 20px;
10    }
11  }
12}
  1. TestContent.tsx 中引入 index.less
 1import './index.less'
  1. Content 页面展示

六、热加载

1. 只有 Popup 页面和 Content 页面需要热加载

如果我们的 manifest.json 文件基本上固定的,不需要更新,只需要 popup 页面和 content 页面在保存的时候进行 build 以及刷新的话,有一种很简单的方式

1.1. 我们本地启动的项目和我们插件的项目在同一个文件夹

这样的话,当我们点击保存的时候,会自动触发刷新页面

1.2. 配置新的 build script 命令,监听文件更新,重新 build

  1. "watch-build": "vite --watch build"
  2. 终端启动
  3. 更新 popup 文件夹和 content 文件夹下的内容即可
 1pnpm run watch-build
  1. 我的本地项目启动之后域名为:http://127.0.0.1:5500/testhtml/test.html
  2. 目录为:/User/demo/chrome/testhtml/test.html
  3. 插件根目录为:/Users/demo/chrome/test-chrome/react-chrome-ext-pro
  4. 我在 chrome 这一层启动 live-server 服务,这个可以自动实现热加载
  5. 配置 build 监听命令是为了保存的时候可以重新 build,再配合刷新的话,这样就不用手动刷新插件和页面了
  6. 新更改的 popup 页面和 content 页面也能及时的显示出来

添加 watch-build 文案,不需要刷新插件也可显示出新页面

popup 页面

Content 页面

2. 插件模块热加载(backgroundservice-worker.ts、content.ts 文件)

插件热加载的话是需要刷新插件的,而且同时也需要监听文件夹的变化 如果是在 V2 版本中,可以在 background.ts 中使用 getPackageDirectoryEntry 方法,获取文件夹内容以及监听变化 但是 getPackageDirectoryEntry 方法在 V3 中被限制了,只能在 popup 页面中使用,但是 popup 页面只有点击的时候才会弹出来… 所以,我们换个方法监听文件

2.1. service-worker.ts 文件写入以下内容

 1console.log('background service-worker file')
 2chrome.management.getSelf(self => {
 3  if (self.installType === 'development') {
 4    
 5    const fileList = [
 6      'http://127.0.0.1:5501/dist/manifest.json',
 7      'http://127.0.0.1:5501/dist/popup/popup.js',
 8      'http://127.0.0.1:5501/dist/background/service-worker.js',
 9      'http://127.0.0.1:5501/dist/content/content.js',
10      'http://127.0.0.1:5501/dist/contentPage/contentPage.js'
11    ]
12    
13    const fileObj: {
14      [prop: string]: string
15    } = {}
16    
17     * reload 重新加载
18     */
19    const reload = () => {
20      chrome.tabs.query(
21        {
22          active: true,
23          currentWindow: true
24        },
25        (tabs: chrome.tabs.Tab[]) => {
26          if (tabs[0]) {
27            chrome.tabs.reload(tabs[0].id);
28          }
29          
30          chrome.runtime.reload();
31        }
32      );
33    };
34
35    
36     * 遍历监听的文件通过请求获取文件内容判断是否需要刷新
37     */
38    const checkReloadPage = () => {
39      fileList.forEach((item) => {
40        fetch(item).then((res) => res.text())
41          .then(files => {
42            if (fileObj[item] && fileObj[item] !== files) {
43              reload()
44            } else {
45              fileObj[item] = files
46            }
47          })
48          .catch(error => {
49            console.error('Error checking folder changes:', error);
50          });
51      })
52    }
53
54    
55    
56    
57
58    
59     * 设置闹钟(定时器)
60     */
61    
62    const ALARM_NAME = 'LISTENER_FILE_TEXT_CHANGE';
63    
64     * 创建闹钟
65     */
66    const createAlarm = async () => {
67      const alarm = await chrome.alarms.get(ALARM_NAME);
68      if (typeof alarm === 'undefined') {
69        chrome.alarms.create(ALARM_NAME, {
70          periodInMinutes: 0.1
71        });
72        checkReloadPage();
73      }
74    }
75    createAlarm();
76    
77    chrome.alarms.onAlarm.addListener(checkReloadPage);
78  }
79})
  1. 第一行日志输出
  2. 第 2~3 两行是判断当开发环境为 development 时,才会走以下流程
  3. 第 5~11 行是重新在当前插件的根目录启动一个 live-server 服务,写入需要监听的文件列表,然后可以通过 fetch 请求的方式获取文件内容(一定要起个服务才行,而且监听文件的 URL 要能正确访问才行,如果 fileList 字段和你的不匹配需要修改才行)
  4. 第 13~15 行是定义一个对象,key 就是文件列表的路径
  5. 第 19~33 行是刷新插件和刷新当前 tab 页面
  6. 第 38~52 行是遍历文件列表,通过 fetch 请求获取文件内容,进行判断是否和 fileObj 中保存的数据是否一致,如果不一致则进行 reload
  7. 第 54~56 行是定义一个 setInterval,间隔多少时间进行遍历文件内容去判断
  8. 第 62~77 行是用 Chromealarms 来当定时器(建议)

2.2. 配置 Manifest.json 文件

因为使用了一些 ChromeAPI,所以需要添加权限才行

 1"permissions": [
 2  "activeTab",
 3  "tabs",
 4  "alarms"
 5],

2.3. 配置 build script 命令,监听文件更新,重新 build

  1. "watch-build": "vite --watch build"
  2. 终端启动
 1pnpm run watch-build
  • build 的包,第一次还是需要点击刷新按钮才行
  • 之后再更新 service-worker.ts/content.ts 或者 popup 以及 content 的页面的时候就会自动刷新了
  • 可以看到 alarms 创建的闹钟最小时间是 6s,如果觉得太长的话可以使用上面的 setInterval

3. Manifest.json 文件热加载

可以发现我们修改 manifest.json 文件还是不会触发热加载,这就需要重新配置,我们使用 nodemon 监听

3.1. 全局安装 nodemon

 1npm i nodemon -g

3.2. 项目根目录新建 watch.mjs 文件,写入以下内容

 1import { spawn } from 'child_process';
 2import path from 'path';
 3import { fileURLToPath } from 'url';
 4import { dirname } from 'path';
 5
 6const __filename = fileURLToPath(import.meta.url);
 7const __dirname = dirname(__filename);
 8
 9const VITE_BIN_PATH = path.resolve(__dirname, 'node_modules/.bin/vite');
10
11const watcher = spawn('nodemon', ['--watch', 'manifest.json', '--exec', VITE_BIN_PATH, 'build'], {
12  stdio: 'inherit',
13});
14watcher.on('exit', (code) => {
15  process.exit(code);
16});
  1. 使用 nodemon 监听 manifest.json 文件,触发监听

3.3. 配置新的 script

 1"watch-json": "node watch.mjs"

再启动一个终端进行 json 的监听

 1pnpm run watch-json

此时更改 manifest.json 文件,在 alarms 触发之后就会刷新插件了

修改 description 字段

4. 插件报错

如果你的插件报如下的错

Error checking folder changes: TypeError: Failed to fetch

这说明你的监听请求有问题,需要看下是不是服务被停止了,重启服务然后清除错误刷新插件即可

七、项目最终目录结构

 1.
 2├── README.md								# readme 文件 								
 3├── manifest.json							# 插件配置文件入口文件
 4├── package.json							# 项目配置文件
 5├── pnpm-lock.yaml
 6├── src
 7│   ├── assets								# 静态资源页面
 8│   │   ├── Vite_React_Chrome_Ext.jpg		
 9│   │   └── react.svg
10│   ├── background							# manifest.json background 字段
11│   │   └── service-worker.ts
12│   ├── content								# manifest.json content_scripts 字段
13│   │   └── content.ts
14│   ├── contentPage							# iframe 内嵌页面
15│   │   ├── App.css
16│   │   ├── App.tsx
17│   │   ├── components
18│   │   │   ├── TestContent.tsx
19│   │   │   ├── index.css
20│   │   │   └── index.less
21│   │   ├── index.css
22│   │   ├── index.html
23│   │   ├── main.tsx
24│   │   └── store
25│   │       └── store.ts
26│   ├── icons								# 插件 icons 资源
27│   │   └── icon.png
28│   ├── popup								# 插件 popup action 页面
29│   │   ├── App.css
30│   │   ├── App.tsx
31│   │   ├── components
32│   │   │   ├── TestPopup.tsx
33│   │   │   ├── index.css
34│   │   │   └── index.less
35│   │   ├── index.css
36│   │   ├── index.html
37│   │   ├── main.tsx
38│   │   └── store
39│   │       └── store.ts
40│   └── vite-env.d.ts						# 类型声明
41├── tsconfig.json
42├── tsconfig.node.json
43├── vite.config.ts							# vite 配置文件
44└── watch.mjs								# 监听 manifest.json 变化的文件

八、总结

  1. 使用 React、TS、UI AntD 库、Less、状态管理 zustandVite 开发浏览器插件到这整个流程就已经走完了,插件涉及的页面也都包括在内了
  2. 开发上线的时候只需要把 http://127.0.0.1:5500/ 换成插件需要的域名即可
  3. Vite 配置和 React 项目都是我们手动修改的,可以很好的适配自己的项目
  4. 写这个教程趟了不少坑,和 V2 版本很不一样
  5. 完结 🎉🎉🎉

九、Vue3 开发浏览器插件

十、源码地址

个人笔记记录 2021 ~ 2025