【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)

内容分享2小时前发布 randymu
0 0 0

目录

第一章 前言

第二章 油猴头部标签全解

2.1 基本身份信息

2.2 自动更新地址

2.3 是否生效页面(include/exclude/match)

2.3.1 原理

2.3.2 语法粒度和匹配规则

2.3.3 优先级顺序

2.3.4 油猴设置面板分别对应字段

2.3.5 最佳实践速记

2.3.6 总结

2.4 跨域白名单

2.4.1 原理

2.4.2 设置面板对应的字段

2.5 能力清单

2.6 总结

第三章 搭建基础骨架

3.1 项目创建

3.2 油猴配置(meta.json)

3.3 项目配置(vite.config.js)

3.4 数据持久化(GM_setValue与GM_getValue )

3.4.1 原理

3.4.2 使用方法

3.4.3 作用域与生命周期

3.4.4 用法示例

3.4.5 与 localStorage 的对比

3.4.6 注意事项

3.4.7 实战使用

 3.5 与后端联调:GM_xmlhttpRequest Promise 化

3.6 不同环境的运行方式

3.7 自动灰度更新(可选)

3.8 审计 & 安全 checklist(提交前打 √):针对打包后的

第四章 实战例子

第五章 源代码地址

入门看小编下面这篇文章:

【谷歌脚本开发】Tampermonkey插件 零基础入门(附源码)

第一章 前言

经过前面一篇基础入门的文章相信大家已经知道了Tampermonkey插件的使用方法了;那么接下来小编就开始带领大家实现一个插件项目的开发,同时使用Vue3+vite做一个项目(注:虽然我们用的是vue3,但是插件的本身就是吃原生技能,大家不要把基础丢掉了)!!通过该篇文章,能带领小编以及大家走到哪里呢?

起点 终点
只会写 alert('hello') 产出可协作、可灰度、可审计的 Vue3 单页插件
只会“板砖” 清楚插件的的各个涵义
全局变量满天飞 用 GM_setValue / GM_getValue 做 加密持久化

如何对接接口

用 GM_request 封装出可用的接口
不同环境的运行方式 配置多个命令实现不同环境的切换
复制粘贴发版本 一条命令 npm run build 自动出 .user.js + CI 灰度

等等 …

第二章 油猴头部标签全解

首先把下面这段油猴(Tampermonkey/Greasemonkey)脚本的“头部”逐行拆开讲清楚,基本涵盖了大部分。大家只要记住:所有以// @开头的行都是“元数据指令”,它们不会被执行,而是告诉脚本管理器“我是谁、我能干什么、要在哪些页面跑、能访问哪些资源”。



// ==UserScript==
// @name         test
// @namespace    npm/vite-plugin-monkey
// @description  这是一个脚本,加点描述
// @author       VE
// @version      20251212.1751
// @icon         https://vitejs.dev/logo.svg 
// @downloadURL  http://x.xx.xxx.xxx/monkey/test.js 
// @updateURL    http://x.xx.xxx.xxx/monkey/test.js 
// @include      *baidu.com*
// @connect      *https://www.baidu.com/ *
// @connect      127.0.0.1
// @connect      localhost
// @connect      192.168.124.xxx
// @grant        GM.addElement
// @grant        GM.addStyle
// @grant        GM.addValueChangeListener
// @grant        GM.cookie
// @grant        GM.deleteValue
// @grant        GM.deleteValues
// @grant        GM.download
// @grant        GM.getResourceText
// @grant        GM.getResourceUrl
// @grant        GM.getTab
// @grant        GM.getTabs
// @grant        GM.getValue
// @grant        GM.getValues
// @grant        GM.info
// @grant        GM.listValues
// @grant        GM.log
// @grant        GM.notification
// @grant        GM.openInTab
// @grant        GM.registerMenuCommand
// @grant        GM.removeValueChangeListener
// @grant        GM.saveTab
// @grant        GM.setClipboard
// @grant        GM.setValue
// @grant        GM.setValues
// @grant        GM.unregisterMenuCommand
// @grant        GM.webRequest
// @grant        GM.xmlHttpRequest
// @grant        GM_addElement
// @grant        GM_addStyle
// @grant        GM_addValueChangeListener
// @grant        GM_cookie
// @grant        GM_deleteValue
// @grant        GM_deleteValues
// @grant        GM_download
// @grant        GM_getResourceText
// @grant        GM_getResourceURL
// @grant        GM_getTab
// @grant        GM_getTabs
// @grant        GM_getValue
// @grant        GM_getValues
// @grant        GM_info
// @grant        GM_listValues
// @grant        GM_log
// @grant        GM_notification
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @grant        GM_removeValueChangeListener
// @grant        GM_saveTab
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_setValues
// @grant        GM_unregisterMenuCommand
// @grant        GM_webRequest
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @grant        window.close
// @grant        window.focus
// @grant        window.onurlchange
// ==/UserScript== 

