前些天开发了个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。
代码语言:javascript复制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的脚本信息段,并在之后加入一条:
代码语言:javascript复制@require file://[编译生成文件路径]
这样,修改程序后刷新待测试页面就可以进行测试了。
油猴API相关
脚本头部
油猴脚本头部的一段注释用于声明脚本的用途与依赖等。此部分可以在构建的最后一步添加在编译结果的头部。对于一些可能冗余的信息(如脚本名称、脚本描述、脚本版本),可以通过文本替换的方式进行插入。
代码语言:javascript复制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_setValue
、GM_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社区对脚本有一些要求。对于我们的开发流程,需要注意:
- 依赖应该通过
@require
而不是一并打包 - 禁止最小化/混淆代码
对于前者,可以通过Webpack的externals
配置不打包Vue与Element。
mode: 'production',
externals: {
// 使用 @require 导入依赖
vue: 'Vue',
'element-ui': 'element-ui'
},
对于后者,optimization
就足够了。似乎也可以设置inline-source-map
,但是讲道理,我不觉得那样叫“不最小化”啊……
// 根据 Greasy Fork 规则取消最小化
optimization: {
minimize: false
},
单元测试的可能性
对于油猴脚本而言,单元测试很难用简单的方式实现,因为
- 油猴本身根本没有支持
- 油猴API缺少可用的Mock
- 原始页面的装载困难重重。尤其如今盛行使用前端渲染
在有限的条件下,能做到的单元测试项目是非常少的。但如果函数划分合理,依旧还是有测试的可能。如对于纯粹进行Ajax请求、解析结果的函数,只需要实现GM_xmlhttpRequest
就可以通过Mock.js
等框架进行测试。此外,对于DOM的简单操作,如插入DOM、装载侦听器、解析DOM等等,也可以通过借助jsdom的方式进行实现,如使用测试框架JEST。但是这样的测试效果是非常有限的,因为所有测试代码只能运作于原始页面的静态“快照”上。对于前端渲染的页面,甚至需要取其渲染结果进行测试,无法在单元测试时将待测试原始页面的获取自动化。所以,最合理的测试方式应当是借用chromedriver一类的浏览器调试,并模拟用户的操作。但是遗憾的是我并没有找到能完全自动化的解决方案,测试一开始还是需要测试者手动安装油猴、配置测试脚本,而且CI的环境搭建也是一大难题。总而言之,针对油猴脚本的单元测试仍旧只能覆盖很小一部分操作,但是可以通过合理的函数划分编写一些单元测试。
Reference
本文大量参考此项目:huangxubo23/tampermonkey-vue