Vue3 浏览器插件开发脚手架推荐:vitesse-webext 全解析

646 阅读9分钟

💡 【1】写在前面

接触并学习Vue已经很久了,经历了其从V2到V3的演进,也或多或少做过一些项目,但都是以web或小程序为主,最近在PC端浏览器中使用沉浸式翻译插件挺感慨的,软件开发并不是只有大而全,小而实用的应用一样可以很成功。那么有没有使用Vue做小而实用的浏览器插件开发的可靠方案呢?经过摸索及试用,(相比原生)用Vue进行chrome extension v3开发意外丝滑,本文以系列实战的方式展开说明。

💡 【2】vitesse-webext脚手架

开源时代做应用开发从0~1搭建项目就太费时费力了,学会站在巨人肩膀有时会取得事半功倍的效果。偶然发现了一个宝藏开源项目vitesse-webext,项目作者是大名鼎鼎的Anthony Fu,Anthony Fu我相信做前端的应该不陌生,神一般的存在。目前vitesse-webext在Github上有3.1K star、230+ fork,当然相比作者的名气,项目本身稍显逊色,但人气不影响项目本身的实力。

image.png

项目作者Anthony Fu Github主页:github.com/antfu

vitesse-webext Github地址:github.com/antfu-colle…

🔔 - 项目特点

  • Vite+热重载,建议搭配chrome插件Extensions Reloader一起使用(下文介绍如何使用)
  • TypeScript支持
    • 类型安全
    • 插件开发本身API类型支持
  • Vue3开发
    • 支持组合式API、支持setup语法
    • 核心模块/API免导入
    • 组件自动导入
  • 集成UnoCSS:预设presetUno()、presetAttributify()、presetIcons()等
    • 支持CSS类Tailwindcss语法,如class="block font-bold mb-2.5"
    • 支持属性分组类样式,如文本样式单独设置:text="[14px] gray-500"
    • 集成Icon图标iconify图标集查询到所需图标后组件式配置,动态加载生效,如<eva-cloud-download-outline class="text-white text-[18px] pl-2.5 p-y-1"/>
  • 插件dev功能集成
    • 插件页面content、popup、sidepanel等相关功能页面开发支持使用Vue3编码
    • manifest ts类型支持,manifest动态加载
    • 支持Chrome及Firefox同构,一次代码两处可用
    • 支持content shadow配置
    • 集成webext-bridge,简化各端通信
    • 集成storage及状态共享

🔔 - 项目创建及初始化

🎈 >> 项目创建及初始化

npx degit antfu/vitesse-webext vitesse-webext-demo
cd vitesse-webext-demo
pnpm i // 项目初始化

# 修改后会自动重新加载(大部分情况下修改后会自动同步,如果不行,使用Extensions Reloader插件辅助)

🎈 >> 增加chrome类型支持

  • 依赖安装
pnpm install --save-dev @types/chrome
  • 找到tsconfig.json增加如下配置项
...
{
  "compilerOptions": {
    "types": ["chrome"]
  }
}
...

🔔 - 项目工程结构(src核心代码目录说明)

  • src\manifest.ts:manifest配置文件
  • src\assets目录:主要存放插件自身的图标,名称自定义,chrome插件建议考虑涵盖的尺寸16*16、32*32、48*48、128*128、256*256、512*512,制作方法详参文末

image.png

+** src\background目录**:插件开发background相关代码 + src\background\contentScriptHMR.ts:在页面导航时动态注入脚本,确保内容脚本能够正常运行。兼容Firefox。 + src\background\main.ts:background核心代码,支持导入自定义类型、hook等

  • src\components目录:自定义组件,自动导入

  • src\composables目录:hooks钩子目录,存放封装好的hook,支持Vue3内置组件或第三方库包如useVue调用

  • src\contentScripts目录:插件content代码相关目录

    • src\contentScripts\views\App.vue如涉及视图元素插入页面如悬浮按钮在此处开发(使用Vue3),支持消息监听,支持第三方库包如useVue调用
    • src\contentScripts\index.ts:content shadwnDom配置、content视图组件全局挂载等在此处配置
  • src\logic目录:与UI代码无关的业务代码封装及存放如状态共享、storage、登录状态管理等

  • src\options目录:插件配置页面,Vue3开发

  • src\popup:插件popup页面,Vue3开发

  • src\sidepanel:插件侧边栏开发,Vue3开发

  • src\styles:popup 和 options 共享的样式

