油猴脚本这四个字,你可能已经见过很多次了——去广告、页面重排、自动展开折叠内容、给视频网站加倍速,这些不起眼的小功能装上之后就再也回不去了。但我发现一个很有意思的现象:很多人用了好几年油猴脚本,却从没想过自己动手写一个。原因倒也不复杂,大家总觉得写脚本需要完整的前端工程能力,其实油猴脚本开发的入门门槛低到难以想象。你只要会一点 JavaScript,甚至现学都来得及,就能从“只能等现成脚本”的消费者,变成“随手解决网页痛点”的开发者。这篇入门教程,就是带零基础或弱基础的你,从装工具开始,一步一步写出一个真正属于自己的油猴脚本,并且搞懂它背后的运行逻辑。
1. 这脚本到底能干嘛:油猴脚本能解决的问题和它的边界
1.1 一句话讲清楚油猴脚本是什么
油猴脚本(UserScript)本质上是一段运行在浏览器页面上下文里的 JavaScript,由脚本管理器这个浏览器扩展负责注入和执行。你可以把它理解成“网页的浏览器插件”——但不需要过审、不需要打包、不考虑跨浏览器兼容性打包工具,改一行代码,刷新页面就能看到效果。
它和正经浏览器扩展的区别在于:扩展是独立进程,能监控所有标签页、修改浏览器界面;油猴脚本只能在你指定的网站页面里运行,但它保留了“随改随生效”的轻量开发体验。很多场景里,写一个扩展要配置 manifest、打包、上传审核,写一个油猴脚本只要新建一个文件,手写几行元数据,保存即用。这也是为什么大量前端工程师、测试工程师、运维在遇到内部提效需求时,第一反应是“写个油猴脚本吧”。
我用过油猴脚本做过的事情包括:自动填写内部系统的重复表单、在项目管理页面给每个任务标题后面加“复制链接”按钮、把某网站默认的小字号改成适合阅读的排版、定时轮询某个数据接口并把结果用桌面通知弹出来。这些都是几十行代码能搞定的活,如果都去做成浏览器扩展,时间和收益完全不成比例。
1.2 能做和不能做,边界要心里有数
能做:
- 操作页面 DOM:增删改查节点、改样式、加事件,页面怎么折腾都行。
- 拦截和增强请求:可以主动发起跨域请求,也可以在页面现有请求返回后做二次处理。
- 跨页面存数据:通过 GM_setValue 之类的 API,脚本可以自己保存配置和状态,不受页面刷新影响。
- 加菜单项:在脚本管理器的菜单里加自定义按钮,用来开关功能、执行操作。
- 发桌面通知:适合做长任务完成提醒、状态变化提醒。
不能做的,或者说做不好的:
- 不能访问浏览器系统 API,比如下载管理、书签管理等需要 extension API 的能力。
- 不能修改浏览器底层的网络栈、证书体系、代理规则这些“系统级”配置。
- 不能在所有页面之外的地方运行,比如浏览器新标签页这种特殊页面,很多都注入不进去。
- 如果目标页面有严格的内容安全策略(CSP),有些动态执行代码的操作会受限。
把这些边界想清楚,选型的时候就不会尴尬。比如你想做一个“拦截下载链接并自动重命名文件”的工具,那大概率还是得老实写一个浏览器扩展;但如果你只想让某个后台管理系统的表格更好看、更好点,那油猴脚本就是最合适的工具。
1.3 这篇文章适合谁
- 会一点 JavaScript 但没做过完整项目的新手:照着抄、改一改就能出成果。
- 经常被重复网页操作困扰的效率党:自己动手消灭重复劳动。
- 前端同学:油猴脚本是你做日常脚本工具、验证页面交互想法的最快路径。
- 测试、运维、运营这类经常处理内部系统的人:写脚本解决内部平台“难用”的问题,性价比极高。
不需要你懂 Node.js、不需要会打包工具、不需要有服务器。油猴脚本开发的环境,其实就是一个浏览器加一个脚本管理器。
2. 开发环境准备:装扩展、开控制台、找试验田
2.1 安装脚本管理器:Tampermonkey 是我一直在用的
油猴脚本必须靠脚本管理器来运行。市面上主流的有 Tampermonkey(篡改猴)、Violentmonkey(暴力猴)、Greasemonkey(油猴的原版)。我推荐 Tampermonkey,理由很实在:
- 兼容性最好,Chrome、Edge、Firefox、Safari 都能装,而且 GM API 实现得最完整。
- 脚本编辑界面自带语法高亮,对新手友好。
- 有导入导出、脚本备份功能,换电脑迁脚本很省事。
- 更新频率高,对新版浏览器的适配快。
安装步骤不复杂,打开 Edge 或 Chrome 的扩展商店,搜索 Tampermonkey,点安装即可。装完后浏览器工具栏会出现一个黑色方块的图标。要注意,装好之后默认是启用的,但个别浏览器会要求你手动开启“允许访问文件 URL”之类的权限,这个看情况开就行,日常开发用不到。
2.2 找一个安全的试验田
你不需要一上来就针对重要网站开发脚本。我发现最好的做法是:找一两个你自己常用、结构相对简单、就算改坏了也不影响工作的普通网站,比如某个资讯类站点、文档类站点,在上面反复试验。
为什么强调“就算是改坏了也不影响工作”?因为开发过程中你会反复刷新页面、改错代码、副作用层出不穷。如果你拿一个公司核心后台当试验田,万一写了个死循环把页面搞卡了,尴尬的是你自己。找个内容型网站,练手的同时还能提高自己的阅读体验,一举两得。
另外,建议你在浏览器里单独开一个用户配置,专门用来跑开发中的脚本。Chrome 和 Edge 都支持创建独立的用户配置文件,这样开发脚本和日常使用互不干扰,脚本出了问题也不会污染你平时的浏览环境。这个小习惯,能让你在调试脚本时少很多心理负担。
2.3 浏览器开发者工具是你最重要的调试台
写油猴脚本不需要装任何 IDE。浏览器自带的开发者工具就是你最核心的开发环境。
按下 F12,你会看到一整套面板,但实际上写油猴脚本最常用的是两个:
- Console(控制台):看脚本的 console.log 输出、报错信息。这是首要调试入口。
- Elements(元素):查看页面结构,确认某个按钮、某个标题对应的 DOM 节点长什么样。
调试时最常见的流程就是:在 Elements 里找到目标节点,确认它的 class 或 id,然后回到 Tampermonkey 的编辑界面里写对应的选择器。这个过程和日常前端开发一模一样,没有任何额外的工具成本。
还有一个很实用的小技巧:在做任何页面 DOM 操作之前,先按 F12 打开 Console,手动输入一行document.querySelector('目标选择器'),看看能不能正确选中元素。这个方法能帮你快速排除选择器写错的问题,省得每次都在脚本里折腾半天才发现是选择器不匹配。
2.4 装上后第一件事:新建一个测试脚本
打开 Tampermonkey 面板,点击“添加新脚本”,会看到一个默认模板。先不去管模板里那些陌生的字段,把内容替换成下面这段最简单的代码,然后保存:
// ==UserScript== // @name 测试脚本 // @namespace https://your-blog.example/ // @version 1.0.0 // @description 测试油猴脚本是否正常运行 // @author you // @match https://example.com/* // @grant none // ==/UserScript== (function () { 'use strict'; console.log('hello from userscript'); })();打开 example.com 这个网站,按 F12 切到 Console,你会看到hello from userscript这行输出。看到这行字,就代表你的环境已经完全搭好了。这只是第一小步,真正的骨架逻辑我们放在下一节讲。
3. 脚本的骨架:看懂并写好 UserScript 头部声明
3.1 一段脚本元数据,决定脚本在什么时候、什么地方跑
Tampermonkey 新脚本模板里有一大堆以// ==UserScript==开头和结尾的注释,这部分叫“元数据块”(metadata block)。很多人刚开始会忽略它,觉得不就是注释吗,没什么用。这是最大的误解——元数据块就是脚本的配置文件,脚本的运行范围、权限、时机全靠它控制。
标准的骨架长这样:
// ==UserScript== // @name 脚本名称 // @namespace https://your-blog.example/ // @version 1.0.0 // @description 脚本功能描述 // @author your-name // @match https://example.com/* // @grant GM_addStyle // @grant GM_setValue // @grant GM_getValue // @run-at document-idle // ==/UserScript==我来逐个解释这些字段,都是开发里经常要打交道的。
3.2 @match、@include、@exclude:精确控制脚本的运行范围
@match是控制脚本在哪些 URL 上执行。它的语法类似于浏览器的通配符匹配,最常用的是*通配符和路径匹配。比如:
https://example.com/*:匹配 example.com 下所有页面。https://example.com/post/*:只匹配 /post/ 路径下的页面。https://*.example.com/*:匹配 example.com 的所有子域名。http://example.com/*:注意,这个只匹配 http 协议,带 https 的要另外写。
还有一个容易忽略的?符号,它只在当前层级生效,比如/post?*会匹配带任意 query 参数的文章页。新手经常会在这里写错,最常见的问题是:@match写得太窄或者太宽。写窄了,脚本怎么都不生效;写太宽,脚本会在所有页面执行,拖慢浏览速度。
我的建议是:能写多窄就写多窄。只针对你要处理的页面路径做匹配,这样既提升脚本执行效率,也避免脚本在其他页面引发莫名其妙的副作用。还有一个老牌字段@include,语法比@match更古老也更宽松,我建议新手一律用@match,因为它的匹配规则更明确、更可控。如果你还需要在某些页面排除脚本,那就用@exclude配合。
3.3 @run-at:选择脚本的执行时机
@run-at控制脚本在页面加载的哪个阶段执行。三个最常用的值:
document-start:页面文档刚开始加载就执行。适合拦截或者修改早期 DOM,比如替换页面标题、阻止某些元素创建。但此时 DOM 可能还没准备好,很多操作会拿不到节点。document-end:DOM 解析完成,但图片样式等资源可能没加载完。这个时机比较均衡。document-idle:页面加载基本完毕,所有资源加载完后执行。大部分脚本推荐用这个,因为此时 DOM 最完整,操作不容易漏。
实操里的经验是:如果你的脚本只是给页面加个按钮、改改样式,无脑用document-idle就好。如果你需要监听页面最初的一段内容变化,或者要在最早期就注入样式避免闪烁,才考虑document-start。
3.4 @grant:脚本向脚本管理器申请权限
@grant是另一个容易踩坑的字段。它的作用是声明脚本需要使用哪些 GM API,比如GM_addStyle、GM_setValue、GM_xmlhttpRequest等。如果不声明,这些函数在脚本里就是 undefined,调用会直接报错。
有一个特殊值:@grant none。它的意思是脚本不申请任何 GM API,完全以页面普通脚本的方式运行。这个模式性能最好、最符合原生态,但它会损失所有 GM 函数的能力。很多纯 DOM 操作的脚本用@grant none就够了,不需要声明一大串授权。
如果拿到别人的脚本模板,它默认可能是@grant none,你新写了一个GM_addStyle的调用,却忘了在头部加@grant GM_addStyle,那么脚本保存后只会静默失败,控制台报一个GM_addStyle is not defined。定位方法也简单:用到哪个 GM API,就在@grant里加哪个,不加就不能用。
3.5 为什么脚本主体要用 IIFE 包起来
元数据块下面,所有的脚本代码都被包在一个 IIFE(立即执行函数表达式)里:
(function () { 'use strict'; // 你的代码 })();这么写主要有两个原因。第一,隔离作用域。油猴脚本里的变量如果不包一层,会直接挂到全局对象上,一旦多个脚本用了同样的通用变量名,就会互相污染,排查起来非常痛苦。第二,'use strict'严格模式能帮你提前暴露一些低级的变量声明错误,比如忘记用 let/const 导致变量意外全局化之类的。
我见过一些新手把脚本写成一列裸代码,不包函数,结果在某个页面同时装两个脚本的时候,动不动就“xxx is not defined”。从第一天开始习惯 IIFE 包裹,可以避免掉这一整类问题。
4. 核心 API 实战:存储、跨域、菜单、样式、通知一个都不少
4.1 GM_setValue / GM_getValue:解决页面刷新后数据丢失问题
很多时候,脚本需要保存一些用户配置,比如“开关是否打开”“默认选择的是哪个值”。如果你用的是 localStorage,会发现它绑定在当前页面域名下,换个页面、清理缓存可能就没了,而且不同脚本之间共享同一个 localStorage 键值空间,容易出现命名冲突。
GM_setValue 和 GM_getValue 是脚本管理器提供的独立存储 API,数据由脚本管理器统一管理,和页面本身完全隔离。用法就像 Map 一样简单:
// 存 GM_setValue('progressBarEnabled', false); // 取,第二个参数是默认值 const enabled = GM_getValue('progressBarEnabled', true);这里要养成的习惯是:读取配置时永远给一个默认值。因为脚本第一次运行时,配置项大概率还不存在,你不给默认值,拿到的就是 undefined,后续逻辑用默认值兜底会健壮很多。实际项目中,我把“脚本开关状态”这类配置存在 GM_setValue 里,切换页面也不会丢,体验比 localStorage 稳得多。
4.2 GM_xmlhttpRequest:绕开页面跨域限制,请求外部接口
这是油猴脚本最有魅力的 API 之一。普通的网页 fetch 或 XHR 会受到浏览器同源策略和 CORS 限制,跨域请求经常被拦截。但 GM_xmlhttpRequest 是由脚本管理器这个扩展发起的,不会受到页面 CORS 策略约束。这意味着你可以直接请求一个完全不同的第三方 API,把数据拿回来展示到当前页面。
使用方法:
// ==UserScript== // @grant GM_xmlhttpRequest // ==/UserScript== GM_xmlhttpRequest({ method: 'GET', url: 'https://api.example-weather.org/v1/current?city=beijing', onload: function (res) { console.log(res.status); console.log(res.responseText); }, onerror: function (err) { console.log('请求失败', err); } });有几个细节要提醒。第一,虽然它不受 CORS 限制,但目标服务器仍然可能校验 Referer、User-Agent 等请求头,所以不是所有请求都一定成功。第二,请求频率千万要克制。你是用自己的浏览器在访问第三方服务,不是给你的服务器集群做压测,一旦频率太高,轻则被目标服务封 IP,重则影响脚本作者的名声。第三,回调是异步的,别在 GM_xmlhttpRequest 调用后面直接同步使用返回值,这也是新手最容易犯的错。
4.3 GM_registerMenuCommand:在脚本管理器菜单里加上自己的按钮
一个脚本如果只有“进页面自动改”,那功能就比较死板。更灵活的做法是通过菜单命令让用户手动控制脚本行为。GM_registerMenuCommand 可以在脚本管理器的菜单里注册一个自定义选项,点击后执行指定函数,非常适合做“开关功能”“导出数据”“手动触发某操作”。
示例:
// ==UserScript== // @grant GM_registerMenuCommand // ==/UserScript== let progressOn = true; function toggleProgress() { progressOn = !progressOn; progressBar.style.display = progressOn ? 'block' : 'none'; } GM_registerMenuCommand('切换进度条显示', toggleProgress);这个 API 的实际使用体验,等于在脚本运行期间给用户提供了一个控制面板。新手开发时不妨优先考虑把一次性的自动操作,做成菜单里可手动触发的动作,这样即使页面结构变化导致自动触发失效,用户还能通过菜单手动救场。
4.4 GM_addStyle:注入样式最干净的方式
要用油猴脚本改页面样式,有几种方式:直接操作 style 属性、创建 style 标签、用 GM_addStyle。最推荐的是 GM_addStyle,因为它是脚本管理器专门准备的样式注入 API,注入的样式天然带一定的隔离性,并且自动支持后续的更新和管理。
// ==UserScript== // @grant GM_addStyle // ==/UserScript== GM_addStyle(` #my-custom-button { position: fixed; bottom: 20px; right: 20px; z-index: 99999; } `);这里有一个经常遇到的坑:目标网站通常有自己的样式体系,优先级不一定比你低。如果你的样式老是不生效,优先检查是不是被目标网站的样式覆盖了。解决方法是提高选择器优先级,或者加上!important,必要时把 z-index 拉高。比如你要加一个悬浮按钮,z-index 写 9999 都不够用的时候,直接上 99999,基本能保证在绝大多数网站上正常显示。
4.5 GM_notification:把提示推送出来
有些脚本是后台运行的,比如定时刷新页面、监控价格变化、等待某个任务完成。这时候用户不可能一直盯着页面。GM_notification 能在系统桌面弹出一个原生通知,适合做“任务完成提醒”。
// ==UserScript== // @grant GM_notification // ==/UserScript== GM_notification({ title: '监控提醒', text: '目标商品价格已更新,记得去看看', timeout: 5000, onclick: function () { // 用户点击通知后的回调 console.log('notification clicked'); } });我用得最多的场景是:让脚本在后台轮询某个接口,一旦检测到状态变化,就发一条桌面通知。这样你可以放心去干别的活,不用一直开着页面盯着看。要注意的是,部分浏览器需要站点有通知权限,如果系统弹不出来,先看看浏览器设置里通知权限是否被禁了。
5. 完整案例:做一个阅读进度条和返回顶部按钮
5.1 需求分析:为什么选这个例子
理论说了一大堆,我们来落地一个完整例子。我的建议是做一个“阅读进度条 + 返回顶部按钮”的脚本,原因是它麻雀虽小五脏俱全,能覆盖 DOM 创建、样式注入、事件监听、滚动计算这几个最核心的技术点,而且代码量不大,二十多分钟就能写通。更重要的是,这个功能几乎所有内容型网站都适用,你装完马上就能感受到用处。
设想的需求很简单:
- 页面顶部出现一条 3 像素高的进度条,随着页面往下滚动,进度条变长,到页面底部时正好占满 100%。
- 页面右侧悬浮一个“返回顶部”按钮,往下滚动超过 400 像素时显示,点击后平滑滚回顶部。
5.2 第一步:写好元数据声明
// ==UserScript== // @name 阅读进度条和返回顶部 // @namespace https://your-blog.example/ // @version 1.0.0 // @description 在文章页顶部添加阅读进度条,并在右下角显示返回顶部按钮 // @author your-name // @match https://example.com/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript==注意这里的@match我用了占位域名,你实际使用时替换成自己的目标文章站点。因为功能只针对阅读型页面,没必要让脚本在所有页面运行,所以我把匹配范围收紧到 https://example.com/* ,然后又在代码里进一步判断当前 URL 是否符合文章页的特征。
5.3 第二步:注入样式
样式用 GM_addStyle 注入。进度条是固定在页面顶部的一条横线,按钮固定在浏览器的右下角。注意两个元素都要规划好 z-index,避免被目标网站的某些固定元素盖住:
GM_addStyle(` #reading-progress-bar { position: fixed; top: 0; left: 0; height: 3px; width: 0%; background: #1a73e8; z-index: 99999; transition: width .1s linear; pointer-events: none; } #back-to-top-btn { position: fixed; right: 24px; bottom: 48px; width: 40px; height: 40px; line-height: 38px; text-align: center; border-radius: 50%; background: rgba(0, 0, 0, .6); color: #fff; font-size: 22px; cursor: pointer; display: none; z-index: 99999; user-select: none; } `);pointer-events: none给进度条加上,鼠标事件就不会被它挡住,用户选中文本时也不会受到影响。这个细节刚开始很容易忽略,写完之后才发现进度条挡在页面顶部,点都点不动。
5.4 第三步:创建 DOM 节点并绑定事件
有了样式,接下来用 JavaScript 创建元素并追加到页面:
// 创建进度条节点 const progressBar = document.createElement('div'); progressBar.id = 'reading-progress-bar'; document.body.appendChild(progressBar); // 创建返回顶部按钮 const backToTopBtn = document.createElement('div'); backToTopBtn.id = 'back-to-top-btn'; backToTopBtn.textContent = '↑'; document.body.appendChild(backToTopBtn); // 点击返回顶部 backToTopBtn.addEventListener('click', function () { window.scrollTo({ top: 0, behavior: 'smooth' }); }); // 滚动时更新进度条和按钮状态 function updateProgress() { const scrollTop = window.scrollY; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const percent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; progressBar.style.width = percent + '%'; backToTopBtn.style.display = scrollTop > 400 ? 'block' : 'none'; } window.addEventListener('scroll', updateProgress, { passive: true }); // 页面刚打开先执行一次,保证初始状态正确 updateProgress();这里最核心的计算是进度百分比:scrollTop / (scrollHeight - innerHeight) * 100。为什么要scrollHeight - innerHeight?因为滚动条能滚动的最大距离是“文档总高度减去视口高度”。如果不减,页面滚到底时 scrollTop 永远达不到 scrollHeight,进度条就永远到不了 100%。这个公式在很多类“阅读进度”的场景里都能复用,建议直接记下来。
监听滚动时加上{ passive: true },是告诉浏览器这个监听器里不会调用 preventDefault,浏览器可以放心做滚动性能优化。对 WordPress 这类内容站,页面有时会比较重,这个参数能让滚动更顺滑。
5.5 第四步:合成完整代码
把上面的代码组合起来,就可以保存试跑了:
// ==UserScript== // @name 阅读进度条和返回顶部 // @namespace https://your-blog.example/ // @version 1.0.0 // @description 在文章页顶部添加阅读进度条,并在右下角显示返回顶部按钮 // @author your-name // @match https://example.com/* // @grant GM_addStyle // @run-at document-idle // ==/UserScript== (function () { 'use strict'; GM_addStyle(` #reading-progress-bar { position: fixed; top: 0; left: 0; height: 3px; width: 0%; background: #1a73e8; z-index: 99999; transition: width .1s linear; pointer-events: none; } #back-to-top-btn { position: fixed; right: 24px; bottom: 48px; width: 40px; height: 40px; line-height: 38px; text-align: center; border-radius: 50%; background: rgba(0, 0, 0, .6); color: #fff; font-size: 22px; cursor: pointer; display: none; z-index: 99999; user-select: none; } `); const progressBar = document.createElement('div'); progressBar.id = 'reading-progress-bar'; document.body.appendChild(progressBar); const backToTopBtn = document.createElement('div'); backToTopBtn.id = 'back-to-top-btn'; backToTopBtn.textContent = '↑'; document.body.appendChild(backToTopBtn); backToTopBtn.addEventListener('click', function () { window.scrollTo({ top: 0, behavior: 'smooth' }); }); function updateProgress() { const scrollTop = window.scrollY; const docHeight = document.documentElement.scrollHeight - window.innerHeight; const percent = docHeight > 0 ? (scrollTop / docHeight) * 100 : 0; progressBar.style.width = percent + '%'; backToTopBtn.style.display = scrollTop > 400 ? 'block' : 'none'; } window.addEventListener('scroll', updateProgress, { passive: true }); updateProgress(); })();保存后在目标文章页面刷新,往下滚,顶部进度条会跟着走;往下滚超过 400 像素,右下角会出现按钮,点击平滑回顶。整个流程很直观。
这个脚本后续还能扩展:加一个“返回上次阅读位置”的标记,加夜间模式切换,甚至加一个阅读计时器。每加一个功能,你就多练一个知识点,慢慢地就从一个只会抄脚本的人变成能独立设计脚本的人了。
6. 调试、发布与常见问题排查
6.1 调试流程:先看报错,再打日志,最后加断点
油猴脚本开发最常见的问题就是“脚本没生效”,这时候千万别急着改代码。先按 F12 打开控制台,看有没有红色报错,然后一步步排查:
- 确认脚本已启用,且当前页面 URL 匹配上了你写的
@match。 - 在脚本第一行加一个
console.log('script loaded'),刷新页面,看控制台有没有输出。没有输出,说明脚本根本没在这个页面运行,问题基本在@match或浏览器扩展权限上。 - 有输出但后续逻辑没执行,说明是某句代码报错中断了,看控制台的具体报错信息。
- 如果逻辑不稳定,比如有时生效有时不生效,考虑是不是
@run-at时机不对,或者页面加载后又有新的 DOM 被渲染。
如果只是想快速验证一个选择器对不对,我常用的土办法是:直接在控制台输入一行document.querySelectorAll('这里写选择器'),看看返回结果。这个技巧能帮你省去无数轮“改代码-保存-刷新”的循环。
6.2 发布到 GreasyFork:让别人也能用上你的脚本
写好的脚本除了自用,也可以发布到 GreasyFork。这是目前最主流的用户脚本分享平台。发布流程不复杂:
- 注册一个 GreasyFork 账号。
- 点击“发布新脚本”,把元数据块和代码整体粘贴进去。
- 填好描述、标签、支持的浏览器。
- 发布后,其他用户点击“安装此脚本”就能通过 Tampermonkey 直接安装。
发布前有一个容易被忽略的点:版本号。每次更新,都要记得改元数据里的@version,否则用户那边可能永远收不到更新提醒。版本号我习惯用1.0.0起步,小改动 +0.0.1,功能变化较大再升级到1.1.0。
还要给脚本起个好的名字和描述。不要只写“XXX 工具”,要说清楚它解决什么问题,比如“为 XX 页面添加返回顶部按钮和阅读进度条”。这样别人在 GreasyFork 里搜索时更容易找到你的脚本。
6.3 常见问题速查表
| 问题 | 常见原因 | 解决办法 |
|---|---|---|
| 脚本完全不运行,控制台无输出 | @match 不匹配当前URL;脚本被禁用;Tampermonkey 扩展权限异常 | 在脚本文本里临时加 console.log,确认匹配范围;逐个检查 @match |
| GM_xxx 报 undefined | 没有在元数据块里 @grant 对应权限 | 用到哪个 API,就在 @grant 上加哪个 |
| 脚本在页面刷新后丢失状态 | 使用了页面 localStorage 而不是 GM_setValue | 换成 GM_setValue / GM_getValue |
| 样式不生效 | 被目标网站样式覆盖 | 提高选择器优先级,加 !important,必要时提高 z-index |
| 页面很卡 | 脚本在太多页面执行;监听器没做节流 | 收窄 @match,滚动监听改用 passive 并在回调里精简操作 |
| 页面内容动态加载,脚本处理不到 | 脚本只在页面初载时执行了一次 | 用 MutationObserver 监听 DOM 变化,或者监听 URL 变化后重新执行 |
| 与其他脚本变量冲突 | 变量名太通用,污染了全局作用域 | 始终用 IIFE 包裹,变量命名带上脚本前缀 |
6.4 动态页面是最大的坑:MutationObserver 的思路
现在很多网站都是 SPA(单页应用),页面内容会随着路由切换和接口返回不断变化。你脚本只在document-idle时跑一次,明明选中的元素当时还在,过一会儿就被框架重渲染掉了,按钮和样式全没了。
面对这类动态页面,最简单的兜底方案是 MutationObserver。它就像页面的“安保摄像头”,每当 DOM 发生变化就通知你,然后在回调里重新执行你需要的 DOM 操作和事件绑定。
基本用法:
const observer = new MutationObserver(function (mutations) { // 检查目标节点是否回来了,回来了就补一份操作 if (!document.querySelector('#target-node')) return; // 你的初始化逻辑 init(); }); observer.observe(document.body, { childList: true, subtree: true });不过 MutationObserver 不能滥用——只要 DOM 一变就触发,如果你在回调里又去改 DOM,很容易触发连环回调,把页面搞得卡顿。实际开发中我会在回调里加一个简单的检查条件,只有目标节点确实需要处理时才执行初始化逻辑,并且初始化前先做一次“是否已初始化”的判断。
6.5 一些来自经验的小建议
最后分享几个我踩过坑之后养成的习惯。
第一,开发阶段的脚本,名称里加一个“-dev”后缀。这样你能一眼分辨哪个是正在调试的版本,哪个是稳定版。发布到 GreasyFork 的时候再把后缀去掉,更新版本号,避免用户安装到测试版。
第二,代码里所有 ID、class 名都加上自己的前缀,比如rp-progress、rp-backtop这种。页面是别人的,class 名冲突不可控,自己加前缀能大幅度减少样式和选择器相互干扰的概率。
第三,写完脚本后不要急着加功能。先用一段时间,看看有没有误伤,结构是否稳定。油猴脚本最大的风险不是写不出来,而是写得太多、太复杂,最后自己都维护不动。把一个痛点解决干净,比堆砌十个功能更有价值。
第四,改完脚本要刷新页面才生效。这个听起来像废话,但真的很多人忘了,Tampermonkey 里保存的脚本不会自动重新注入当前已打开的页面。如果你在某个页面上调试,改完代码后记得 Ctrl+Shift+R 强制刷新,确保拿到的是最新脚本。