Chrome插件学习笔记(三)
参考文章:
1、项目搭建
在前两篇文章中使用的原生js去操作dom,很费劲并且效果并不是很好,于是决定使用vue3来开发浏览器插件
1.1、创建项目
shell
npm create vite@latest
1.2、依赖安装
shell
# @types/node、@types/ws:用来补充类型信息
# chokidar:用来监听文件变化
# chrome-types:chrome相关的类型提示
# rollup-plugin-copy:用于复制manifest.json
# ws:用于创建WebSocket
npm install -D @types/node @types/ws chokidar chrome-types rollup-plugin-copy ws
1.3、vite.config.ts
ts
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from "node:path";
import copy from "rollup-plugin-copy";
// https://vite.dev/config/
export default defineConfig({
server: {
port: 5500,
},
plugins: [
vue(),
copy({
targets: [
{
src: 'src/manifest.json',
dest: path.resolve(__dirname, 'dist')
}
],
hook: 'writeBundle'
}),
],
build: {
outDir: path.resolve(__dirname, 'dist'),
rollupOptions: {
input: {
side_panel: path.resolve(__dirname, 'src/index.html'),
content_script: path.resolve(__dirname, 'src/content_script/content_script.ts'),
service_worker: path.resolve(__dirname, 'src/service_worker/service_worker.ts'),
},
output: {
assetFileNames: 'assets/[name]-[hash].[ext]',
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: (chunkInfo) => {
const facadeModuleId = chunkInfo.facadeModuleId!
const baseName = path.basename(facadeModuleId, path.extname(facadeModuleId))
const saveArr = ['content_script', 'service_worker']
return `[name]/${saveArr.includes(baseName) ? baseName : chunkInfo.name}.js`;
},
name: '[name].js'
}
}
}
})
1.4、manifest.json
json
{
"name": "Assistant",
"description": "A tool for quick conversations with AI",
"version": "0.0.1",
"manifest_version": 3,
"permissions": [
"sidePanel"
],
"host_permissions": [
"*://*/*"
],
"action": {
"default_title": "Click to switch side panel"
},
"side_panel": {
"default_path": "src/index.html"
},
"background": {
"service_worker": "service_worker/service_worker.js",
"type": "module"
},
"content_scripts": [
{
"js": [
"content_script/content_script.js"
],
"matches": [
"*://*/*"
],
"all_frames": true,
"run_at": "document_end",
"match_about_blank": true
}
]
}
1.5、vite-env.d.ts
/// <reference types="vite/client" />
/// <reference types="chrome-types/index" />
1.6、service_worker.ts
ts
console.log("service_worker");
chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true})
1.7、content_script.ts
ts
console.log("content_script");
1.8、index.html
index.html需要移动到src目录下
1.9、目录结构

1.10、测试

2、热部署
热部署的方式很多,如利用额外的插件(可参考文章https://cm-mng.bilibili.co/agent/#/mlogin),这里使用的是socket通信来实现的热部署
2.1、.env.development
VITE_ENV_MODE=development
2.2、 .env.production
VITE_ENV_MODE=production
2.3、file_watcher.ts
ts
import {WebSocketServer} from "ws";
import chokidar from "chokidar";
import path from "node:path";
export function file_watcher(wss: WebSocketServer) {
wss.on('connection', (ws) => {
console.log('Client connected');
// 监听src文件夹下的所有文件,并排出node_modules下所有文件
const watcher = chokidar.watch(path.join(__dirname, 'src'), {ignored: [/(^|[\/\\])\../, /node_modules/]});
// 只有文件变化才会通知,如果设置成all将会通知add、change、addDir等事件
watcher.on('change', (event, path) => {
ws.send(JSON.stringify({type: 'reload', event, path}));
});
ws.on('close', () => {
console.log('Client disconnected');
watcher.close();
});
});
}
2.4、vite.config.ts
ts
import {defineConfig, loadEnv} from 'vite'
import vue from '@vitejs/plugin-vue'
import * as path from "node:path";
import copy from "rollup-plugin-copy";
import {WebSocketServer} from "ws";
import {file_watcher} from "./file_watcher.ts";
// https://vite.dev/config/
export default defineConfig((configEnv) => {
// 这里取的环境变量是.env.xxxxx中的变量,根据加载的环境变量判断是否启动socket服务端,用于通知浏览器插件重新加载
let env = loadEnv(configEnv.mode, process.cwd(), '');
if (env.VITE_ENV_MODE === 'development') {
file_watcher(new WebSocketServer({port: 5501}));
}
return {
server: {
port: 5500,
},
plugins: [
vue(),
copy({
targets: [
{
src: 'src/manifest.json',
dest: path.resolve(__dirname, 'dist')
}
],
hook: 'writeBundle'
}),
],
build: {
outDir: path.resolve(__dirname, 'dist'),
rollupOptions: {
input: {
side_panel: path.resolve(__dirname, 'src/index.html'),
content_script: path.resolve(__dirname, 'src/content_script/content_script.ts'),
service_worker: path.resolve(__dirname, 'src/service_worker/service_worker.ts'),
},
output: {
assetFileNames: 'assets/[name]-[hash].[ext]',
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: (chunkInfo) => {
const facadeModuleId = chunkInfo.facadeModuleId!
const baseName = path.basename(facadeModuleId, path.extname(facadeModuleId))
const saveArr = ['content_script', 'service_worker']
return `[name]/${saveArr.includes(baseName) ? baseName : chunkInfo.name}.js`;
},
name: '[name].js'
}
}
}
}
})
2.5、hot_reload.ts
ts
let timeout: number
export function hot_reload(socket: WebSocket) {
if (timeout) {
clearTimeout(timeout);
}
socket.onopen = () => {
console.log('Connected to dev server')
}
socket.onmessage = (event) => {
const message = JSON.parse(event.data)
if (message.type === 'reload') {
setTimeout(() => {
chrome.runtime.reload()
}, 1000)
}
}
socket.onclose = (event) => {
console.log(`Socket closed: ${event.reason}`);
// 断开连接后5秒后尝试再次连接
timeout = setTimeout(() => {
hot_reload(new WebSocket(`ws://localhost:5501/`))
}, 5000);
};
socket.onerror = (error) => {
console.error('Socket error:', error);
};
}
2.6、service_worker.ts
ts
import {hot_reload} from "../hot_reload.ts";
console.log("service_worker");
chrome.sidePanel.setPanelBehavior({openPanelOnActionClick: true})
// 如果是开发环境才尝试链接socket服务端
if (import.meta.env.VITE_ENV_MODE === 'development') {
console.log('开发环境');
hot_reload(new WebSocket(`ws://localhost:5501/`))
} else {
console.log('生产环境');
}
2.7、package.json
注意这里的scripts需要修改下,开发的时候使用命令npm run watch,修改代码后插件会自动重新加载
json
{
"name": "assistant",
"private": true,
"version": "0.0.0",
"type": "module",
"scripts": {
"dev": "vite",
"watch": "vite build --watch --mode development",
"build": "vue-tsc -b && vite build",
"preview": "vite preview"
},
"dependencies": {
"@types/node": "^24.1.0",
"@types/ws": "^8.18.1",
"chokidar": "^4.0.3",
"chrome-types": "^0.1.363",
"rollup-plugin-copy": "^3.5.0",
"vue": "^3.5.17",
"ws": "^8.18.3"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.0",
"@vue/tsconfig": "^0.7.0",
"typescript": "~5.8.3",
"vite": "^7.0.4",
"vue-tsc": "^2.2.12"
}
}