用户脚本编写(油猴 / 脚本猫)
用户脚本是一段带 ==UserScript== 元数据块的 JS,由脚本管理器注入到匹配的页面里运行。核心流程:
定类型 → 写元数据 → 申请 @grant → 写逻辑 → 装到管理器验证 → 迭代
本机已接入脚本猫 MCP(见 scriptcat-mcp-bridge 技能),写完可以直接推送安装,不必手动复制粘贴 — 见文末「安装与迭代闭环」。
第一步:定脚本类型
| 类型 | 元数据 | 运行环境 | 有无 DOM | 典型场景 |
|---|---|---|---|---|
| 页面脚本 | 只有 @match |
注入到匹配的页面 | ✅ | 改页面外观、去广告、自动点击/填表、抓页面数据 |
| 后台脚本 | @background |
独立后台常驻 | ❌ 无 DOM | 长期轮询、跨页状态维护、需要与多个页面交互的调度器 |
| 定时脚本 | @crontab <表达式> |
独立后台,按周期触发 | ❌ 无 DOM | 自动签到、定时提醒、周期同步 |
> 后台脚本属于脚本猫特有,标准油猴没有。写通用可移植脚本时优先用页面脚本,只有需要"页面没打开也要跑"时才用后台/定时。
第二步:元数据块(.user.js 开头)
// ==UserScript==
// @name 脚本名称
// @namespace https://example.com/ // name+namespace 决定唯一性
// @version 0.1.0 // 遵循 semver,变更会触发用户更新提示
// @description 一句话说明
// @author You
// @match https://example.com/* // 遵循 Chrome match pattern
// @icon https://example.com/favicon.ico
// @grant GM_setValue // 用到的每个 GM API 都要声明
// @grant GM_xmlhttpRequest
// @connect api.example.com // 跨域请求/读 cookie 的目标站
// @run-at document-idle // 默认即此项
// ==/UserScript==
必记规范
@grant是权限门禁,调用了没声明的 API 会直接报错。特殊值:none→ 不在沙盒跑,可以用任何页面 window 变量,但完全不能用 GM APIunsafeWindow→ 脚本猫必须显式声明(标准油猴不需要),这是最常见的移植失败点@match通配符:*为通配,tld匹配顶级域,*.domain.com也会匹配domain.com本身写https://example.com/*会同时命中其子路径,但不会命中http://。@run-at可选值:document-start(脚本猫 v0.3+)、document-end、document-idle(默认)、document-body(v0.6.2+,有 body 才注入)。已移除document-menu。@inject-into:page(默认,可unsafeWindow访问页面 window 和 DOM)/content(不受 CSP 限制、能碰 DOM,但拿不到页面 window)。脚本猫不支持油猴的auto。@early-start(v1.1.0+)配@run-at document-start让脚本比页面更早加载,配合CAT_scriptLoaded()判断加载完成。@storageName(实验)指定 Value 存储空间,同名的多个脚本可共享数据、跨脚本通信。脚本猫特有。@definition指向一个.d.ts,给编辑器提供自动补全。@resource/@require/@require-css支持资源完整性校验:#sha384-xxx(W3C 建议用 sha384 及以上,别用 md5/sha1)。@connect声明目标站;GM_download的native模式也看它(标准油猴不看)。
完整元数据清单见官方文档 <https://docs.scriptcat.org/docs/dev/meta/>,详细表格在 references/scriptcat-specifics.md。
第三步:核心工程模式
完整 API 签名见 references/gm-api.md(GM_)和 references/cat-api.md(CAT_)。下面是最常写错的几个模式。
1. 等待元素出现(SPA 页面必用)
现代站(Vue/React)DOM 是异步渲染的,直接 querySelector 十有八九拿到 null:
// 轮询式:简单可靠
async function waitForEl(selector, timeout = 10000, root = document) {
const start = Date.now();
while (Date.now() - start < timeout) {
const el = root.querySelector(selector);
if (el) return el;
await new Promise(r => setTimeout(r, 200));
}
throw new Error(`等待超时: ${selector}`);
}
// MutationObserver 式:响应更快,适合频繁变动的节点
function onElAppear(selector, cb, root = document.body) {
const check = () => {
const el = root.querySelector(selector);
if (el) { obs.disconnect(); cb(el); }
};
const obs = new MutationObserver(check);
obs.observe(root, { childList: true, subtree: true });
check(); // 处理已存在的情况
return () => obs.disconnect();
}
2. 模拟点击与表单填写
页面若做事件校验(e.isTrusted),普通 .click() 无效 — 需要真实触发流程(先 pointerdown/mousedown 再 mouseup/click),或直接用脚本猫的 GM_openInTab / 手动 dispatchEvent 序列。这是滑块类验证码的核心难点,详见文末相关技能。
function setInputValue(input, value) {
// 必须用 getPrototypeOf(input),不要直接引 HTMLInputElement.prototype:
// 沙盒环境下脚本与页面不在同一个 realm,构造函数不同,直接引会拿不到描述符
const setter = Object.getOwnPropertyDescriptor(
Object.getPrototypeOf(input), 'value')?.set;
if (setter) setter.call(input, value); // 绕过框架的数据绑定
else input.value = value;
input.dispatchEvent(new Event('input', { bubbles: true }));
input.dispatchEvent(new Event('change', { bubbles: true }));
}
> 上面这种 setter 写法是专门针对 React / Vue 受控输入框的(框架自己劫持了 value 描述符);普通原生表单直接 input.value = x 就够了。
3. 跨域请求
// @grant GM_xmlhttpRequest
// @connect api.example.com
GM_xmlhttpRequest({
method: 'POST',
url: 'https://api.example.com/data',
headers: { 'Content-Type': 'application/json' },
data: JSON.stringify({ a: 1 }),
responseType: 'json', // text|arraybuffer|blob|json|document|stream
timeout: 15000,
onload: res => console.log(res.response),
onerror: err => console.error(err),
ontimeout: () => console.error('超时'),
});
- 必须
@connect目标域名,否则用户会被反复弹确认框 anonymous: true+ 传cookie时只发指定的 cookie- ⚠️ v0.17.0 起
maxRedirects废弃,改用redirect: 'follow'|'error'|'manual' - ⚠️ Firefox 下 cookie 相关功能不支持
4. 值存储
// @grant GM_getValue
// @grant GM_setValue
// @grant GM_deleteValue
// ❌ 错误:写操作是异步的,紧接着读/关页面会丢数据
GM_setValue('k', 'v');
location.reload();
// ✅ 正确:await Promise 风格
await GM.setValue('k', 'v');
await GM.deleteValue('k');
> 坑:GM_setValue('k', undefined) 在脚本猫里是删除该键(标准油猴是存 undefined 这个值)。
5. 插入样式与元素(可绕 CSP)
// @grant GM_addStyle
// @grant GM_addElement
GM_addStyle('.ad-banner { display: none !important; }');
GM_addElement('script', { src: 'https://cdn.example.com/x.js' });
6. 菜单命令与通知
// @grant GM_registerMenuCommand
// @grant GM_notification
GM_registerMenuCommand('立即执行', () => main(), { accessKey: 'r' });
GM_notification({ title: '完成', text: '签到成功', timeout: 5000, onclick: () => {} });
name传空字符串且无listener→ 渲染成右键菜单分隔线- 同名菜单默认只显示一个,需要区分请传
id或individual: true - 更新已有通知必须用相同的
tag progress/buttons以及GM_closeNotification/GM_updateNotification在 Firefox 不支持
7. 劫持 XHR / Fetch(抓数据、改响应)
要点:必须在 @run-at document-start 注入,赶在页面自己的代码拿到 XMLHttpRequest / fetch 之前替换掉。
// @run-at document-start
const _open = XMLHttpRequest.prototype.open;
const _send = XMLHttpRequest.prototype.send;
const origFetch = window.fetch;
// 替换原型方法,包一层自己的读/写逻辑
addEventListener 劫持同理。这块展开太長,完整写法见 references/scriptcat-specifics.md。
第四步:脚本猫特有 — 后台脚本与定时脚本
cron 表达式
标准 5 位 分 时 日 月 周(推荐)。也支持 6 位(首字段为秒),但官方不推荐——浏览器不保证秒级精度且有额外开销。周位 0 或 7 = 周日,1-6 = 周一至周六。
// @crontab * * * * * 每分钟
// @crontab 0 9 * * * 每天 09:00
// @crontab */30 9-15 * * 1-5 工作日 9-15 点每 30 分钟
// @crontab * * once * * 每天最多一次(时间点不限)
// @crontab * once(9-17) * * * 9~17 点每小时各一次
once 语义:同一周期内只要成功执行过一次,其余命中时间点全部忽略。once 写在第几位 = 限制哪一级周期(写在"日"位就是每天一次)。浏览器久未打开或重启过,用 once 能避免重复执行。
> ⚠️ 一个脚本里只有第一个 @crontab 生效。
> ⚠️ 单次执行时间 + 重试延迟 必须小于 cron 间隔,否则会出现执行重叠。
异步完成与重试
含异步操作时必须返回 Promise,且在逻辑真正结束后才 settle——settle 之后管理器认为本次运行结束,后续 GM 操作不再生效。
// @grant GM_xmlhttpRequest
// @crontab * * once * *
return new Promise((resolve, reject) => {
GM_xmlhttpRequest({
url: 'https://example.com/checkin',
onload: res => resolve('ok'),
onerror: () => reject(new CATRetryError('签到失败', 30)), // 30 秒后重试
});
});
CATRetryError(msg, seconds) 触发重试,最小重试间隔 5 秒。
第五步:必读坑位清单
写完代码对照检查:
- [ ] 每个用到的 GM/CAT API 都写了
@grant?用unsafeWindow也要单独声明 - [ ] 用到跨域请求/cookie/下载,
@connect声明了吗? - [ ]
GM_setValue/deleteValue/setValues/deleteValues之后有await,或没立刻window.close()? - [ ] 有没有
GM_setValue(k, undefined)(脚本猫里等于删除)? - [ ] 目标站是 SPA 吗?元素获取有没有做等待而不是一次性查询?
- [ ]
@match覆盖了所有需要的 URL?写了https://不会命中http:// - [ ] 劫持类方案配了
@run-at document-start吗? - [ ] 后台/定时脚本里有没有碰 DOM /
GM_getTab?(都不支持) - [ ] cron 执行时长 + 重试 < 间隔?
- [ ] 用了
GM_getResourceText取图片等二进制资源?(会返回空字符串,要改用GM_getResourceURL) - [ ] 需要兼容 Firefox 吗?(部分 API 不支持,见
references/gm-api.md备注列)
第六步:调试
- 开启允许用户脚本:Chromium 内核 ≥138 → 扩展详情页打开「允许用户脚本」;<138 → 扩展管理页开「开发人员模式」。改完要重启扩展或浏览器才生效。
- 普通脚本:打开 F12 控制台,
console.log照常输出。 - 后台/定时脚本:
- 脚本列表页把鼠标悬停在运行状态栏 → 看运行状态、下一次执行时间
- 点击查看GM_log打印的运行日志(支持 level:GM_log(msg, 'warn'))
- 编辑器内也能调试后台脚本,但已知问题:value 同步异常、菜单命令不触发,要真机验证就去扩展设置开开发者模式后调试background.html - API 精确签名以
scriptcat.d.ts或内置编辑器提示为准(网页文档可能有滞后)。
安装与迭代闭环(本机已具备)
这台机器已把脚本猫接进 WorkBuddy(scriptcat-mcp-bridge 技能),所以写完可以直接推:
- 写好的脚本存成
.user.js scripts_install_request(传code或本地文件路径 /url)→ 浏览器弹确认页等你批准- 验证效果,需要修改时用
scripts_edit_request发{oldText, newText}差异(不必先读源码) - 临时停用用
scripts_toggle_request
> 读源码(scripts_source_get)和所有写操作都会弹确认页并阻塞等待,只能在你坐在电脑前时使用。
参考资料
- 官方开发文档 <https://docs.scriptcat.org/docs/dev/>
- 官方元数据参考 <https://docs.scriptcat.org/docs/dev/meta/>
- GM/CAT API 参考 <https://docs.scriptcat.org/docs/dev/api/> · <https://docs.scriptcat.org/docs/dev/cat-api>
- 官方示例集 <https://github.com/scriptscat/scriptcat/tree/main/example>
- 教程站 learn.scriptcat.org(脚本猫维护的中文《油猴开发指南》):入门篇 17 篇 / 中级篇 11 篇 / 高级篇 5 篇 / 工程篇 6 篇 / 实战篇 10 篇 / 原理篇 2 篇 / 番外篇 5 篇
- 社区:油猴中文网 <https://bbs.tampermonkey.net.cn> · 脚本站 <https://scriptcat.org> · GreasyFork <https://greasyfork.org>
- 更细的表格见
references/目录