Userscript × Webpack

Userscript × Webpack

大家好,这里是诈尸更博的轻雨酱。

之前好长一段时间没怎么出现了啦(感觉上有一年?),主要是不知道写点什么。这次转换一下身份,假装自己是一个萌萌哒的前端开发者。写点自我满足的东西,分享给大家。

好,那我们就开始吧 XD

这次想写的内容是 Userscript 相关,这大概像是所谓「极客」 向状物。原理说起来很简单,是通过一个浏览器插件(通常是 Tamper Monkey),使你自定义的 Javascript 运行在指定网页上,从而通过简短的代码实现一些有趣或是实用的功能。

比如前段日子,我给集训队作业的 OJ ioihw20.duck-ac.cn 写了个脚本。

这个 OJ 的用户名是顺序编码(形如 ioi2021_08ioi2021_11);大家还有一个完成里面 150 道作业题的指标。对着数字编码人工脑补出这人是谁就很费劲,想看看谁是卷王(点名批评 @gcz)更只能一个一个点,那怎么办??

当然谁卷阿鲁巴谁当然是写个 Userscript 啦!根据内嵌的数据表,建立出用户名(形如 ioihw2021_11)和真实姓名(xxx)的对应关系。啊这就是那个卷王 gcz 啊,怎么还没有被阿鲁巴。

至于排行榜的功能,注意到 UOJ 的前端实现中有用到 jQuery 库。这时候 Userscript 的好处就体现出来了,我们可以直接调用 UOJ 网站的 jQuery!通过 Promise.all()$.get() 方法,同时取得每个人的个人信息页面 /user/profile/ioi2021_<xx>。并用脚本处理数据,汇总到页面上。

此外,出于一些或是恶趣味,我还加了个给排行版的 rk1 带一个 卷王 的 badge 的小功能。这样无论在哪个页面,都能用红色的字写着:这就是那个卷王!!

你讲了这么多,和 Webpack 有啥关系?

欸对啊,这篇博文的标题不是 Webpack 嘛,怎么成了进来听轻雨酱扯淡吹逼啊。

注意到这么一个简简单单的脚本,我只进行了简单的函数封装,就已经有足足 10kb 了。如果我们想要完成更加复杂的脚本,这可怎么办呢?

那就请出我们的主角 webpack 吧(喂喂,这股小学生作文画风是怎么回事啊)。它是一个静态的 Javascript 打包工具。会根据你的配置和代码,生成 Javascript 代码的依赖关系,并打包成一个或多个 bundle。

这在 Vue.js 等开发工具中经常用到(他们也需要库和源代码全部打包到一个文件中),和我们的需求也十分类似。那么,或许我们就可以用类似 node.js 的方式开发我们的 Userscript,然后通过 Webpack 打包,并应用到 Tampermonkey 中?

那怎么写代码啊?

来考虑一下我们的目录结构:

  • ./src 存放我们的脚本。其中以 ./src/app.js 为入口。
  • ./dist 存放自动打包生成的文件,如 bundle.js
  • ./node_modules 存放程序运行所需要的第三方库,无需关心。
  • ./package.json 文件,存放 npm 的配置(如包的版本)。
  • ./webpack.config.js 文件,存放 webpack 的配置。

编辑 package.jsonwebpack.config.js 之前,我们先来考察一下我们开发和调试的需求。

  • 开发环境(Development):在 Tampermonkey 中 @require 一个本地脚本。在 Chrome 设置中允许 Tampermonkey 访问本地文件。这样我们更新到打包更新到 bundle 文件的内容,就能实时应用到浏览器中。
  • 生成环境(Production):通过 webpack 的工具进行打包和压缩。加上 Tampermonkey 所需的文件头,以作为独立的 TamperMonkey 脚本运行。进一步的,可以考虑使用 Github Action 等自动化工具部署。

webpack 支持我们分别为两个环境进行配置,并复用公共部分。

编辑 package.json 文件(请自行替换尖括号内的内容):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
"name": "<name>",
"version": "<version>",
"description": "<description>",
"author": "<author>",
"license": "<license>",
"scripts": {
"build": "webpack --env production && node packer.js",
"dev": "webpack --env development --watch"
},
"dependencies": {
"css-loader": "^4.2.1",
"less": "^3.12.2",
"less-loader": "^6.2.0",
"style-loader": "^1.2.1",
"webpack": "^4.44.1",
"webpack-bundle-analyzer": "^3.9.0",
"webpack-cli": "^3.3.12",
"webpack-merge": "^5.2.0"
}
}

编辑 webpack.config.js 文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
const path = require('path');
const { merge } = require('webpack-merge');

const config = {
public: { // 公共的配置
entry: {
app: './src/app.js'
},
output: {
filename: 'bundle.js',
path: path.resolve(__dirname, 'dist')
},
module: {
rules: [
{
test: /\.less$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },
'less-loader',
],
},
{
test: /\.css$/,
use: [
'style-loader',
'css-loader',
],
},
],
},
plugins: [],
},
production: { // 生产环境的配置
mode: 'production',
},
development: { // 开发环境的配置
mode: 'development',
devtool: 'inline-source-map',
}
};

module.exports = (env) => merge(
config.public,
env.includes('production') ? config.production : config.development
);

运行 npm install 安装我们需要的库。

在当前目录新建 header.js ;新建 packer.js,写入

1
2
3
4
5
6
7
const fs = require("fs");
const path = require("path");

const header = fs.readFileSync(path.join(__dirname, "header.js")).toString();
const bundle = fs.readFileSync(path.join(__dirname, "dist", "bundle.js")).toString();

fs.writeFileSync(path.join(__dirname, "dist", "userscript.js"), header + "\n" + bundle);

创建 src 文件夹,新建 app.js 文件并写入

1
2
const { css } = require("./style.less");
alert("Hello, World");

新建 style.less 文件并写入

1
* { color: red !important; }

在浏览器插件(此处以 Tampermonkey 为例)中创建一个脚本,并写入:

1
2
3
4
5
6
7
8
9
// ==UserScript==
// @namespace <name>
// @name <name>[dev]
// @match http://*
// @match https://*
// @require file://<path-to-your-project>/dist/bundle.js
// ==/UserScript==

// 这里啥都不用写。

运行 npm run build 进行打包,会发现自动生成了 ./dist/bundle.js

我们在浏览器中随便打开一个网页,会发现弹出 Hello, World! 对话框,且所有字体颜色变为了红色。

看到这个丑丑的页面,就说明我们的脚本成功运行了。

需要开发的话,可以直接修改 app.jsstyle.less 的内容。你可以在 app.jsimport 一个别的 Javascript 文件,或者引入通过 NPM 安装的库,webpack 都会一并进行打包。

但这样做每次都要重新构建。这边选择使用 webpack 的 watch 模式监测源文件修改,并实时差量编译。前文我们已经配置好了,运行 npm run dev 即可启用该模式。

我写好了我的脚本,可怎么发布呢?

TamperMonkey 的脚本需要一个固定格式的文件头,而直接用 webpack 打包出来的 bundle.js 显然是不自带这样一段。

新建 header.js 文件,写入你配置的文件头。前文我们配置的 packer.js 可以直接把 header.js 的内容插入到 bundle.js 前面,并写入到文件 ./dist/userscript.js 中。(真是简单粗暴呢)

userscript.js 的内容复制到 Tampermonkey 中即可运行。

发布脚本的时候可以顺便薅一下资本主义羊毛 Github Action(@ouuan 哥哥教育我的),写入 ./.github/workflows/deploy.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
name: "Deploy to Artifacts"
on: push
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Build
run: |
mkdir dist
npm install
npm run build
- name: Deploy
uses: actions/upload-artifact@v1
with:
name: "${{ github.sha }}"
path: dist/userscript.js

后话

嘛,感觉也不太适合写这种博客啊 = = ……受众范围有点小,不知道有没有人看。