2.1 基本身份信息

@name 脚本在管理器列表里显示的名字,可随意改。


// @name         test

@namespace命名空间,用来区分同名的不同脚本;通常写作者主页、仓库地址或任意唯一字符串即可。


// @namespace    npm/vite-plugin-monkey

@description,简介,插件描述


// @description  这是一个脚本,加点描述

@author,落下你的大名!


// @author       VE

@version版本号,管理器用它判断要不要更新。建议用“语义化版本”如 1.2.3,这里用日期时间也行。


// @version      20251212.1751

@icon管理器里脚本前面的小图标,可省。


// @icon         https://vitejs.dev/logo.svg

2.2 自动更新地址

downloadURL:管理器“检查更新”时会把整段脚本下载覆盖旧版。updateURL:如果把元数据单独拎出来存成 .meta.js,管理器只拉元数据比较版本,节省流量。 (两个都写时,优先用 downloadURL。)



// @downloadURL  http://x.xx.xxx.xxx/monkey/test.js 
// @updateURL    http://x.xx.xxx.xxx/monkey/test.js 

2.3 是否生效页面(include/exclude/match)

2.3.1 原理

通配符说明:
*
代表任意长度字符,匹配协议、端口、路径。只要地址栏里出现对应字符串就触发脚本。可以同时写多条@include、@match,也可再加@exclude做反向排除。@match相比@include更加准确更稳更快


// @include      *baidu.com*

2.3.2 语法粒度和匹配规则