# 仅供参考

D:\CHROMEPROJECT\JLOAD\SRC
│  auto-imports.d.ts      
│  components.d.ts        
│  constants.ts
│  env.ts
│  global.d.ts
│  manifest.ts
│  types.ts
│
├─assets
│      icon-128.png
│      icon-16.png
│      icon-256.png
│      icon-32.png
│      icon-48.png
│      icon-512.png
│      logo.svg
│
├─background
│      contentScriptHMR.ts
│      main.ts
│
├─components
│  │  README.md
│  │  SharedSubtitle.vue
│  │
│  └─__tests__
│          Logo.test.ts
│
├─composables
│      useWebExtensionStorage.ts
│
├─contentScripts
│  │  index.ts
│  │
│  └─views
│          App.vue
│
├─logic
│      common-setup.ts
│      index.ts
│      storage.ts
│
├─options
│      index.html
│      main.ts
│      Options.vue
│
├─popup
│      index.html
│      main.ts
│      Popup.vue
│
├─sidepanel
│      index.html
│      main.ts
│      Sidepanel.vue
│
├─styles
│      index.ts
│      main.css
│
├─tests
│      demo.spec.ts
│
└─utils
        common.ts

🔔 - 开发调试编译

  • pnpm管理

    • 依赖引入:pnpm add [packagename]
    • 开发调试:pnpm dev
    • 编译打包:pnpm build
  • 编译打包输出路径:extension

image.png

image.png

image.png

🔔 - 积累、Tips及坑

🎈 >> 各类尺寸插件图标制作方法

  • Figma制作:使用IOS模式导出,可选尺寸会比较灵活,参考尺寸如下
  • 16*16
  • 32*32
  • 48*48
  • 128*128
  • 512*512

image.png

  • 免费网站制作图标工厂在自定义中可以添加想要的尺寸,支持免费导出,但是有广告。

image.png

🎈 >> 添加依赖

🔑 以引入vue-sonner(优雅Toast)为示例
  • 依赖添加:pnpm add vue-sonner
  • 找到src\contentScripts\index.ts,增加如下配置
/* eslint-disable no-console */
import { createApp } from 'vue'
import App from './views/App.vue'
import { setupApp } from '~/logic/common-setup'
import { Toaster } from 'vue-sonner';



// Firefox `browser.tabs.executeScript()` requires scripts return a primitive value
(() => {
  // mount component to context window
  const container = document.createElement('div')
  container.id = __NAME__
  const root = document.createElement('div')
  const styleEl = document.createElement('link')
  const shadowDOM = container.attachShadow?.({ mode: __DEV__ ? 'open' : 'closed' }) || container
  styleEl.setAttribute('rel', 'stylesheet')
  styleEl.setAttribute('href', browser.runtime.getURL('dist/contentScripts/style.css'))
  shadowDOM.appendChild(styleEl)

  shadowDOM.appendChild(root)
  document.body.appendChild(container)

  const app = createApp(App)


  // 挂载 vue-sonner 的 Toaster 组件到全局 document.body
  const toasterContainer = document.createElement('div');
  toasterContainer.id = 'vue-sonner-container';
  document.body.appendChild(toasterContainer);

  const toasterApp = createApp({
    render: () => h(Toaster, { richColors: true, position: 'top-center' }),
  });
  toasterApp.mount(toasterContainer);


  setupApp(app)
  app.mount(root)
})()
  • src\contentScripts\views\App.vue中应用
import { toast } from 'vue-sonner'

const toastCustomStyle = 'min-h-[36px] min-width-[68px] px-[24px] py-[18px]'

toast(NotificationMessage.COMMITTING, {
    class: toastCustomStyle,
    duration: 5000
  })

