一. 前言
B站页面普遍采用的模式, 主html
带有部分初始化页面的数据, 之后的更多数据采用异步加载的方式实现, 这是一种经典的即保证页面渲染呈现, 同时不影响页面的加载速度的页面模式.
首页
顶部一小部分的视频
检索结果页面
第一个搜索页面
当搜索的的url
不是首页, 理论上应该会出现以下错误:
https://search.bilibili.com/all?vt=50926548&keyword=%E6%9F%AF%E5%8D%97&from_source=webtop_search&spm_id_from=333.880&search_source=5&page=2&o=30
播放页面
侧边栏的相关视频, 当加载下一个视频, 相关的信息载入就是通过api
加载.
二. 分析过程
打开控制板 => 网络
=> xhr
.
找到发起请求的脚本来源: https://s1.hdslb.com/bfs/static/laputa-search/client/assets/index.d50424d6.js.
代码很容易阅读, 就是对fetch
的封装(对于大型站点而言, B站不管是页面还是代码(没有过多的历史兼容包袱, 没有刻意的代码混淆...), api都绝对是良心的体现, 干净, 简单, 实用, 希望B站的基业长青吧).
// 代码太长, 删掉小部分
Fa = class {
constructor(e={}) {
re(this, "defaults");
re(this, "_req");
re(this, "_techPbCodeRep");
this.asyncDataKey = is,
this.serverConfigKey = Yi,
this.defaults = I(I({}, sr), e),
this._techPbCodeRep = !1,
typeof fetch == "function" ? this._req = fetch.bind(window) : this._req = ()=>{
throw new Error("no request function")
}
}
create(e={}) {
return new Fa(e)
}
$get(e, t={}, s) {
let n;
return s ? n = we(I({}, s), {
params: t
}) : t.params ? n = t : n = {
params: t
},
n.method = "GET",
n.params && (e = fn(e, n.params)),
this._send(e, n)
}
$post(e, t={}, s) {
let n;
return s ? n = we(I({}, s), {
body: t
}) : t.body ? n = t : n = {
body: t
},
n.method = "POST",
n.withCSRF && (n.body = n.body || {},
n.body.csrf = zi("bili_jct")),
Wi(n),
this._send(e, n)
}
get() {
var e = arguments;
return new Promise((t,s)=>G(this, null, function*() {
const [n,a] = yield this.$get(...e);
if (n) {
s(n);
return
}
t(a)
}))
}
post() {
var e = arguments;
return new Promise((t,s)=>G(this, null, function*() {
const [n,a] = yield this.$post(...e);
if (n) {
s(n);
return
}
t(a)
}))
}
_send(e, t) {
const s = I({}, t)
, n = ji(this.defaults.baseURL, e);
return new Promise(a=>{
var o;
try {
const i = ((o = s == null ? void 0 : s.ctx) == null ? void 0 : o.headers) || {};
delete i.host,
delete i["accept-encoding"],
s.headers = I(I({
Accept: "application/json, text/plain, */*"
}, i), s.headers || {}),
s.credentials = s.credentials || this.defaults.credentials;
const r = er("_sendUrl", n, s)
, u = Qi(n, s);
if (u) {
r.finish(),
a([null, u]);
return
}
let h = !1;
const g = Ji(n, s)
, f = s.timeout || this.defaults.timeout
, _ = s.onRequest || this.defaults.onRequest
, w = s.onResponse || this.defaults.onResponse
, p = E=>{
let C;
const x = Uo
, L = new x
, y = setTimeout(()=>{
if (L.abort(),
h = !0,
E) {
r.setTag("error", !0),
a([`request ${n} TIMEOUT`, null]);
return
}
r.setTag("retry_url", n),
p(!0)
}
, f);
if (g)
if (Ui(g))
C = g;
else {
clearTimeout(y),
r.finish(),
a([null, g]);
return
}
if (!C)
if (_) {
const b = _({
url: n,
config: s
});
C = this._req(b.url, we(I({}, b.config), {
signal: L.signal
}))
} else
C = this._req(n, we(I({}, s), {
signal: L.signal
}));
const m = (b,k)=>{
var D, P, V, N, O, se, ne, X, ce, j, Q, ee, F, q, J, ie;
if (!He && this._techPbCodeRep && (window == null ? void 0 : window.__biliMirror__)) {
const $e = `${(V = (P = (D = window.__biliMirror__) == null ? void 0 : D.options) == null ? void 0 : P.origin) != null ? V : 0}.${(se = (O = (N = window.__biliMirror__) == null ? void 0 : N.options) == null ? void 0 : O.module) != null ? se : 0}`;
(ie = No) == null || ie({
type: (k == null ? void 0 : k.ok) === !1 ? "error" : "custom",
eventId: $e + ((k == null ? void 0 : k.ok) === !1 ? ".ERROR.catchError" : ".DATA.requestReport"),
msg: {
code: (X = (ne = k == null ? void 0 : k.code) != null ? ne : k == null ? void 0 : k.status) != null ? X : "emptyCode",
msg: (j = (ce = k == null ? void 0 : k.message) != null ? ce : k == null ? void 0 : k.statusText) != null ? j : "",
url: (Q = b == null ? void 0 : b.url) != null ? Q : "",
params: (F = (ee = b == null ? void 0 : b.config) == null ? void 0 : ee.params) != null ? F : {},
method: (J = (q = b == null ? void 0 : b.config) == null ? void 0 : q.method) != null ? J : ""
}
})
}
}
;
C.then(b=>{
clearTimeout(y),
!(!E && h) && (w(a, {
url: n,
config: s
}, b, m),
r.finish())
}
).catch(b=>{
var k;
m({
url: n,
config: s
}, {
ok: !1,
statusText: (k = b == null ? void 0 : b.message) != null ? k : b
}),
clearTimeout(y),
!(!E && h) && (r.setTag("error", !0),
a([b, null]))
}
)
}
;
p(!1)
} catch (i) {
a([i, null])
}
}
)
}
}
设置断点调试
// 将封装的请求对象 this._req = fetch.bind(window);
typeof fetch == "function" ? this._req = fetch.bind(window) : this._req = ()=> {
throw new Error("no request function")
}
// 发起请求
C = this._req(b.url, we(I({}, b.config), {
signal: L.signal
}))
找到代码中的关键部分.
三. 代码实现
{
function test(from) {
console.log(from + ': test');
}
const f = test.bind(window);
test = new Proxy(test, {
apply(...args) {
args[2][0] = 'proxy +' + args[2][0];
return Reflect.apply(...args);
}
});
test('me');
f('bind');
}
proxy +me: test
bind: test
函数进行了bind
绑定之后, 对于原函数的Proxy
无法拦截
const fetch = window.fetch;
window.fetch = (...args) =>
(async (args) => {
const url = args[0];
const ad_list = ['trace', 'beacon'];
if (ad_list.some((e) => url.includes(e))) throw new new UserException('fuck tencent');
else return await fetch(...args);
})(args);
上述模式的代码无法拦截上述的fetch.bind(window)
的数据请求操作.
// 上述的代码是无法捕捉到这个fetch
this._req = fetch.bind(window)
需要获得这个bind
之后的"新"
函数.
// 对拦截到的函数进行二度的封装
const trap = (fn) => {
fn = async (...args) => {
let [url, config] = args;
let response = await fetch(url, config);
if (url.startsWith('https://api.bilibili.com/x/web-interface/wbi/search/type?__refresh')){
const json = () =>
// response只能读取一次, 需要.clone()做一个备份, 然后再进行操作
response
.clone()
.json()
.then((data) => {
// 对检索title, 作者全部进行修改
data.data.result.forEach(e=> {
e.author = 'HLA';
e.title = "nothing to show";
});
return data
});
response.json = json;
}
return response;
};
return fn
}
// 对所有函数的bind操作进行拦截
Function.prototype.bind = new Proxy(Function.prototype.bind, {
apply(...args) {
if (args.length > 1) {
const a = args[1];
const m = a.name;
if (m && m.includes('fetch')) args[1] = trap(a);
}
return Reflect.apply(...args);
}
});
B站
检索api
的数据结构, 结构非常清晰易读:
3.1 结果
可以看到, 修改后的response
可以返回被B站自身脚本正常处理.
这就意味着, 可以实现对B
站的内容实现过滤, 甚至高度自定义修改, 例如载入其他api
接口的信息返回等.
由于是异步载入的数据, 这就意味着修改html
页面元素过程的某些操作也是可以拦截, 进行修改的.
{
HTMLAnchorElement.prototype.setAttribute = new Proxy(HTMLAnchorElement.prototype.setAttribute, {
apply(...args) {
console.log(args);
Reflect.apply(...args);
}
});
}
例如, 干预标签 a
的属性操作.
四. 小结
Proxy, Reflect
对于干预原脚本的执行提供了绝对强大的武器, 相比于重写相关函数的实现, 代理模式让干预变得非常简单, 不需要复杂的重构, 只是对其中的某些参数进行干预就能够实现很好的干预效果, 同时不影响原脚本的执行.
以上内容仅作为交流学习, 请勿用于非法用途.