加载中...

Chrome插件踩坑日志(一)Vite + Vue3

Chrome插件踩坑日志(一)Vite + Vue3

“我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第2篇文章,点击查看活动详情

之前有段时间一直在开发chrome插件,踩了好多的坑,而且问题都很碎片化,所以打算开一个系列来记录一下。在这个系列里会逐步产出两个东西,一个是用于插件开发模板工程,即拉即用,一个是根据模板开发的一款插件,功能是结合我日常工作中比较痛点的一些功能。

PS:为了降低阅读疲劳,单篇幅两千字左右 🚲,看不完就点个赞。

Chrome插件简介 🧩

首先来简单介绍下什么是chrome插件,后面也会穿插一些关于插件的基本知识。

chrome插件又叫crx(chrome extension),看下图就很明显了,实际上应该叫chrome扩展程序,只是中文表达叫插件更方便。

New Image

插件主要能提供了一些浏览器在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版本问题

New Image

这是一张来自官方文档的图片,简单来说就是manifest的版本以后只会支持V3的版本,V3的升级主要是以下几个点:

  • background上下文升级为service worker
  • 网络相关API有所变动,需要提前声明权限
  • 不能加载远程代码,script标签,eval
  • 多场景支持Promise调用

文档链接

V3和V2的对比(个人观点)

优点:

  1. 隐私、安全性能都有所增强
  2. 更快的审核时间和更高的通过率

缺点:

  1. 无法加载远程代码导致灵活性下降
  2. 权限控制严格,开发者上手成本增加

PS:chrome 88才开始对v3的支持

技术方案 🧰

既然底层的技术栈是前端三剑客,那么就能运用现代前端技术来进行构造,经过调研,最终选择了vite + vue3的方案。

why not webpack?

webpack在构建web应用的时候非常好,有很多成熟的解决方案,但是在chrome插件V3版本的场景下会有一些麻烦的边际问题要处理,如果是V2版本的插件我还是会推荐webpack,索性就直接用vite的build打包更加轻量

vite + vue3

  1. 安装vite + vue3 + ts
css
复制代码
pnpm i -D vite typescript vue @vitejs/plugin-vue
  1. 设置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文件,主要的作用后面会详细介绍。

  1. 再加个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对应的功能就如下图所示,在所有页面的右上角增加一个具备弹窗的按钮。

New Image

  1. 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"] }

额外引了viteelement-plus的type定义

  1. 监听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包的扩展

初步架构

New Image

目前初步的架构如上图所示,会利用npm-run-all同时启动三个进程运行:

  1. 监听部分静态文件变化,直接copy进目标目录
  2. vite.config.ts默认输出backgroudn 和 popup相关
  3. 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,这时候如果直接用的话,会出现下图的情况,颜色变量完全不生效

New Image

原因:

打包出来的css 颜色变量其实是挂载在:root下,这个可以理解是页面的根节点,但是处于shadow DOM样式隔离的情况下无法生效。

New Image

解决方案:

把所有: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仓库:

github.com/Tinsson/vit…

结语

目前chrome插件v3版本的文章不是很多,有很多坑需要去踩,本系列也会持续更新,模板仓库提供的能力也会跟着文章持续迭代。

创造不易,希望jym多多 点赞 + 关注 二连,持续更新中!!!

PS: 文中有任何错误,欢迎jym指正

往期精彩📌

参考:

github.com/antfu/vites…

segmentfault.com/a/119000001…

cn.vitejs.dev/config/buil…