特性 @include / @exclude @match
通配符 * 可以代表任意长度字符(包括 /、?、&) 不允许通配符;只能用更严格的“分段式”语法
协议 可省略,省略后匹配 http + https 必须显式写出 http:// 或 https:// 或 *://
域名(/主机) *baidu.com* 这种写法合法 只能用 *.baidu.com(子域)或 baidu.com(精确)
端口 通配符里一起扫掉 必须显式 :8080 或 :*
路径 通配符随便写 只允许固定路径 + 末尾 /* 或 /
大小写 不敏感 不敏感
性能 管理器内部转成正则,稍慢 直接映射成浏览器原生匹配,更快
错误容忍 写错了也能“尽量猜” 写错一个字母整条规则作废,控制台直接报错

案例理解:

需求:让脚本在https://www.baidu.com/s?wd=hellohttp://map.baidu.com/都生效。



// 用 @include 一句话搞定:
// @include  *://*.baidu.com/*
 
// 用 @match 需要写两条(子域必须单独列)
// @match   *://www.baidu.com/*
// @match   *://map.baidu.com/*
 
// 如果以后百度冒出 a.baidu.com b.baidu.com
// @include 那条自动覆盖;
// @match 必须再手动加如对应的,否则不生效。

2.3.3 优先级顺序

先算 @exclude —— 一旦命中直接踢掉,后面不再看。再算 @include / @match —— 只要有一条命中就放行。两者同时存在时:并列关系,不互斥;管理器会把它们一起编译成正则。用户自己在管理器编辑界面额外填的 include/exclude 最后叠加,脚本里改不了。

2.3.4 油猴设置面板分别对应字段

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)

界面看到的文字 对应脚本头字段 备注
原始包括 @include 脚本代码里写的 include 规则
原始匹配 @match 脚本代码里写的 match 规则
原始排除 @exclude 脚本代码里写的 exclude 规则
用户包括 用户自定义 include 在管理器界面手动添加,不会写进脚本文件
用户匹配 用户自定义 match 同上
用户排除 用户自定义 exclude 同上

注:“添加为用户排除 / 添加为用户包括”那几个按钮 = 把当前这条规则丢到“用户**”区域,方便不改代码就能临时屏蔽或放行。演示如下:

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)

2.3.5 最佳实践速记

想“无脑通杀”、路径千变万化 → @include 最省事。想“精确到某几个子域、固定路径”、又要性能 → @match 更稳更快。可以混用:



// @match   *://github.com/*        // 精确主站
// @include *://*.github.io/*       // 用户页各种子域
// @exclude *://gist.github.com/*   // 但排除 gist

如果写 @match 报错,立刻看控制台,语法必循“分段”标准,例如*://*.example.com:*/path/* 任何一段乱写都会整行失效。

2.3.6 总结

@include/@exclude 像“模糊搜索”,写起来快、覆盖广;@match 像“身份证校验”,写得啰嗦但更快更严。根据场景混用即可:先用 @match 锁主战场,再用 @include 补边角,最后用 @exclude 踢掉不要的坑位。

2.4 跨域白名单

2.4.1 原理

油猴 2.x 以后,凡是用 GM_xmlhttpRequest 访问非“当前页面域名”的地址,都必须先在 @connect 里列白名单,否则会被拦截。(配置之后油猴会默认跨域)写法支持域名、IP、通配符 *、甚至 *://*.example.com:*。上面把脚本里可能请求的后端地址、本地开发地址都放行了。



// @connect      *https://www.baidu.com/ *
// @connect      127.0.0.1
// @connect      localhost
// @connect      192.168.124.xxx

2.4.2 设置面板对应的字段

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)

2.5 能力清单

以 GM. 开头的是新版 Promise 化 API(TM 4.11+)。以 GM_ 开头的是旧版回调 API,两者可混用。unsafeWindow 表示要访问页面原始 window,绕过油猴的沙箱。window.close/focus/onurlchange 这类是脚本想直接操作窗口级事件。



// @grant        GM.addStyle
// @grant        GM.addValueChangeListener
// @grant        GM.cookie
// @grant        GM.deleteValue
// @grant        GM.deleteValues
// @grant        GM.download
// @grant        GM.getResourceText
// @grant        GM.getResourceUrl
// @grant        GM.getTab
// @grant        GM.getTabs
// @grant        GM.getValue
// @grant        GM.getValues
// @grant        GM.info
// @grant        GM.listValues
// @grant        GM.log
// @grant        GM.notification
// @grant        GM.openInTab
// @grant        GM.registerMenuCommand
// @grant        GM.removeValueChangeListener
// @grant        GM.saveTab
// @grant        GM.setClipboard
// @grant        GM.setValue
// @grant        GM.setValues
// @grant        GM.unregisterMenuCommand
// @grant        GM.webRequest
// @grant        GM.xmlHttpRequest
// @grant        GM_addElement
// @grant        GM_addStyle
// @grant        GM_addValueChangeListener
// @grant        GM_cookie
// @grant        GM_deleteValue
// @grant        GM_deleteValues
// @grant        GM_download
// @grant        GM_getResourceText
// @grant        GM_getResourceURL
// @grant        GM_getTab
// @grant        GM_getTabs
// @grant        GM_getValue
// @grant        GM_getValues
// @grant        GM_info
// @grant        GM_listValues
// @grant        GM_log
// @grant        GM_notification
// @grant        GM_openInTab
// @grant        GM_registerMenuCommand
// @grant        GM_removeValueChangeListener
// @grant        GM_saveTab
// @grant        GM_setClipboard
// @grant        GM_setValue
// @grant        GM_setValues
// @grant        GM_unregisterMenuCommand
// @grant        GM_webRequest
// @grant        GM_xmlhttpRequest
// @grant        unsafeWindow
// @grant        window.close
// @grant        window.focus
// @grant        window.onurlchange
// @grant        ……

注意:
没列在 @grant 里的 API 运行时会被拦截。如果一行 @grant 都不写,管理器默认开启“沙箱+自动注入”,但无法使用任何 GM_* 函数。写 // @grant none 表示“我不用任何特殊 API”,管理器会直接把脚本丢到页面上下文,性能最好,但也最不安全

2.6 总结

上面头部所描述的那么多,可以这么理解:其实是脚本的“户口本”:“我叫什么、版本多少、长什么样、在哪些网站上班、能喊哪些后台、会哪些超能力”——管理器针对这些行决定“给不给你发证,发什么证”。

第三章 搭建基础骨架

3.1 项目创建

下面是用脚手架搭建vue3项目的流程;需要注意的是:由于我们做的是插件项目,所以很多组件库其实是不需要添加的,就算最后有需求,先用原生,再考虑添加!所以我们只需要添加个项目名,其他路由、状态管理器、eslint等,都选择no就好

vue3基础+进阶(一、Vue3项目创建并相比vue2熟悉项目结构)

创建完成之后,为了方便,小编添加了一个移动端适配与element-plus组件如下:

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)

重中之重,决定是插件还是vue项目的插件库:vite-plugin-monkey


npm i -D vite-plugin-monkey

目录规范(企业级):

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)

3.2 油猴配置(meta.json)

将油猴的基本配置放在meta.json文件夹下统一添加:



{
    "name": "ve_test",
    "author": "VE",
    "description": "油猴插件脚本",
    "icon": "https://vitejs.dev/logo.svg",
    "namespace": "npm/vue3-template-plugin-pro",
    "include": [
        "*baidu.com*"
    ],
    "connect": [
        "*https://www.baidu.com/*",
        "127.0.0.1",
        "localhost"
    ],
    "version": "1.0.0",
    "grant": [
        "*"
    ]
}

添加完成后:

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)

3.3 项目配置(vite.config.js)

上面操作完成之后vite.config.js 添加配置:



import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import monkeyPlugin from 'vite-plugin-monkey'
// 自动注入油猴头部配置
import userscriptConfig from './meta.json';
 
// 这里做一层处理的原因:有的需要动态配置可以在这里实现
const dynamicUserscriptConfig = {
    ...userscriptConfig
}
 
export default defineConfig({
  plugins: [
    vue(),
    monkeyPlugin({
      entry: 'src/main.js',
      userscript: dynamicUserscriptConfig,
      build: { 
        autoGrant: true,
        fileName: `ve_test.user.js`,
        metaFileName: true
      }
    })
  ]
})

运行:



npm run dev        # 热更新
npm run build      # 生成 dist/index.user.js

3.4 数据持久化(GM_setValue与GM_getValue )

3.4.1 原理

GM_setValue 与 GM_getValue 是油猴脚本在“用户脚本沙箱”里存/取持久化数据的核心 API,相当于浏览器给每个脚本单独开了一小块 localStorage 增强版

3.4.2 使用方法

使用方法:



// 存
await GM.setValue(key, value)      // 新版 Promise 写法
GM_setValue(key, value)            // 旧版同步写法
 
// 取
let v = await GM.getValue(key)     // 新版
let v = GM_getValue(key [, defaultValue])  // 旧版

能存什么类型(一定要区分出localstore;因为小编下面很多代码都是按localstore,刚开始项目急,没有好好的看,走了很多弯路,如果大家有好的建议评论区提供!!)
字符串、数字、布尔、对象、数组、Date、Map、Set、ArrayBuffer、甚至 Blob 都能直接塞;底层自动走 JSON.stringify + structured clone,无需自己序列化;单条数据最大 ≈ 浏览器 IndexedDB 上限(理论 2 GB 级),远大于 localStorage 的 5 MB。

3.4.3 作用域与生命周期

按脚本隔离: 脚本 A 存的值,脚本 B 完全看不到,也不会跟页面本身的 localStorage 冲突。按浏览器轮廓持久: 清浏览器缓存、重启、升级脚本都不会丢,除非: 用户手动“重置脚本数据” 卸载脚本 手动删浏览器 IndexedDB 里的 tampermonkey 库同源不同页面共享: 同一脚本在 erp.xxx.com 和 crm.xxx.com 都能读到同一份数据。

3.4.4 用法示例



// 旧版同步写法(仍最常用)
// 记录上次折叠状态
GM_setValue('foldState', true);
 
// 下次进入页面读出来
const folded = GM_getValue('foldState', false);  // 没存过就返回 false

3.4.5 与 localStorage 的对比

特性 GM_getValue / setValue localStorage
容量 大到 IndexedDB 上限 10 MB
类型 任意 JS 结构化数据 仅字符串
作用域 同一脚本 当前域名下所有代码
跨域 脚本全域名共享 只能同源
用户清缓存 默认保留 容易被一起清掉
同步/异步 旧版同步,新版 Promise 同步

3.4.6 注意事项

旧版 GM_setValue 是同步阻塞的,数据大时会卡主线程;新版 GM.setValue 返回 Promise,不堵页面。不要拿它当高速缓存疯狂写,IndexedDB 的写入事务太频繁也会降性能。如果脚本被卸载,管理器会提示“是否同时删除数据”,用户点“是”就全清空了——不要存不可恢复的唯一数据。想“跨脚本”共享,只能用 GM.setValue + GM.getValue 再包一层“约定 key 前缀”,或者干脆走后台服务器。

3.4.7 实战使用

这里小编是做了一层封装(因为小编很多地方都是用存储做的,包括多个页面的切换、用户登录等等,然后页面上监听存储的变化从而实现的);封装的思想就是先使用油猴自带的插件处理,如果没用则用localStorage存,每次存完了之后自定义事件用于组件响应式更新



/**
 * 存储工具函数 - 优先使用油猴GM_* API,其次回退到localStorage
 */
 