🎈 >> UnoCSS预先设置

🔑 品牌色定义(示例)
  • 找到unocss.config.ts,找到theme/colors/brand增加如下配置
import { defineConfig } from 'unocss/vite'
import { presetAttributify, presetIcons, presetUno, transformerDirectives } from 'unocss'

export default defineConfig({
  presets: [
    presetUno(),
    presetAttributify(),
    presetIcons(),
  ],
  transformers: [
    transformerDirectives(),
  ],
  theme: {
    colors: {
      brand: {
        DEFAULT: '#FF9155',    // class="bg-brand"
        secondary: '#FFB773',   // class="bg-brand-secondary"
      },
    }
  },
  shortcuts: {
    'flex-row-between': 'flex flex-row items-center justify-between',
    'flex-row-center': 'flex items-center justify-center',
    'box-shadow': 'shadow-[0px_4px_4px_rgba(0,0,0,0.25)]',
    'flex-col-center': 'flex flex-col justify-center'
  },
})

🔑 自定义色阶(示例)
  • 找到unocss.config.ts,找到theme/colors/gray(自定义自己的灰色色阶)(与预设重叠因优先级最高会覆盖)增加如下配置
import { defineConfig } from 'unocss/vite'
import { presetAttributify, presetIcons, presetUno, transformerDirectives } from 'unocss'

export default defineConfig({
  presets: [
    presetUno(),
    presetAttributify(),
    presetIcons(),
  ],
  transformers: [
    transformerDirectives(),
  ],
  theme: {
    colors: {
      gray: {
        DEFAULT: '#BFBFBF', // class="bg-gray"
        '100': '#EAEAEA',// class="bg-gray-100"
        '200': '#BFBFBF',// class="bg-gray-200"
        '300': '#959595',// class="bg-gray-300"
        '400': '#6A6A6A',// class="bg-gray-400"
        '500': '#404040',// class="bg-gray-500"
        '600': '#151515',// class="bg-gray-600"
      }
    }
  },
  shortcuts: {
    'flex-row-between': 'flex flex-row items-center justify-between',
    'flex-row-center': 'flex items-center justify-center',
    'box-shadow': 'shadow-[0px_4px_4px_rgba(0,0,0,0.25)]',
    'flex-col-center': 'flex flex-col justify-center'
  },
})

🔑 以自定义shortcut为例
  • shortcut的效果是组合多个生成新的类,找到unocss.config.ts,找到shortcuts(与预设重叠因优先级最高会覆盖)增加如下配置
import { defineConfig } from 'unocss/vite'
import { presetAttributify, presetIcons, presetUno, transformerDirectives } from 'unocss'

export default defineConfig({
  presets: [
    presetUno(),
    presetAttributify(),
    presetIcons(),
  ],
  transformers: [
    transformerDirectives(),
  ],
  ...
  shortcuts: {
    'flex-row-between': 'flex flex-row items-center justify-between',
    'flex-row-center': 'flex items-center justify-center',
    'box-shadow': 'shadow-[0px_4px_4px_rgba(0,0,0,0.25)]',
    'flex-col-center': 'flex flex-col justify-center'
  },
})

🔑 UnoCSS group分组应用
  • 分组标记方便对子元素特定行为进行操作也即方便精确控制子元素指定样式属性,group-hover 表示当父容器 .group 被 hover 时,子元素的 text-red-500 类会被应用,即文本颜色变成红色。
<template>
  <div class="group p-8 bg-gray-200">
    <!-- 当父元素被 hover 时,子元素的背景色会变成红色 -->
    <span class="text-gray-700 group-hover:text-red-500 transition-all duration-300">
      Hover me to change color!
    </span>
  </div>
</template>

🎈 >> 图标调用

  • iconify图标集图标集找到想要的图标,图标默认以i开头,有i无i都可以生效,如solar-star-brokeni-solar-star-broken

image.png

🎈 >> webext-bridge部件的使用

