likes
comments
collection
share

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

作者站长头像
站长
· 阅读数 17

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

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

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

Chrome插件简介 🧩

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

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

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

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

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

这是一张来自官方文档的图片,简单来说就是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
pnpm i -D vite typescript vue @vitejs/plugin-vue
  1. 设置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文件,主要的作用后面会详细介绍。

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

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

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

额外引了viteelement-plus的type定义

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

初步架构

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

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

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

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

原因:

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

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

解决方案:

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

github.com/Tinsson/vit…

结语

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

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

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

往期精彩📌

参考:

github.com/antfu/vites…

segmentfault.com/a/119000001…

cn.vitejs.dev/config/buil…

转载自:https://juejin.cn/post/7143439418215530509
评论
请登录