使用 Vue+Element 开发 Tampermonkey 插件

前些天开发了个OneDrive下载直链提取的油猴脚本,也是我第一次开发有复杂操作界面的油猴脚本。很早之前,我也写过一些有图形界面的脚本(参见:两个油猴脚本分享),只不过那个界面太简单。但就是那种简单的界面,使用jQuery控制页面也需要非常繁复的操作。而由于这次的脚本需要操作表格、完成多选操作甚至弹出模态框,因此如果还用jQuery就太折磨人了。最好是能借鉴现代前端开发的几大套件,顺便也用用现成UI库,节省一些工作量。

技术选型:Webpack + Vue.js + Element

因为Tampermonkey需要单一脚本文件,所以打包工具是逃不掉的。Webpack基本上是最适合的选择:最常用、功能全面、打包细节可控。其次是界面,我选择了使用Vue.js。部分熟悉我的人可能会说,“呦呦呦,这不React吹吗?几天不见,用Vue啦”。对此我的解释是,我虽然推崇React,但是我从来没有排斥过使用Vue.js。相反我认为快速开发、后台开发、从旧Web开发过渡的开发等等都十分适合使用Vue。对于Tampermonkey脚本,Vue显然是有很多优势的:

  • 组件样式自动管理,不会影响原始网页
  • 双向绑定数据流,能简化很多操作。脚本的操作界面不需要多复杂的逻辑控制,此时双向绑定的优势就体现出来了
  • 部分操作类似jQuery,对已有DOM的修改相对来说更方便

最后是界面库了,为什么选择Element呢?其实没啥原因,一个是以前用过比较熟悉,另一个是找到的脚手架项目就是这些技术选型(捂脸)

方法论

油猴插件的核心是对原始网页的解析/修改,鉴于油猴官方没有任何自动化加载脚本的方法(热重载更是想都别想),因此开发过程中如果每次都通过“编译-复制”来调试,那将会是一件很恐怖的事情。但Vue与Webpack提供的热加载方案又属实好用,因此要是想用上热加载,就需要将脚本中界面的部分进行抽离。换言之就是独立出脚本功能模块,并给每个导出的模块函数编写Mock。

let selected = actual;
// 开发环境启用 Mock
if (isDev) {
    selected = mock;
}

// 函数
export const getFileList = selected.getFileList;
// ...

对于实现脚本功能的模块,可以通过油猴自带的编辑器进行逐一的编码和测试。所以核心的开发流程就是编写页面相关函数、按模块组织、编写Mock,之后进行UI的开发。

热加载与调试

UI开发时,可以使用热加载的方式进行测试。可以通过HtmlWebpackPlugin创建空白页面进行测试,之后启动webpack的热模块替换。

  plugins: [
    new HtmlWebpackPlugin({
      title: 'test page'
    }),
    new webpack.HotModuleReplacementPlugin() // hot reload
  ]

热加载通常用于调试UI效果,因此对脚本功能的调试也无能为力。此时可以通过比较Hack的方式来让油猴实时加载编译生成的脚本。首先以watch且development的模式使Webpack可以实时编译输出。之后在Chrome插件管理-Tampermonkey-详细信息勾选允许访问本地文件 URL。然后在油猴后台创建新脚本,仅复制Tampermonkey的脚本信息段,并在之后加入一条:

@require file://[编译生成文件路径]

这样,修改程序后刷新待测试页面就可以进行测试了。

油猴API相关

脚本头部

油猴脚本头部的一段注释用于声明脚本的用途与依赖等。此部分可以在构建的最后一步添加在编译结果的头部。对于一些可能冗余的信息(如脚本名称、脚本描述、脚本版本),可以通过文本替换的方式进行插入。

const app = fs.readFileSync(`./dist/${entryFile}`, 'utf8')
let tampermonkeyConfig = fs.readFileSync('./tampermonkey.js', 'utf8')
tampermonkeyConfig = tampermonkeyConfig.replace('__APP_NAME__', appName)
tampermonkeyConfig = tampermonkeyConfig.replace('__APP_VERSION__', appVersion)
fs.writeFileSync(`./dist/${entryFile}`, tampermonkeyConfig + '\n' + app)

油猴API

Tampermonkey本身也提供了一系列API以供脚本使用。其中如脚本数据持久化(GM_setValueGM_getValue)、Ajax请求(GM_xmlhttpRequest)等接口都十分常用。虽然说在模块中可以随意使用这些函数,但是由于缺少Mock(很多也没法编写)、类型定义与自动补全,因此不建议直接使用这些函数。可以使用可编写Mock的形式对其进行包装。

// actual
/**
 * 取得 Aria 配置
 * @returns {{downloadPath: string, apiBase: string, token: string}}
 */
const getAriaConfig = () => {
    let json = GM_getValue(KEY_ARIA_CONF, "null");
    return JSON.parse(json);
};

// mock
/**
 * 取得 Aria 配置
 * @returns {{downloadPath: string, apiBase: string, token: string}|null}
 */
const getAriaConfig = () => {
    if (!window._aria_config) {
        return null;
    }
    return window._aria_config;
};

符合社区规范

一般而言,油猴脚本发布的Greasy Fork社区对脚本有一些要求。对于我们的开发流程,需要注意:

  1. 依赖应该通过@require而不是一并打包
  2. 禁止最小化/混淆代码

对于前者,可以通过Webpack的externals配置不打包Vue与Element。

  mode: 'production',
  externals: {
    // 使用 @require 导入依赖
    vue: 'Vue',
    'element-ui': 'element-ui'
  },

对于后者,optimization就足够了。似乎也可以设置inline-source-map,但是讲道理,我不觉得那样叫“不最小化”啊……

  // 根据 Greasy Fork 规则取消最小化
  optimization: {
    minimize: false
  },

单元测试的可能性

对于油猴脚本而言,单元测试很难用简单的方式实现,因为

  1. 油猴本身根本没有支持
  2. 油猴API缺少可用的Mock
  3. 原始页面的装载困难重重。尤其如今盛行使用前端渲染

在有限的条件下,能做到的单元测试项目是非常少的。但如果函数划分合理,依旧还是有测试的可能。如对于纯粹进行Ajax请求、解析结果的函数,只需要实现GM_xmlhttpRequest就可以通过Mock.js等框架进行测试。此外,对于DOM的简单操作,如插入DOM、装载侦听器、解析DOM等等,也可以通过借助jsdom的方式进行实现,如使用测试框架JEST。但是这样的测试效果是非常有限的,因为所有测试代码只能运作于原始页面的静态“快照”上。对于前端渲染的页面,甚至需要取其渲染结果进行测试,无法在单元测试时将待测试原始页面的获取自动化。所以,最合理的测试方式应当是借用chromedriver一类的浏览器调试,并模拟用户的操作。但是遗憾的是我并没有找到能完全自动化的解决方案,测试一开始还是需要测试者手动安装油猴、配置测试脚本,而且CI的环境搭建也是一大难题。总而言之,针对油猴脚本的单元测试仍旧只能覆盖很小一部分操作,但是可以通过合理的函数划分编写一些单元测试。

Reference

本文大量参考此项目:huangxubo23/tampermonkey-vue

分享到

KAAAsS

喜欢二次元的程序员,喜欢发发教程,或者偶尔开坑。(←然而并不打算填)

相关日志

  1. 没有图片
  2. 没有图片

评论

还没有评论。

在此评论中不能使用 HTML 标签。