写过几个油猴脚本,经常对页面请求返回的数据进行拦截或者覆盖,这篇文章就做个总结,涉及到 fetch
和 xhr
两种类型的请求。
环境搭建
先简单写个 html
页面,搭一个 koa
服务进行测试。
html
页面提供一个 id=json
的 dom
用来加数据,后边我们补充 test.js
文件来请求接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>Document</title> </head> <body> 我运行了 <div id="json"></div> </body> <script src="test.js"></script> </html>
|
将 html
通过 VSCode
的 live-server
插件运行在 http://127.0.0.1:5500/
上。
安装 koa
和 koa-route
的 node
包,提供一个接口。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31
| const koa = require("koa"); const app = new koa(); const router = require("koa-router")(); router.get("/api/query", async (ctx, next) => { ctx.body = { data: [1,2,3], code: 0, msg: "成功", }; });
app.use(async (ctx, next) => { ctx.set("Access-Control-Allow-Origin", "http://127.0.0.1:5500"); ctx.set( "Access-Control-Allow-Headers", "Content-Type, Content-Length, Authorization, Accept, X-Requested-With" ); ctx.set("Access-Control-Allow-Methods", "PUT, POST, GET, DELETE, OPTIONS"); if (ctx.method === "OPTIONS") { ctx.body = 200; } else { await next(); } });
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3002);
|
提供了 /api/query
接口,返回 data: [1,2,3],
。运行在本地的 3002
端口上,并且设置跨域,允许从 http://127.0.0.1:5500
访问。
油猴脚本
先简单写一个插入 我是油猴脚本的文本
的脚本,后边再进行修改。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
|
(function () { console.log(window.unsafeWindow) const dom = document.createElement("div"); dom.innerText = '我是油猴脚本的文本' document.getElementsByTagName("body")[0].append(dom); })();
|
此时页面已经被成功拦截:
这里提一句,油猴脚本如果使用 @grant
申请了权限,此时脚本会运行在一个沙箱环境中,如果想访问原始的 window
对象,可以通过 window.unsafeWindow
。
并且我们加了 @run-at
,让脚本尽快执行。
fetch 请求
在 html
请求的 test.js
中添加 fetch
的代码。
1 2 3 4 5 6
| fetch("http://localhost:3002/api/query") .then((response) => response.json()) .then((res) => { const dom = document.getElementById("json"); dom.innerText = res.data; });
|
看下页面,此时就会把 data
显示出来。
如果想更改返回的数据,我们只需要在油猴脚本中重写 fetch
方法,将原数据拿到以后再返回即可。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34
|
(function () { console.log(window.unsafeWindow) const dom = document.createElement("div"); dom.innerText = '我是油猴脚本的文本' document.getElementsByTagName("body")[0].append(dom); const originFetch = fetch; console.log(originFetch) window.unsafeWindow.fetch = (url, options) => { return originFetch(url, options).then(async (response) => { console.log(url) if(url === 'http://localhost:3002/api/query'){ const responseClone = response.clone(); let res = await responseClone.json(); res.data.push('油猴脚本修改数据') const responseNew = new Response(JSON.stringify(res), response); return responseNew; }else{ return response;
} }); }; })();
|
对 response
的处理有点绕,当时也是试了好多次才试出了这种方案。
做的事情就是把原来返回的 respones
复制,通过 json
方法拿到数据,进行修改数据,最后新生成一个 Response
进行返回。
看下效果:
成功修改了返回的数据。
xhr
我们将 fetch
改为用 xhr
发送请求,因为页面简单所以请求可能在油猴脚本重写之前就发送了,正常网站不会这么快,所以这里加一个 setTimeout
进行延时。
1 2 3 4 5 6 7 8 9 10
| setTimeout(() => { const xhr = new XMLHttpRequest(); xhr.open('GET', 'http://localhost:3002/api/query'); xhr.send(); xhr.onload = function() { const res = JSON.parse(this.responseText); const dom = document.getElementById("json"); dom.innerText = res.data; }; }, 0)
|
和 fetch
的思路一样,我们可以在返回前更改 responseText
。
重写 XMLHttpRequest
原型对象的 open
或者 send
方法,在函数内拿到用户当前的 xhr
实例,监听 readystatechange
事件,然后重写 responseText
。
1 2 3 4 5 6 7 8 9 10 11 12 13
| const originOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (_, url) { if (url === "http://localhost:3002/api/query") { this.addEventListener("readystatechange", function () { if (this.readyState === 4) { const res = JSON.parse(this.responseText); res.data.push("油猴脚本修改数据"); this.responseText = JSON.stringify(res); } }); } originOpen.apply(this, arguments); };
|
运行一下:
拦截失败了,网上搜寻下答案,原因是 responseText
不是可写的,我们将原型对象上的 responseText
属性描述符打印一下。
可以看到 set
属性是 undefined
,因此我们重写 responseText
失败了。
我们无法修改原型对象上的 responseText
,我们可以在当前 xhr
对象,也就是 this
上边定义一个同名的 responseText
属性,赋值的话有两种思路。
1、直接赋值
我们定义一个 writable: true,
的属性,然后直接赋值为我们修改后的数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| const originOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (_, url) { if (url === "http://localhost:3002/api/query") { this.addEventListener("readystatechange", function () { if (this.readyState === 4) { const res = JSON.parse(this.responseText); Object.defineProperty(this, "responseText", { writable: true, }); res.data.push("油猴脚本修改数据"); this.responseText = JSON.stringify(res); } }); }
originOpen.apply(this, arguments); };
|
看下页面会发现成功拦截了:
2、重写 get
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23
| const originOpen = XMLHttpRequest.prototype.open; XMLHttpRequest.prototype.open = function (_, url) { if (url === "http://localhost:3002/api/query") { const xhr = this; const getter = Object.getOwnPropertyDescriptor( XMLHttpRequest.prototype, "response" ).get; Object.defineProperty(xhr, "responseText", { get: () => { let result = getter.call(xhr); try { const res = JSON.parse(result); res.data.push('油猴脚本修改数据') return JSON.stringify(res); } catch (e) { return result; } }, }); } originOpen.apply(this, arguments); };
|
我们拿到原型对象的 get
,然后在当前对象上定义 responseText
的 get
属性,修改数据后返回即可。
相比于第一种方案,这种方案无需等待 readystatechange
,在开始的时候重写即可。
需要注意的是,上边方案都只是重写了 responseText
字段,不排除有的网站读取的是 response
字段,但修改的话和上边是一样的,这里就不写了。
总
通过对 fetch
和 xhr
的重写,我们基本上可以对网页「为所欲为」了,发挥想象力通过油猴脚本应该可以做很多有意思的事情。