“我报名参加金石计划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,可以理解为是插件的配置清单。
下面就是实际的一个例子,如果是第一次接触不需要细看,后面会结合场景来介绍实际的含义。
perl复制代码{ "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
css复制代码pnpm i -D vite typescript vue @vitejs/plugin-vue
- 设置vite.config.ts
php复制代码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
php复制代码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配置如下
json复制代码{ "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 + 静态文件
scss复制代码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文件:
perl复制代码{ "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完全是独立的样式空间。
javascript复制代码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插件进行全局替换
javascript复制代码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指正
往期精彩📌
参考: