注册
web

搭建一个快速开发油猴脚本的前端工程

一、需求起因

最近遇到一个问题:公司自用的 bug 管理工具太老了,网页风格还是上世纪的文字页面。虽然看习惯了还好,但是某些功能确实很不方便。比如,联系人都是邮箱或者英文名,没有中文名称,在流转 bug 时还得复制粘贴英文名去企业微信里搜索对应的人名。第二是人员比较多,在一堆邮箱里很难找到对应的人......

总之,诸如此类的问题让我有了对该网页进行改造的想法。

但是这种网页都是公司创业时期拿的开源产品私有化部署,网页源码能不能找到都不好说。再者,公司也不会允许此类的“小聪明”,这并不是我的主职工作,所以修改源码是非常不现实的。

那目前的思路,就是在原网页基础上进行脚本注入,修改网页内容和样式。方案无非就是浏览器插件或者脚本注入两种。

脚本的话就是利用油猴插件的能力,写一个油猴脚本,在网页加载完成后注入我们书写的脚本,达到修改原网页的效果。

插件也是类似的原理,但是写插件要麻烦得多。

出于效率考虑,我选择了脚本的方案。这里其实也是想巩固下 js 的 DOM API,框架写多了,很多原生的 API 反而忘得一干二净。

二、关于油猴脚本

先看一份 demo

// ==UserScript==
// @name script
// @namespace
// @version 0.0.1
// @description 这是一段油猴脚本
// @author xxx
// @match *://baidu.com/*
// @run-at document-end
// @license MIT
// ==/UserScript==

(function () {
"use strict";
const script = document.createElement("script");
document.body.appendChild(script);
})();

油猴脚本由注释及 js 代码组成。注释需要包裹在

// ==UserScript==

// ==/UserScript==

两个闭合标签内。同时只能书写类似 @name 规定好的注释头,用于标明脚本的一些元信息。其中比较重要的是 @match 和 @run-at

@match 规定了该脚本所运行的域名,例如,只有当我打开了百度的网页时我才运行脚本,这个 @match 可以书写多个。@run-at 则规定了脚本的运行时机,一般是网页加载开始,网页加载结束。@run-at 只声明一次。

@run-at 有以下可选值:

image.png

图片看得不清晰也没关系,这种都是用到再查。

更多注释配置请参考:油猴脚本

而代码部分是一个立即执行函数,所有的内容都需要写在这个立即执行函数内,否则无法生效。

三、问题显现

刚开始,我并没有工程化开发的想法,我想的是就是一个脚本,直接一梭子写到底即可,反正就是那样,就是个普通的 js 文件,一切都是那么原始,朴实无华。

但是当代码来到两千多行后(我是真的很爱加东西),绷不住了,每次写代码都需要在文件上下之间反复横跳,有时候有些变量定义了都不记得,写代码还得滚动半天才能到最底下。

加东西也变得越来越臃肿,越来越丑陋。

忍无可忍,我决定对这个脚本进行工程化改造。但是工程化之前有几个问题需要解决,或者说需要调研清楚。

四、关键点分析

1.构建工具

首先肯定是打包成 iife 的产物,很多工具都支持。既然工程化了,一般大家的选择就是 webpack 或者 vite。这里因为涉及到开发模式,需要及时产出打包产物,且能够搭建 dev 服务器,方便访问本地打包后的资源,因此需要选择具备 dev 服务器的开发构建工具。

我选择 vite。当然,webpack 也是不错的选择。

如果你对实时预览要求不高,能够接受复制粘贴到油猴再刷新页面预览,也可以选择纯粹的打包器,例如 rollup

2.css 预编译器

传统的添加样式的方式,一般就是生成一个 style 标签,然后修改其 innerHTML

export const addStyle = (css: string) => {
const style = document.createElement('style');
style.type = 'text/css';
style.innerHTML = css;
document.getElementsByTagName('head')[0].appendChild(style);
}

addStyle(`
body {
width: 100%;
height: 100%;
}
`
);

这样就能实现往网页里添加自定义的样式。但是我现在不满足于书写传统的 css,我既然都工程化了,肯定要把 less 或者 scss 用上。

我的目的,就是可以新建一个例如 style.less 的文件开心地书写 less,打包时候编译一下这个 less 文件,并将其样式注入到目标 HTML 中。

但在传统模块化工程里,构建工具对 less 的支持,是直接在 HTML 中生成一个 style 标签,引入编译后的 less 产物(css)。