/**
 * 设置存储值
 * @param {string} key - 存储键名
 * @param {any} value - 存储值(支持对象,会自动JSON序列化)
 * @returns {boolean} - 是否设置成功
 */
export const storageSet = (key, value) => {
  try {
    // 优先使用油猴GM_setValue
    if (typeof GM_setValue === 'function') {
      // 对于对象类型,需要转换为字符串存储
      const storeValue = typeof value === 'object' ? JSON.stringify(value) : value;
      GM_setValue(key, storeValue);
      // 触发自定义事件,用于组件响应式更新
      window.dispatchEvent(new CustomEvent('localStorageChange', { detail: { key, value } }));
      return true;
    }
    // 回退到localStorage
    else if (typeof localStorage !== 'undefined') {
      const storeValue = typeof value === 'object' ? JSON.stringify(value) : value;
      localStorage.setItem(key, storeValue);
      // 触发自定义事件,用于组件响应式更新
      window.dispatchEvent(new CustomEvent('localStorageChange', { detail: { key, value } }));
      return true;
    }
  } catch (error) {
    console.error('存储设置失败:', error);
    return false;
  }
  return false;
}
 
/**
 * 获取存储值
 * @param {string} key - 存储键名
 * @param {any} defaultValue - 默认值(当获取失败或值不存在时返回)
 * @returns {any} - 存储值(对象会自动JSON解析)
 */
export const storageGet = (key, defaultValue = null) => {
  try {
    let value;
    // 优先使用油猴GM_getValue
    if (typeof GM_getValue === 'function') {
      value = GM_getValue(key);
    }
    // 回退到localStorage
    else if (typeof localStorage !== 'undefined') {
      value = localStorage.getItem(key);
    }
    
    // 如果值不存在,返回默认值
    if (value === undefined || value === null) {
      return defaultValue;
    }
    
    // 尝试解析JSON字符串
    try {
      return JSON.parse(value);
    } catch (e) {
      // 如果不是JSON字符串,直接返回原始值
      return value;
    }
  } catch (error) {
    console.error('存储获取失败:', error);
    return defaultValue;
  }
};
 
/**
 * 删除存储值
 * @param {string} key - 存储键名
 * @returns {boolean} - 是否删除成功
 */
export const storageDelete = (key, isClearEvent = false) => {
  try {
    // 优先使用油猴GM_deleteValue
    if (typeof GM_deleteValue === 'function') {
      GM_deleteValue(key);
      // 触发自定义事件,用于组件响应式更新
      isClearEvent && window.dispatchEvent(new CustomEvent('localStorageChange', { detail: { key, value: null } }));
      return true;
    }
    // 回退到localStorage
    else if (typeof localStorage !== 'undefined') {
      localStorage.removeItem(key);
      // 触发自定义事件,用于组件响应式更新
      isClearEvent && window.dispatchEvent(new CustomEvent('localStorageChange', { detail: { key, value: null } }));
      return true;
    }
  } catch (error) {
    console.error('存储删除失败:', error);
    return false;
  }
  return false;
}
 
/**
 * 清空所有存储值
 * @returns {boolean} - 是否清空成功
 */
export const storageClear = () => {
  try {
    // 油猴没有直接的清空方法,需要遍历所有键删除
    if (typeof GM_listValues === 'function' && typeof GM_deleteValue === 'function') {
      const keys = GM_listValues();
      keys.forEach(key => GM_deleteValue(key));
      // 触发自定义事件,用于组件响应式更新
      window.dispatchEvent(new CustomEvent('localStorageChange', { detail: { clear: true } }));
      return true;
    }
    // 回退到localStorage
    else if (typeof localStorage !== 'undefined') {
      localStorage.clear();
      // 触发自定义事件,用于组件响应式更新
      window.dispatchEvent(new CustomEvent('localStorageChange', { detail: { clear: true } }));
      return true;
    }
  } catch (error) {
    console.error('存储清空失败:', error);
    return false;
  }
  return false;
};

 3.5 与后端联调:GM_xmlhttpRequest Promise 化



// utils/gm-request.js
export function gmx(opts) {
  return new Promise((resolve, reject) => {
    GM_xmlhttpRequest({
      method: opts.method || 'GET',
      url: opts.url,
      headers: opts.headers || {},
      data: opts.data,
      responseType: opts.responseType || 'json',
      onload: res => resolve(res),
      onerror: err => reject(err)
    })
  })
}

使用:



import { gmx } from '@/utils/gm-request.js'
const res = await gmx({
  url: 'https://erp.xxx.com/api/order/export',
  method: 'POST',
  headers: { Authorization: `Bearer ${user.token}` },
  data: { ids: selectedIds }
})

3.6 不同环境的运行方式

本地添加环境变量文件:

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)



# 本地环境地址
VITE_BASE_URL = xxxx
 
# 测试环境地址
VITE_BASE_UR = xxx
 
# 生产环境地址
VITE_BASE_URL = xxxx

package.json添加命令:

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)

3.7 自动灰度更新(可选)



// App.vue
import { onMounted } from 'vue'
 
onMounted(() => {
  setInterval(async () => {
    const meta = await fetch(GM.info.scriptUpdateURL + '?t=' + Date.now())
      .then(r => r.text())
    const remoteVer = meta.match(/@versions+(d+.d+.d+)/)?.[1]
    if (remoteVer && remoteVer !== GM.info.script.version) {
      if (confirm(`发现新版本 ${remoteVer},立即更新?`)) {
        location.href = GM.info.script.downloadURL
      }
    }
  }, 1000 * 60 * 30)   // 30 min
})

3.8 审计 & 安全 checklist(提交前打 √):针对打包后的

[ ] 敏感配置全部走 GM_*Value,无硬编码[ ] 无 eval、new Function、innerHTML=用户输入[ ] @grant 与所用 GM_api 一一对应,不多不少[ ] 生产脚本 ≤ 100 KB(gzip)[ ] 版本号三段式,Tag 与 @version 同步

第四章 实战例子

小编在components中添加了一个组件:(除了使用vue3,其他基本上都是原生,包括js、css)

【谷歌脚本开发】一、企业级Tampermonkey插件深度进阶|Vue3 + Vite + GM_api 全链路实战(附 Gitee 模板)



<template>
  <div class="home-index-container">
    <div class="header">
      <span class="title">插件面板</span>
      <div class="minimize-btn" @click.stop="handleMinimize">
        <el-icon><Minus /></el-icon>
      </div>
    </div>
    <div class="content">
      <el-button type="primary" @click.stop="handleClick">测试按钮</el-button>
    </div>
  </div>
</template>
<script setup>
import { ElMessageBox, ElMessage } from "element-plus";
const emit = defineEmits(["close"]);
 
const handleMinimize = () => {
  emit("close");
};
 
const handleClick = () => {
  ElMessageBox.confirm("确认框测试?")
    .then(() => {
      ElMessage.success("提示测试");
    })
    .catch(() => {});
};
</script>
<style scoped>
.home-index-container {
  position: relative;
  width: 320px;
  min-height: 180px;
  display: flex;
  flex-direction: column;
  background-color: #fff;
  border-radius: 8px;
  overflow: hidden;
}
 
.header {
  display: flex;
  align-items: center;
  justify-content: space-between;
  padding: 12px 16px;
  border-bottom: 1px solid #ebeef5;
  background-color: #f5f7fa;
}
 
.title {
  font-size: 14px;
  font-weight: 600;
  color: #303133;
}
 
.minimize-btn {
  cursor: pointer;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 24px;
  height: 24px;
  border-radius: 4px;
  transition: background-color 0.2s;
  color: #909399;
}
 
.minimize-btn:hover {
  background-color: rgba(0, 0, 0, 0.05);
  color: #606266;
}
 
.content {
  flex: 1;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  padding: 24px;
  gap: 12px;
}
 
.desc {
  font-size: 12px;
  color: #909399;
}
</style>

App引入组件:



<script setup>
import rightIcon from "./assets/imgs/right-icon.png";
import { useDraggable } from "@vueuse/core";
import { useTemplateRef, ref } from "vue";
import HomeIndex from "./components/HomeIndex.vue";
 
const mainBox = useTemplateRef("main_box");
const isDragged = ref(false);
const { x, y, style } = useDraggable(mainBox, {
  preventDefault: true,
  initialValue: { x: 140, y: 140 },
  onStart: () => {
    isDragged.value = false;
  },
  onMove: () => {
    isDragged.value = true;
  },
  onEnd: () => {
    setTimeout(() => {
      isDragged.value = false;
    }, 0);
  },
});
 
const loading = ref(false);
const showSimple = ref(false);
const showMinBox = ref(true);
 
const showMax = () => {
  showSimple.value = true;
  showMinBox.value = false;
};
 
const showMin = () => {
  showSimple.value = false;
  showMinBox.value = true;
};
</script>
 
<template>
  <div v-show="!showMinBox">
    <div
      ref="main_box"
      :style="style"
      class="mian-modal"
      v-loading="loading"
      element-loading-text="加载中..."
      v-if="showSimple"
    >
      <HomeIndex @close="showMin"></HomeIndex>
    </div>
  </div>
  <div
    class="min-box"
    v-show="showMinBox"
    :style="{ backgroundImage: `url(${rightIcon})` }"
    @click="showMax"
  >
    <el-icon color="#2878FF"><ArrowLeft /></el-icon>
  </div>
</template>
 
<style scoped>
.mian-modal {
  position: fixed;
  background: #ffffff;
  box-shadow: 0px 8px 22px 7px rgba(191, 191, 191, 0.24);
  border-radius: 8px;
  z-index: 100;
  text-align: left;
}
.min-box {
  position: fixed;
  display: flex;
  align-items: center;
  justify-content: center;
  width: 22px;
  height: 72px;
  right: -5px;
  top: 50%;
  transform: translateY(-50%);
  z-index: 100;
  background-size: cover;
  background-repeat: no-repeat;
  cursor: pointer;
}
</style>

自此,一插件的基本流程就完成了,跟着上面的流程走,针对绝大部分插件都能实现

第五章 源代码地址

https://gitee.com/shallow-winds/vue3-template-plugin-pro

© 版权声明

相关文章

暂无评论

none
暂无评论...