“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情”
之前有段时间一直在开发chrome
插件,踩了好多的坑,而且问题都很碎片化,所以打算开一个系列来记录一下。在这个系列里会逐步产出两个东西,一个是用于插件开发模板工程,即拉即用,一个是根据模板开发的一款插件,功能是结合我日常工作中比较痛点的一些功能。
PS:为了降低阅读疲劳,单篇幅两千字左右 🚲,看不完就点个赞。
Chrome插件简介 🧩
首先来简单介绍下什么是chrome插件,后面也会穿插一些关于插件的基本知识。
chrome插件又叫crx(chrome extension
),看下图就很明显了,实际上应该叫chrome扩展程序,只是中文表达叫插件更方便。
插件主要能提供了一些浏览器在Web页面之外的能力,用来增强用户体验,比如说对页面字符串进行json格式化,整体页面长截图,React调试工具等等,受众面非常广。
chrome插件的底层代码其实就是前端同学最熟悉的 html + js + css
三剑客,同时再加上一些原生的chrome API
。
manifest.json
这个json文件在插件的目录下是必须要有的,重要性可等同于前端项目里的package.json
,可以理解为是插件的配置清单。
下面就是实际的一个例子,如果是第一次接触不需要细看,后面会结合场景来介绍实际的含义。
{
"name": "插件名",
"version": "0.0.1",
"manifest_version": 3,
"author": "dty6809183@gmail.com",
"description": "TinssonTai的插件模板",
"icons": {
"16": "/assets/dev.png",
"48": "/assets/dev.png",
"96": "/assets/dev.png",
"128": "/assets/dev.png"
},
"permissions": [
"activeTab"
],
"host_permissions": [
"http://*/*",
"https://*/*"
],
"background": {
"service_worker": "/background/index.js"
},
"content_scripts": [
{
"matches": ["<all_urls>"],
"js": ["/contentScript/index.js"],
"run_at": "document_end"
}
],
}
字段会分必填项,推荐项,配置选项,部分字段说明:
字段名 | 含义 | 选项类型 |
---|---|---|
name | 插件名称 | 必填项(Required) |
version | 插件版本号,随开发递增 | 必填项(Required) |
manifest_version | 清单文件版本,2 or 3 | 必填项(Required) |
description | 插件描述 | 推荐项(Recommended) |
icons | 不同位置的图标 | 推荐项(Recommended) |
background | 常驻的后台配置 | 配置选项(Optional) |
permissions | 部分chrome API需要申请的权限 | 配置选项(Optional) |
V3版本问题
这是一张来自官方文档的图片,简单来说就是manifest的版本以后只会支持V3的版本,V3的升级主要是以下几个点:
- background上下文升级为
service worker
- 网络相关API有所变动,需要提前声明权限
- 不能加载远程代码,
script
标签,eval
等 - 多场景支持
Promise
调用
V3和V2的对比(个人观点)
优点:
- 隐私、安全和性能都有所增强
- 更快的审核时间和更高的通过率
缺点:
- 无法加载远程代码导致灵活性下降
- 权限控制严格,开发者上手成本增加
PS:chrome 88才开始对v3的支持
技术方案 🧰
既然底层的技术栈是前端三剑客,那么就能运用现代前端技术来进行构造,经过调研,最终选择了vite
+ vue3
的方案。
why not webpack?
webpack
在构建web应用的时候非常好,有很多成熟的解决方案,但是在chrome插件V3版本的场景下会有一些麻烦的边际问题要处理,如果是V2版本的插件我还是会推荐webpack,索性就直接用vite的build打包更加轻量。
vite + vue3
- 安装vite + vue3 + ts
pnpm i -D vite typescript vue @vitejs/plugin-vue
- 设置vite.config.ts
import { defineConfig } from 'vite'
import Vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export const r = (...args: string[]) => resolve(__dirname, '.', ...args)
export const commonConfig = {
root: r('src'),
plugins: [
Vue()
],
}
export default defineConfig({
...commonConfig,
build: {
watch: {},
cssCodeSplit: false,
emptyOutDir: false,
sourcemap: false,
outDir: r('local'),
rollupOptions: {
input: {
background: r('src/background/index.ts'),
},
output: {
entryFileNames: '[name]/index.js',
extend: true,
format: 'iife'
},
},
},
})
可以看到在config中主要是build的配置,这是因为在输出目标应用的时候我们需要生成真实存在的文件,不走dev模式下的koa代理,为了便于开发,默认开启watch,并关闭css代码分块。
这里生成了一个background/index.js
文件,主要的作用后面会详细介绍。
- 再加个vite.content.config.ts
import { defineConfig } from 'vite'
import { r, commonConfig } from './vite.config'
import { replaceCodePlugin } from 'vite-plugin-replace'
// bundling the content script
export default defineConfig({
...commonConfig,
build: {
watch: {},
cssCodeSplit: false,
emptyOutDir: false,
sourcemap: false,
outDir: r('local/contentScript'),
rollupOptions: {
input: {
contentScript: r('src/contentScript/index.ts'),
},
output: {
assetFileNames: '[name].[ext]',
entryFileNames: 'index.js',
extend: true,
format: 'iife'
},
},
}
})
这个配置主要是为了单独输出contentScript/index.js
文件,是插件注入页面的一段js文件。
在模板仓库里contentScript
对应的功能就如下图所示,在所有页面的右上角增加一个具备弹窗的按钮。
- tsconfig配置如下
{
"compilerOptions": {
"baseUrl": ".",
"module": "ESNext",
"target": "es2016",
"lib": ["DOM", "ESNext"],
"strict": false,
"esModuleInterop": true,
"incremental": false,
"skipLibCheck": true,
"jsx": "preserve",
"moduleResolution": "node",
"resolveJsonModule": true,
"noUnusedLocals": true,
"forceConsistentCasingInFileNames": true,
"types": [
"vite/client",
"element-plus/global"
],
"paths": {
"~/*": ["src/*"]
}
},
"exclude": ["dist", "node_modules"]
}
额外引了vite
和element-plus
的type定义
- 监听manifest.json + 静态文件
const fs = require('fs-extra')
const chokidar = require('chokidar')
const path = require('path')
const { resolve } = path
const r = (rootPath) => resolve(__dirname, '..', rootPath)
const origin = {
manifest: r('src/manifest.json'),
assets: r('src/assets')
}
const target = {
manifest: r('local/manifest.json'),
assets: r('local/assets')
}
const copuManifest = () => {
fs.copy(origin.manifest, target.manifest)
}
copuManifest()
const copyAssets = () => {
fs.copy(origin.assets, target.assets)
}
copyAssets()
// 监听文件变化,同步至插件根目录
chokidar.watch([origin.manifest])
.on('change', () => {
copuManifest()
})
这个脚本会监听mainifest.json
文件,有变化就copy进目标目录下,同时也把assets里的静态文件复制过去。
chokidar
:轻量跨平台文件监听工具
fs-extra
:node-fs包的扩展
初步架构
目前初步的架构如上图所示,会利用npm-run-all
同时启动三个进程运行:
- 监听部分静态文件变化,直接copy进目标目录
vite.config.ts
默认输出backgroudn 和 popup相关vite.content.config.ts
单独处理contentScripts(有一些坑下面会提到)
为了更好理解仓库,贴上package.json文件:
{
"name": "vite-crx-template",
"version": "0.0.1",
"description": "Simple Chrome Extension Vite Starter Template",
"scripts": {
"dev": "npm run clear && run-p dev:*",
"dev:code": "vite build",
"dev:content": "vite build --config vite.content.config.ts",
"dev:json": "node scripts/monitor.js",
"clear": "rimraf local"
},
"author": "TinssonTai",
"devDependencies": {
"@types/node": "^18.7.17",
"@vitejs/plugin-vue": "^3.1.0",
"chokidar": "^3.5.3",
"fs-extra": "^10.1.0",
"npm-run-all": "^4.1.5",
"rimraf": "^3.0.2",
"typescript": "^4.8.3",
"vite": "^3.1.0",
"vite-plugin-replace": "^0.1.1",
"vue": "^3.2.39"
},
"dependencies": {
"element-plus": "^2.2.16"
}
}
遇坑 🚧
Css样式冲突
contentScript
可以理解为插件注入到页面的一段js,如果想要写一些功能肯定会涉及到css样式,比如上面提到的按钮弹窗。
如果直接在body底下注入一段div是有可能被页面原本的全局样式影响的,比如页面里的css直接作用于body下所有元素。
解决方案:
利用shadow DOM
的样式隔离来避免全局样式污染,可以把shadow DOM
视为“DOM中的DOM”,内层dom完全是独立的样式空间。
import { createApp } from 'vue'
import ElementPlus from 'element-plus'
import 'element-plus/dist/index.css'
import App from './App.vue'
(() => {
const container = document.createElement('div')
const root = document.createElement('div')
const styleEl = document.createElement('link')
const shadowDOM = container.attachShadow?.({ mode: 'open' }) || container
styleEl.setAttribute('rel', 'stylesheet')
styleEl.setAttribute('href', chrome.runtime.getURL('contentScript/style.css'))
shadowDOM.appendChild(styleEl)
shadowDOM.appendChild(root)
document.body.appendChild(container)
const app = createApp(App)
app.use(ElementPlus)
app.mount(root)
})()
上面代码就是把vue3创建出来的App实例挂载在shadow DOM
下。
:root下color变量var不生效
通过上述的代码jym会发现引入了ElementPlus
,这时候如果直接用的话,会出现下图的情况,颜色变量完全不生效
原因:
打包出来的css 颜色变量其实是挂载在:root
下,这个可以理解是页面的根节点,但是处于shadow DOM样式隔离的情况下无法生效。
解决方案:
把所有:root
替换成:host
就能改变css颜色变量的挂载节点,这时候在vite的config文件下增加一个replace插件进行全局替换
import { defineConfig } from 'vite'
import { replaceCodePlugin } from 'vite-plugin-replace'
// bundling the content script
export default defineConfig({
...commonConfig,
plugins: [
replaceCodePlugin({
replacements: [
{
from: /:root{/g,
to: ':host{'
}
]
})
]
})
最后贴上当前模板的git仓库:
结语
目前chrome插件v3版本的文章不是很多,有很多坑需要去踩,本系列也会持续更新,模板仓库提供的能力也会跟着文章持续迭代。
创造不易,希望jym多多 点赞 + 关注 二连,持续更新中!!!
PS: 文中有任何错误,欢迎jym指正
往期精彩📌
参考: