笔者的业余爱好之一就是逛 Pixiv,收藏好看的插画等作品并下载到本地。油猴脚本 Pixiv Previewer 做了两个笔者很喜欢的便利性工作:
- 作品预览:鼠标悬浮在作品缩略图上时自动在当前页面显示大图,快速决定要不要点进作品页。
- 作品排序:作品搜索页可以按“排序”按钮,自动获取若干页的作品并按照收藏数排序,快速筛选优质作品。
出于爱好,笔者去脚本仓库翻阅了一下源码,觉得有一些可以改进的地方,遂 Fork 之,进行二次开发。
工程化改造
首当其冲的自然是前端项目工程化改造。大多数油猴脚本仓库都是孤零零的一个 JavaScript 文件,Pixiv Previewer 也不例外。直接手搓 JavaScript 虽然很便利,但在项目长期的维护开发中多半会掩埋掉许多未曾留意的 Bug,同时由于缺少关联提示,开发效率也会大打折扣。作为前端工程师,这样可怕的事情自然是不能接受的。
通过 tsup 构建项目
初始化 package.json
等过程跳过不表,添加 TypeScript 支持自然是对项目进行二次开发的前置步骤。由于我们最后构建的产物将作为油猴脚本运行,即打包为库,因此笔者选择了开箱即用的 tsup:
yarn add -D tsup
添加 tsup 的配置文件 tsup.config.ts
:
import { defineConfig } from "tsup";
import packageJson from "./package.json";
export default defineConfig({
entry: ["src/index.ts"],
target: ["chrome107"],
minify: false,
splitting: false,
clean: true,
platform: "browser",
env: {
VERSION: packageJson.version,
},
banner: {
js: `// ==UserScript==
// @name PixivPreviewerL
// @namespace ${packageJson.homepage}
// @version ${packageJson.version}-${new Date().toLocaleDateString()}
// @description ${packageJson.description}
// @author ${packageJson.author}
// @match *://www.pixiv.net/*
// @grant unsafeWindow
// @grant GM.xmlHttpRequest
// @grant GM_xmlhttpRequest
// @license ${packageJson.license}
// @icon https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=32&url=https://www.pixiv.net
// @icon64 https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=64&url=https://www.pixiv.net
// @require https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js
// ==/UserScript==`,
},
});
上面的配置表示,tsup 将打包构建源文件 src/index.ts
至默认产物文件 dist/index.js
,保证兼容 Chrome >= 107
。其中,笔者复用了 package.json
里定义的若干字段,用于添加项目环境变量(脚本通过版本号判断是否显示更新日志框),以及生成油猴脚本顶部的配置项。
这样,当执行 tsup
命令时,构建的产物形如:
// ==UserScript==
// @name PixivPreviewerL
// @namespace https://github.com/LolipopJ/PixivPreviewer
// @version 0.1.3-2025/3/17
// @description Original project: https://github.com/Ocrosoft/PixivPreviewer.
// @author Ocrosoft, LolipopJ
// @match *://www.pixiv.net/*
// @grant unsafeWindow
// @grant GM.xmlHttpRequest
// @grant GM_xmlhttpRequest
// @license GPL-3.0
// @icon https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=32&url=https://www.pixiv.net
// @icon64 https://t0.gstatic.com/faviconV2?client=SOCIAL&type=FAVICON&fallback_opts=TYPE,SIZE,URL&size=64&url=https://www.pixiv.net
// @require https://raw.githubusercontent.com/Tampermonkey/utils/refs/heads/main/requires/gh_2215_make_GM_xhr_more_parallel_again.js
// ==/UserScript==
// ... 脚本构建后的代码
在默认情况下,tsup 会使用预设的 tsconfig.json
。但笔者在开发过程中,发现 VSCode 有时会遗漏某些类型的定义,于是手动添加了 tsconfig.json
在根目录:
{
"compilerOptions": {
"target": "ES2022",
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "bundler",
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
"lib": ["esnext", "dom"]
},
"include": ["src/"],
"exclude": ["node_modules/", "dist/"]
}
代码质量检查与格式化工具
TypeScript 自身已包含类型检查等能力,但还可以引入更多质量检查的通用规范,提升项目的健壮性。对于 TypeScript 项目,目前来看最好的选择应当是 typescript-eslint
,参考官方文档添加依赖并配置 eslint.config.mjs
:
yarn add --dev eslint @eslint/js typescript typescript-eslint
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
);
现在执行 eslint src --fix
即可查找并尝试修复 src/
目录下所有文件的质量问题。
接着为项目添加自动格式化能力,Prettier 是笔者最推荐的选项。此外,笔者喜欢将 Prettier 与 ESLint 结合起来一起使用,即在执行 eslint src --fix
修复质量问题的同时,自动格式化代码。ESLint 的插件 eslint-plugin-prettier
可以实现如上能力,添加此插件作为依赖并进一步配置 eslint.config.mjs
:
yarn add -D prettier eslint-plugin-prettier eslint-config-prettier
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
eslintPluginPrettierRecommended,
);
此外,Pixiv Previewer 项目依赖于 JQuery 来执行查找与修改 DOM 的操作,笔者添加了相应的类型提示:
yarn add -D @types/jquery globals
// @ts-check
import eslint from "@eslint/js";
import tseslint from "typescript-eslint";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import globals from "globals";
export default tseslint.config(
eslint.configs.recommended,
tseslint.configs.recommended,
{
languageOptions: {
globals: {
...globals.browser,
...globals.jquery,
},
},
},
eslintPluginPrettierRecommended,
);
至此,基本的项目工程化改造告一段落。项目可以基于 TypeScript 开发,并且拥有了质量检查和格式化能力。将原来的 JavaScript 代码放到 src/index.ts
文件里,开始正式的二次开发吧!
预览功能优化
笔者在阅读 Pixiv Previewer 源码时发现,原作者根据当前页面的不同,实现了不同的通过 DOM 节点寻找可预览作品的 <img>
标签的方法,并为它添加监听事件,当鼠标悬浮在上面的时候显示预览。为了应对作品动态加载的情况,脚本设置了循环定时器,每隔一小段时间重新执行上述方法。
这种力大砖飞的实现方案存在一个不可忽视的缺陷:要考虑到每个页面里不同的作品 <img>
标签的排布情况,为之实现绑定事件的方法。对于没有考虑到的作品 <img>
标签,自然无法显示预览。例如,在当前的 Pixiv Previewer@3.7.32
版本,艺术家弹窗里最近的作品就无法显示预览:

另外,如果 Pixiv 前端页面某一天迭代更新,寻找作品 <img>
标签的方法也可能需要大刀阔斧地修改。
发布这篇博客的第二天,2025 年 03 月 18 日,Pixiv 就上线了新的首页,“推荐作品”模块的下面变成了无限滚动的推文模式。由于推文存在高度上限,因此部分尺寸的图片还是会出现显示不全的情况,可以添加预览功能。
笔者想到的改进方案是:把找作品 <img>
标签的工作,从脚本手中移交到用户手中。即监听鼠标移动事件,当悬浮到可预览作品上时,显示预览。
const onMouseMove = (mouseMoveEvent: JQuery.MouseMoveEvent) => {
onMouseOverIllust(mouseMoveEvent.target);
};
$(document).on("mousemove", onMouseMove);
在上面的代码里,笔者为 Document 全局绑定了 mousemove
监听事件,当鼠标移动时,调用 onMouseOverIllust(target)
方法,其中 target
是当前鼠标所悬浮的元素。如果监听的是 mouseover
事件,脚本可以有更好的性能表现,但是笔者在测试过程中,发现 mouseover
事件回调对象里的 ctrlKey
等值有时不能正确反映实际状态,所以这里选择了监听 mousemove
事件。
接着,需要判断鼠标当前悬浮的元素是否为可预览的作品,如果是则显示预览。观察 DOM 节点:

如果当前鼠标悬浮在可预览的作品 <img>
元素上,则本身包含了访问链接的 src
属性,通过简单的正则表达式处理即可获取作品的 Pixiv ID 和大图链接。同时,外层还包裹着一层 <a>
元素,包含跳转链接。
考虑到部分 <img>
元素虽然包含作品缩略图的 src
属性,但跳转的页面并不一定是作品页本身(https://www.pixiv.net/artworks/artwork-id
),即可能只是作为封面打开别的页面,鼠标悬浮在这些 <img>
元素上时不应当显示预览。因此笔者实现的判断方法是:统一取当前悬浮元素的第一个父 <a>
节点,当其 href
值满足 /\/artworks\/(\d+)/
时,显示预览:
const getIllustMetadata = (target: JQuery<HTMLElement>) => {
let imgLink = target;
while (!imgLink.is("A")) {
imgLink = imgLink.parent();
if (!imgLink.length) {
return null;
}
}
const illustHref = imgLink.attr("href");
const illustHrefMatch = illustHref?.match(/\/artworks\/(\d+)/);
if (!illustHrefMatch) {
return null;
}
const illustId = illustHrefMatch[1];
const ugoiraSvg = imgLink.children("div:first").find("svg:first");
const illustType =
ugoiraSvg.length || imgLink.hasClass("ugoku-illust")
? IllustType.UGOIRA // 动图作品
: IllustType.ILLUST; // 插画或漫画作品
return {
/** 作品 ID */
illustId,
/** 作品类型 */
illustType,
};
};
const onMouseOverIllust = (target: JQuery<HTMLElement>) => {
const { illustId, illustType } = getIllustMetadata(target) || {};
previewIllust({ target, illustId, illustType });
};
插画或漫画作品可能存在多页,处理时可以始终调用 Pixiv 官方的接口 /ajax/illust/${artwork-id}/pages
,获取指定作品包含的所有链接。
再修修补补,为事件加上节流函数,响应鼠标滚动切换页数等,实现了和原版并无二异的预览能力:

可喜可贺,可喜可贺!
排序功能优化
原脚本的预览功能已经能够非常好地实现笔者需要的能力了,笔者想要在此基础上加入一些额外的能力或做一些优化,如:
- Feature:支持按照发布日期排序(相当于过滤掉收藏量小于指定值的作品)。
- Feature:支持在用户作品页进行排序。
- Feature:排序结果高亮显示“优质作品”,即收藏量增长速率高的作品。需要定义并实现增长速率函数。
- Performance:排序结果滑动到底部时加载更多,避免一次性渲染全部导致内存占用过大。
但是目前仍处于咕咕咕状态,敬请期待 XD