也就是说,我需要手动实现 less 到 css 到 js 这个过程。

转变的步骤就是用 less 本身的编译能力,将其产物转变为一个 js 模块。

具体实现放到后面再聊。

3.实现类似热更新的效果

我们启动一个传统的 vite 工程时,我们更新了某个 js 文件或者相关文件后,工程会监听我们的文件被修改了,从而触发热更新,服务也会自动刷新,从而达到实时预览的效果。

这是因为工程会在本地启动一个开发服务器,最终产物也会实时构建,那网页每次去获取这个服务器上的资源,就会获取到最新的代码。根据这点,我们同样需要启动一个本地服务器,而这在 vite 中直接一个 vite 命令即可。

在油猴脚本中,我们新建一个 script 标签,将其 src 指向我们本地服务器的构建产物的地址,即可实现实时的脚本更新,而不用复制产物代码再粘贴到油猴。

代码如下:

// ==UserScript==
// @name script
// @namespace
// @version 0.0.1
// @description 这是描述
// @author xxx
// @match *://baidu.com/*
// @run-at document-end
// @license MIT
// ==/UserScript==

(function () {
"use strict";
const script = document.createElement("script");
script.src = "http://localhost:6419/dist/script.iife.js";
document.body.appendChild(script);
})();

这里的 localhost:6419/dist/script.iife.js 都取决于你 vite.config.js 中的配置。

具体后面再聊。

五、开始搭建工程

1.使用 yarn create vite 或者 pnpm create vite 初始化一个 vite 模板工程

image.png

image.png

image.png

其他的你自己看着选就可以。

2.修改 vite.config.js

/**
* @type {import('vite').UserConfig}
*/

module.exports = {
server: {
host: 'localhost',
port: 6419,
},
build: {
minify: false,
outDir: 'dist',
lib: {
entry: 'src/main.ts',
name: 'script',
fileName: 'script',
formats: ['iife'],
},
},
resolve: {
alias: {
'@': '/src',
'@utils': '/src/utils',
'@enum': '/src/enum',
'@const': '/src/const',
'@style': '/src/style',
}
}
}

这里使用 cjs 是因为我们会实现一些脚本,脚本里可能会用到这里的某些配置,所以使用 cjs 导出也有利于外部的使用。

3.创建一个 tampermonkey.config 文件,将油猴注释放在这里

// ==UserScript==
// @name script
// @namespace http://tampermonkey.net/
// @version 0.0.1
// @description 这是描述
// @author xxx
// @match *://baidu.com/*
// @run-at document-end
// @license MIT
// ==/UserScript==

当然,你要觉得这样多余、没必要,也可以看自己喜好,只要最终产物里有这个注释即可。但是拆出来有利于我们维护,后续也会新增脚本,有利于工程化的整体性和可维护性。

4.使用 nodemon 监听文件修改

因为我们自己对 less 有特殊处理,加上未来可能会对需要监听的文件进行精细化管理,所以这里引入 nodemon,如果你自己对工程化有自己的理解,也可以按照自己的理解配置。

执行 pnpm i nodemon -D

根目录新增 nodemon.json

{
"ext": "ts,less",
"watch": ["src"],
"exec": "pnpm dev:build && vite"
}

这里的 pnpm dev:build 还另有玄机,后面再展开。

到这里,我们的工程雏形已经具备了。但是还有一个最关键的点没有解决——那就是 less 的转换。

六、less 的转换以及几个脚本

首先,less 代码需要编译为 css,但是我们需要的是 css 的字符串,这样才能通过 innerHTML 之类的方法注入到网页中。

使用 less.render 方法可以对 less 代码进行编译,其是一个 Promise,我们可以在 then 中接收编译后的产物。

我们可以直接在根目录新建一个 script 文件夹,在 script 文件夹下新建一个 gen-style-string.js 的脚本:

const less = require('less');
const fs = require('fs');
const path = require('path');

const styleContent = fs.readFileSync(path.resolve(__dirname, '../src/style.less'), 'utf-8');

less.render(styleContent).then(output => {
if(output.css) {
const code = `export default \`\n${output.css}\``;

const relativePath = '../style/index.ts';
const filePath = path.resolve(__dirname, relativePath)

if(fs.existsSync(filePath)) {
fs.rm(filePath, () => {
fs.writeFileSync(path.resolve(__dirname, relativePath), code)
})
} else {
fs.writeFileSync(path.resolve(__dirname, relativePath), code)
}
}
})

