油猴用户脚本开发心得

文档参考

注意

  • 以下内容均为在特定时间特定环境的个人经验之谈,可能会在以后或不同环境中不适用。

Greasemonkey/Tampermonkey API相关

  • 自某个版本开始有了 GM.* 形式的API,可以用于取代旧版本的 GM_* 形式的API。
    • GM.* 的API函数有些是异步函数,具体请查看文档,而 GM_* 的API函数是同步的。

unsafeWindow

  • 在声明了 @grant unsafeWindow 之后,脚本上下文中的 window 会被替换成一个由插件提供的”沙箱window”。
  • 在沙箱环境下,Worker 以及一些API可能会无法工作。
  • 可以使用 window = unsafeWindow; 的方式脱离沙箱环境,但要保证这行在需要脱离沙箱的代码之前。另外也可以保留一个原来的 window 的引用以备用。

ECMAScript 相关

WorkerDedicatedWorkerGlobalScope

  • Worker 可以通过如下方法来实现从代码直接加载而不用URL以实现跨域加载 Worker

    1
    2
    3
    4
    const content = 'Worker中要执行的代码';
    const url = URL.createObjectURL(new Blob([content], { type: 'text/javascript' }));
    const worker = new Worker(url);
    // ...
  • 参考Web workers without a separate Javascript file?

  • 还有一种可行的从跨域URL加载的方式,简单说就是在上述方法中使用 importScripts 加载启用CORS的域的代码

1
2
3
const URL = 'xxxxxx';
const content = `importScripts(${URL});`;
const worker = new Worker(URL.createObjectURL(new Blob([content], { type: 'text/javascript' })));

SharedWorkerSharedWorkerGlobalScope

  • SharedWorker 在通过上述方式使用时没有效果(但这种方法不会提示错误,是否真的不可用还有待研究),由于同源策略也无法执行你自己的脚本。目前还没有找到一个合理有效的方法在用户脚本中使用 SharedWorker
  • 参考How can I load a shared web worker with a user-script?
  • 注:上述页面中的部分答案可能有用,但这不应该是 SharedWorker 本身应用的场景

ServiceWorkerServiceWorkerGlobalScope

  • ServiceWorkerSharedWorker 没有合理有效的方法在用户脚本中使用,但使用上述方法时均直接提示错误,因此目前这些方法对 ServiceWorker 均不可用

模块化

从URL中加载的方式 import / export

  • 从某个版本开始,各大浏览器实现了动态import,因此可以通过这种方式在用户脚本中从URL加载模块(Module)

  • 示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    // 模块中的代码
    export function f () { console.log('here is function f'); }
    export const k = 1;
    export default function func () { console.log('here is function func'); }

    // 用户脚本或其他地方的代码,注意动态import的使用方式和静态import的使用方式有所不同
    const mod = await import(URL);
    console.log(mod.f, mod.f());
    console.log(mod.k);
    console.log(mod.default); // 注意这里,default指代的是用export default导出的东西
  • Web Worker 中暂时无法使用import,参考Web Workers - How To Import Modules

    • Chrome 80 修复了这个bug

直接执行代码的方式 eval / Function / AsyncFunction

  • 如果想要异步加载,需要保证加载过程的代码中的每个异步函数都有正确地await,否则可能导致加载顺序不一致
  • 关于evalFunction性能问题:在 Chrome 中测试得到 eval有最高的效率;在 Firefox 中测试得到 Function 更快。此外,在 Chrome 中的执行速度比 Firefox 整体快了数倍

网页请求相关

CORS

  • 在用户脚本中跨域获取HTTP请求自己的文件可以通过脚本的@resource实现,或把文件挂在代码托管的网站上,因为这些网站通常是默认开启CORS的

  • 如果需要请求他人的文件(如调用API等)可以使用GM.xmlHttpRequest

  • CORS对Worker/SharedWorker/ServiceWorker都不能产生效果,因此你不能通过如下方式从跨域URL加载

    1
    2
    3
    var blockedWorker = new Worker("https://not-example.com/");
    blockedWorker = new SharedWorker("https://not-example.com/");
    navigator.serviceWorker.register('https://not-example.com/sw.js');
  • 参考Why can I fetch this Cross origin file but I can’t create a Worker from it?

  • importScripts受CORS控制,因此可以用这个方式解决上述的问题,见上文

Content-Security-Policy

  • 如果网站启用CSP并且作出限制,用户脚本基本上没法做被限制的操作,不过我目前没有遇到

GM.xmlHttpRequest / XMLHttpRequest / Fetch_API 对比

特性 GM.xmlHttpRequest XMLHttpRequest Fetch_API
跨域请求 油猴控制 CORS控制 CORS控制
修改User-Agent 有效 无效 无效
修改Host 有效 无效 无效
修改Origin 有效 无效 无效
修改Referer 有效 无效 受限
修改Cookie 有效 无效 无效
修改Sec- 有效 无效 无效
HTTP 2 不支持 支持 支持
  • 此外在使用 GM.xmlHttpRequest 的时候要自行填写所有需要的 Header,这可能会带来麻烦和其他风险

  • 表中是否有效的判断是指是否能通过给定的API来打到对应的目的

  • Fetch_API 中的 受限 是指可以修改为同页面下的任意URL

    • 示例
    1
    2
    3
    4
    5
    6
    7
    // 假定在 http://example.com/demo 下进行的请求
    fetch('http://example.com/demo/api', {
    method: 'GET',
    referrerPolicy: 'unsafe-url',
    referrer: 'http://cross-origin.com/balabala' // 在这种情况下,设置的 referrer 无效,最终的 Referrer 不变
    referrer: 'ThisIsReferrer' // 在这种情况下,最终的 Referrer 为 http://example.com/demo/ThisIsReferrer
    });
  • 测试环境为 Chrome 80 下的默认设置,TamperMonkey 4.9