一、使用 Vite
创建 React
项目
1npm create vite@latest # npm
2yarn create vite # yarn
3pnpm create vite # pnpm
选择 React
和 TS
进入项目,并进行 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. 项目修改
- 删除项目根目录下的
index.html
文件 - 删除
src
目录下的App.tsx、main.tsx、App.css、index.css
- 删除根目录下的
public
文件夹 - 在根目录下创建
manifest.json
文件,此乃插件入口文件 - 创建
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
文案 - 创建
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
文案 - 创建
background
:在src
目录下创建background
文件夹,background
文件夹中创建service-worker.ts
,文件里面写入console.log('background service-worker file')
- 创建
content
:在src
目录下创建content
文件夹,content
文件夹下创建content.ts
,文件写入console.log('content file')
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. 解析
manifest_version
字段一定得是 3- 因为只有一张图,所以
icons
和action/default_icon
就用同一张图片 - 可以看到
action/default_popup
配置的值为popup/index.html
,是因为我想把项目build
成这种路径 background/service_worker
也是同理,要build
成background/service-worker.js
这种路径- 通了
content_scripts
配置也是一样,build
成content/content.js
match
配置了一个本地的路径,方便调试(使用vscode
的Live Server
插件或者node
包启动一个服务)- 如果你想在哪个页面显示就配置哪个页面的域名即可
2. 配置 Chrome
插件的 Types
因为我们使用的是 TypeScripts
来进行开发 Chrome
插件,所以需要配置一个 Chrome
插件 API
等 Types
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.ts
和service-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.json
中content_script
的all_frames
为true
,这个代表着我们的content.ts
会注入所有的frames
中,加一个这个是在判断top
与self
相等的时候在注入
- 因为我们的
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
页面使用图片
- 直接引入,
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}
- 重新
build
项目,刷新插件,刷新页面,点击popup action
,弹出popup
页面
1.2. Content
页面使用图片
- 直接引入,
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}
- 重新
build
项目,刷新插件,刷新页面
2. 使用 UI
库
以 Ant Design
为例
2.1. 安装 Ant Design
包
1pnpm i antd
2.2. Popup
页面使用
- 直接引入,
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
- 重新
build
项目,刷新插件,刷新页面,点击popup action
,弹出popup
页面
2.3. Content
页面使用
- 直接引入,
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
- 重新
build
项目,刷新插件,刷新页面
3. 状态管理 Zustand
Zustand 是一个简单而强大的状态管理库,它提供了一个小巧的 API,可以让你轻松地管理组件的状态。 Zustand 的 API 简单而直观,适用于小型到中型的应用。
3.1. 安装 zustand
1pnpm i zustand
3.2. Popup
页面使用
- 在
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
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
- 在
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
- 重新
build
,刷新插件,刷新页面,点击popup action
弹出页面,点击按钮操作
3.3. Content
页面使用
- 在
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
counter.ts
和popup
中的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
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
- 重新
build
,刷新插件,刷新页面
4. 使用 CSS
预处理器
Vite
同时提供了对.scss, .sass, .less, .styl
和.stylus
文件的内置支持
因为 vite
内置支持,所以只需要安装依赖就行
4.1. 安装 less
1pnpm i less -D
4.2. Popup
页面使用
TestPopup.tsx
中加入以下代码
1<div className="test-popup">
2 <ul>
3 <li>popup</li>
4 </ul>
5</div>
components
新建index.less
文件
1src/popup/components
2├── TestPopup.tsx
3└── index.less
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}
TestPopup.tsx
中引入index.less
1import './index.less'
Popup
页面展示
4.3. Content
页面使用
TestContent.tsx
中加入以下代码
1<div className="test-content">
2 <ul>
3 <li>content</li>
4 </ul>
5</div>
components
新建index.less
文件
1src/contentPage/components
2├── TestContent.tsx
3└── index.less
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}
TestContent.tsx
中引入index.less
1import './index.less'
Content
页面展示
六、热加载
1. 只有 Popup
页面和 Content
页面需要热加载
如果我们的 manifest.json
文件基本上固定的,不需要更新,只需要 popup
页面和 content
页面在保存的时候进行 build
以及刷新的话,有一种很简单的方式
1.1. 我们本地启动的项目和我们插件的项目在同一个文件夹
这样的话,当我们点击保存的时候,会自动触发刷新页面
1.2. 配置新的 build script
命令,监听文件更新,重新 build
"watch-build": "vite --watch build"
- 终端启动
- 更新
popup
文件夹和content
文件夹下的内容即可
1pnpm run watch-build
- 我的本地项目启动之后域名为:
http://127.0.0.1:5500/testhtml/test.html
- 目录为:
/User/demo/chrome/testhtml/test.html
- 插件根目录为:
/Users/demo/chrome/test-chrome/react-chrome-ext-pro
- 我在
chrome
这一层启动live-server
服务,这个可以自动实现热加载 - 配置
build
监听命令是为了保存的时候可以重新build
,再配合刷新的话,这样就不用手动刷新插件和页面了 - 新更改的
popup
页面和content
页面也能及时的显示出来
添加 watch-build
文案,不需要刷新插件也可显示出新页面
popup 页面
Content 页面
2. 插件模块热加载(background
的 service-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})
- 第一行日志输出
- 第 2~3 两行是判断当开发环境为
development
时,才会走以下流程 - 第 5~11 行是重新在当前插件的根目录启动一个 live-server 服务,写入需要监听的文件列表,然后可以通过 fetch 请求的方式获取文件内容(一定要起个服务才行,而且监听文件的 URL 要能正确访问才行,如果 fileList 字段和你的不匹配需要修改才行)
- 第 13~15 行是定义一个对象,
key
就是文件列表的路径 - 第 19~33 行是刷新插件和刷新当前
tab
页面 - 第 38~52 行是遍历文件列表,通过
fetch
请求获取文件内容,进行判断是否和fileObj
中保存的数据是否一致,如果不一致则进行reload
- 第 54~56 行是定义一个
setInterval
,间隔多少时间进行遍历文件内容去判断 - 第 62~77 行是用
Chrome
的alarms
来当定时器(建议)
2.2. 配置 Manifest.json
文件
因为使用了一些 Chrome
的 API
,所以需要添加权限才行
1"permissions": [
2 "activeTab",
3 "tabs",
4 "alarms"
5],
2.3. 配置 build script
命令,监听文件更新,重新 build
"watch-build": "vite --watch build"
- 终端启动
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});
- 使用
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 变化的文件
八、总结
- 使用
React、TS、UI AntD
库、Less
、状态管理zustand
、Vite
开发浏览器插件到这整个流程就已经走完了,插件涉及的页面也都包括在内了 - 开发上线的时候只需要把
http://127.0.0.1:5500/
换成插件需要的域名即可 Vite
配置和React
项目都是我们手动修改的,可以很好的适配自己的项目- 写这个教程趟了不少坑,和
V2
版本很不一样 - 完结 🎉🎉🎉