我们将编译后的 css 代码结合 js 代码导出为一个模块,供外部使用。也就是说,这部分编译必须在打包之前执行,这样才能得到正常的 js 模块,否则就会报错。

这段脚本执行完后会在 style/index.ts 中生成类似代码:

export default `
body {
width: 100%;
height: 100%;
}
`

这样 less 代码就能够被外部引入并使用了。

这里多说一句,因为 style/index.ts 的内容是根据 less 编译来的,而我们的 nodemon 会监听 src 目录,因此这个 less 编译后的 js 产物,不能放在 src 下,因为假设将它放在 src 目录下,它在写入的过程中也会触发 nodemon,会导致 nodemon 进入死循环。

除此之外,我们之前还将油猴注释拎出来单独放在一个文件里:tampermonkey.config

在最终产物中,我们需要将其合并进去,思路同上:

const fs = require('fs');
const path = require('path');
const prettier = require('prettier');

const codeFilePath = '../dist/script.iife.js';
const configFilePath = '../tampermonkey.config';
const codeContent = fs.readFileSync(path.resolve(__dirname, codeFilePath), 'utf-8');
const tampermonkeyConfig = fs.readFileSync(path.resolve(__dirname, configFilePath), 'utf-8');

if (codeContent) {
const code = `${tampermonkeyConfig}\n${codeContent}`;
prettier.format(code, { parser: 'babel' }).then((formatted) => {
fs.writeFileSync(path.resolve(__dirname, codeFilePath), formatted)
})
}

最后,因为我们的 tampermonkey.config 以及 vite.config.js 可能会更改配置,所以每次我们在开发模式时生成的临时油猴脚本,也需要变,我们不可能每次都去修改,而是应该跟随上面两个配置文件进行生成,我们再新建一个脚本:

const fs = require('fs');
const path = require('path');
const prettier = require('prettier');
const viteConfig = require('../vite.config');

const codeFilePath = '../tampermonkey.js';
const tampermonkeyConfig = fs.readFileSync(path.resolve(__dirname, '../tampermonkey.config'), 'utf-8');
const hostPort = `${viteConfig.server.host}:${viteConfig.server.port}`;
const codeContent = `
(function () {
'use strict'

const script = document.createElement('script');

script.src = 'http://${hostPort}/dist/${viteConfig.build.lib.name}.iife.js';

document.body.appendChild(script);
})()
`
;

const code = `${tampermonkeyConfig}\n${codeContent}`;

prettier.format(code, { parser: 'babel' }).then((formatted) => {
if(fs.existsSync(path.resolve(__dirname, codeFilePath))) {
fs.rm(path.resolve(__dirname, codeFilePath), () => {
fs.writeFileSync(path.resolve(__dirname, codeFilePath), formatted);
});
}
else {
fs.writeFileSync(path.resolve(__dirname, codeFilePath), formatted);
}
})

稍微用 prettier 美化一下。

七、完善 package.json 中的 script

我们其实只有开发模式,新建一个命令:

"dev": "node script/gen-tampermonkey.js && nodemon"

优先生成 tampermonkey.js,这时候会启动服务器,记得先将 tampermonkey.js 中的内容拷贝到油猴,才能方便热更新,不然又需要复制粘贴。

对于 build 命令:

"dev:build": "node script/gen-style-string.js && tsc && vite build && node script/gen-script-header-comment.js"

需要先将 less 编译为可用的 js 字符串模块,然后才能执行 buildbuild 完还需要拼接油猴注释,这样最终产物才具备可用的能力。

开发完成后,就将打包产物替换掉之前粘贴进油猴的内容。

八、额外的补充

vite 命令会直接启动本地开发服务器,而我们的 script 命令中,使用 && 时,下一个命令会等待上一个命令执行完成后再执行,所以 vite 需要放在最后执行,这是串行逻辑。当然,借助一些库我们可以实现并行 script 命令。但是我们这里需要的是串行,只是不完美的是,每次文件变更,都需要重新执行 pnpm dev:build && vite,这样会重复新启一个服务器,但是不重启的话,始终使用最初的那个服务,最新编译的资源无法被油猴感知,资源没有得到更新。

所以,聪明的你有办法解决吗?


作者:北岛贰
来源:juejin.cn/post/7437887483259584522

0 个评论

要回复文章请先登录注册