其核心是减少麻烦并简化在扩展的不同部分之间保持数据同步的工作。 webext-bridge 是一个微型库,它提供了一个简单且一致的 API,用于在 Web 扩展的不同部分(如 background, content-script, devtools, popup, options, and window contexts.)之间收发消息。

  • 常用函数签名及示例
/** 
 * 发送消息 
 * + messageId:通常将其设置为大写 enum 或 string 值,见名知义
 * + data:具体的JSON数据
 * + destination 是消息发送到的位置,例如 `background`、`popup`、`content-script@{tabId}` 等
 * + 类型签名:destination?: Destination
 *  + 可以是{ context: 'content-script', tabId }
 *  + 可以是字符串,如"background"
 *  + 可以是拼合字符串:"content-script@"+tabs[0].id)
 */
sendMessage(messageId,data,destination)

// 示例:popup sendToBackground
import { sendMessage } from "webext-bridge/popup";

const sendToBackground = async () => {
    await sendMessage("RECORD_NAME", {
        first_name: 'John',
        last_name: 'Doe'
    }, "background");
}



/** 
 * 监听消息/接收消息
 * + messageId:正在侦听的消息的 ID。这与 `sendMessage()` 方法中的 `messageId` 匹配
 * + `callback`:用于处理方法的回调函数
 */
onMessage(messageId,callback)


// 示例:backgound onMessage
import { onMessage } from "webext-bridge/background";

onMessage( "RECORD_NAME", recordName );
async function recordName( {data} ){
    // Do whatever processing you need here. 
    return {
        // Some response here
    }; 
}

🎈 >> vite.config.mts预配置

  • 支持使用~import { delay, getPageUrl } from '~/utils/common'导入,因为vite.config.mts作了如下配置
...
root: r('src'),
resolve: {
alias: {
  '~/': `${r('src')}/`,
},
},
...
  • 支持组件自动导入无需手动导入,支持UnoCSS,支持图标自动下载,因为vite.config.mts作了如下配置
...
plugins: [
    Vue(),

    AutoImport({
      imports: [
        'vue',
        {
          'webextension-polyfill': [
            ['=', 'browser'],
          ],
        },
      ],
      dts: r('src/auto-imports.d.ts'),
    }),

    // https://github.com/antfu/unplugin-vue-components
    Components({
      dirs: [r('src/components')],
      // generate `components.d.ts` for ts support with Volar
      dts: r('src/components.d.ts'),
      resolvers: [
        // auto import icons
        IconsResolver({
          prefix: '',
        }),
      ],
    }),

    // https://github.com/antfu/unplugin-icons
    Icons(),

    // https://github.com/unocss/unocss
    UnoCSS(),

    // rewrite assets to use relative path
    {
      name: 'assets-rewrite',
      enforce: 'post',
      apply: 'build',
      transformIndexHtml(html, { path }) {
        return html.replace(/"\/assets\//g, `"${relative(dirname(path), '/assets')}/`)
      },
    },
  ],
...

💣 >> 【坑】首次dev或者build后更改icon图标不生效

  • 存入src\assets并不会自动同步到extension\assets,需要手动将图标存入extension\assets目录

💣 >> 【坑】特定网站修改rem值导致UnoCSS样式失控

  • UnoCSS默认rem单位,特定网站会修改rem值导致样式失控,可以通过修改页面样式、px或者vw/vh替换默认类名如w-[24px]等方式解决

综上我们发现,vitesse-webext功能完善、高度集成,极大地缩减了vite及chrome extension配置,解放双手,对开发效率提升助益很大。

💡 【3】Demo演示(部分)

  • popup页面

image.png

  • options页面

image.png

  • centent注入页面

image.png

💡 【4】写在后面

其实关注Vue3开发chrome extension v3插件有一段时间了,手动配置也能实现就是稍显复杂也没有那么充裕的时间去摸索,现成的脚手架对于提升开发效率帮助挺大,当然也有一些其他的开源项目如vite-vue3-browser-extension-v3(集成的功能全到望而却步)山高路远后续再试。本文是Vue实战Chrome插件开发的开篇,可以理解为脚手架的使用指南,后续会以系列实战的方式持续更新,有任何问题欢迎交流。