工作中有些一些场景要拦截或改写网络库,比如在做跨端基建的时候,移动端同学说:原生拦截不好使,我和你约定一个request 方法吧。你直接用这个 request 方法调用。在你质疑他技术水平 :)的同时,我们可以思考一下这个场景。如果 H5 侧要拦截所有网络请求并用 request,大家怎么做。
业务同学使用的第三方网络库都不一样,axios,fetch,fly,一大堆。但大家观察一下 chrome 的请求类型基础就两个 fetch 和 XHR。那么搞定这两个我们的这个问题就解决了。
基本结构很简单,你让 llm 帮你写即可,我们仅聊聊思路和跨端场景下的边界场景。首先是劫持或是替换 fetch 和 XHR,以 fetch 举例。
js
// ez code
const originalFetch = globalThis.fetch
const originalXMLHttpRequest = globalThis.XMLHttpRequest
globalThis.fetch = myFetch
globalThis.XMLHttpRequest = myXMLHttpRequest
// my fetch
const myFetch = async function (
input: RequestInfo | URL,
options: RequestInit = {},
): Promise<Response> {
try {
// 处理请求参数
const url = input.toString()
const method = options?.method || 'GET'
const headers = {}
const body = xxx
// customRequest 是移动端给的一个网络请求han'shu
const response = await customRequest({
body: JSON.stringify(body),
url,
method,
headers,
timeout: 10000,
});
try {
response.data = JSON.parse(response.responseText);
} catch {
console.error(
"Failed to parse response as JSON, using raw response text."
);
response.data = {};
}
const out = {
ok: response.statusCode >= 200 && response.statusCode < 300,
status: response.statusCode,
statusText: `${response.statusCode}` || "",
headers: new Headers(response.headers || {}),
json: () => {
return Promise.resolve(response.data);
},
text: () => Promise.resolve(response.responseText || ""),
blob: () => Promise.reject(new Error("Blob not supported in fly")),
formData: () =>
Promise.reject(new Error("FormData not supported in fly")),
arrayBuffer: () => Promise.resolve(response.data),
clone: () => ({
ok: response.statusCode >= 200 && response.statusCode < 300,
status: response.statusCode,
statusText: `${response.statusCode}` || "",
headers: new Headers(response.headers || {}),
json: () => Promise.resolve(response.data),
text: () => Promise.resolve(response.responseText || ""),
blob: () => Promise.reject(new Error("Blob not supported in fly")),
formData: () =>
Promise.reject(new Error("FormData not supported in fly")),
arrayBuffer: () => Promise.resolve(response.data),
}),
data: {
...response.data,
json: () => {
return Promise.resolve(response.data);
},
},
body: new ReadableStream({
start(controller) {
// 将响应数据推送到流中
const data = response.data || response.responseText || "";
const encoder = new TextEncoder();
controller.enqueue(
encoder.encode(
typeof data === "string" ? data : JSON.stringify(data)
)
);
controller.close();
},
}),
redirected: false,
type: "",
bytes: null,
bodyUsed: false,
url,
} as Response;
return out
} catch (error) {
// 创建一个错误响应
}
}
这一块需要注意 fetch 请求的 body 需要一个 ReadStream 类型。大多数第三方库会使用这个值。如果没有这个值,即使你请求正常也会拿到空对象(如果第三方库的默认是空)。
常见问题
这一块你基本上可以用 vibe coding。但是代码结构非常重要,因为会有一些边界场景,比如 override 代码(运行时代码执行比业务代码慢导致部分网络请求用原生 fetch / XHR)。有几个思路,
方案一:阻塞业务代码
阻塞主流程直到网络库覆盖完成。优点是直截了当,非常粗暴。但问题是
- 运行时代码侵入到业务代码里了
- 移动端提供的 request 方法可能不可用,因为 H5 和移动端往往通过 bridge 桥接。桥接流程如果存在同步操作。可能移动端在第一时间无法正常承接你的网络请求,但请求已经进去了。
方案二:网络库重发
相对和业务代码解耦,但问题是
- 业务代码注定要面临前几次网络请求被重发的问题。如果处理不好,用户可能会看到接口报错
方案三:容器侧注入运行时
相对常用的运行时注入方案,先对逻辑容器注入,运行时对象。
对于 iOS
swift
jsContext.executeJavaScript(`globalfetch = xxx`)
等移动端确定后再加载 H5 的代码,建立业务实例。这样可以完全解耦。