网络(http 统一 API 与改写请求/响应)
三层都有 http 命名空间,挂在各自全局变量上:wadLogicLayer.http(wx.request 语义)、wadRenderLayer.http(fetch 语义)、wadPanelLayer.http(宿主代理)。返回结构统一为 {statusCode, header, data, errMsg?},且非 2xx 不 reject(自己判断 statusCode)。
本页你将学到
- 三层 http 的语义差异(默认 dataType、超时、abort 行为);
request(opts)全部字段;- 用
capture.intercept改写请求/响应的五类案例; - 声明式改写
capture.addRule。
request(opts)
通用请求方法,三层的语义对齐宿主最自然的方式。
| 字段 | 说明 |
|---|---|
url | 请求地址(必填) |
method | 默认 'GET' |
header / headers | 请求头对象(两者等价) |
data / body | 请求体(两者等价) |
timeout | 毫秒(逻辑默认 60000;面板请求 60000,65s 仅为 RPC 外层;渲染 fetch 无内置超时) |
dataType | 'json'(三层默认,尝试解析响应为对象)|'text' |
responseType | 'text'(默认)|'arraybuffer' |
success / fail / complete | 兼容 wx 风格回调 |
行为约定(三层一致):
- GET/HEAD 的 data 自动拼进 query(对象会被 encode,字符串原样追加);
- 对象 data 自动 JSON.stringify,且未显式给 content-type 时补
application/json; - 返回值是「task 对象」:既能
await(.then/.catch/.finally),也能task.abort()取消; - 非法层:本层不支持的方法见 三层 API 对比。
get / post / put / delete
快捷方法,语义与 request 一致。
- 签名:
get(url, opts?)/post(url, data?, opts?)/put(url, data?, opts?)/delete(url, data?, opts?)
(async () => {
// 逻辑层写法;渲染层 wadRenderLayer.http、面板 wadPanelLayer.http 同形
const res = await wadLogicLayer.http.get('https://httpbin.org/get');
// res = { statusCode: 200, header: {...}, data: {...对象,dataType 默认 json...}, errMsg: '' }
// POST JSON(对象 data 自动序列化 + 补 content-type)
const out = await wadLogicLayer.http.post('https://api.example.com/order', { id: 42, count: 1 });
// 自定义头 + 文本响应
const raw = await wadLogicLayer.http.request({
url: 'https://api.example.com/may-404',
method: 'GET',
header: { Authorization: 'Bearer ' + token },
dataType: 'text'
});
if (raw.statusCode === 429) wadLogicLayer.ui.toast('限流了');
})();task.abort()
取消请求。三层的取消能力不同:
| 层 | abort 行为 |
|---|---|
| 逻辑层 | 真取消(wx task.abort) |
| 渲染层 | 真取消(AbortController) |
| 面板 | no-op:调用不报错,但不会真正中断请求(宿主代理已发出);响应仍会返回 |
// 需要取消的轮询(逻辑层/渲染层)
const task = wadLogicLayer.http.get('https://api.example.com/status');
task.abort();面板 abort 是 no-op
面板层的 task.abort() 不会中断请求。需要真正取消时,把请求放到逻辑层发。
upload(url, body, opts?)
大内容直传。三层都有,只有面板层享受宿主直传的无帧限通道:
| 层 | 语义 |
|---|---|
| 面板 | 宿主 POST /v1/upload {url, body, contentType},由宿主进程直接发起 HTTP,不经总线 |
| 逻辑层 | wx.request POST 包装(返回统一三件套) |
| 渲染层 | request POST |
(async () => {
// 面板层:upload(url, body, opts?) → {data, statusCode, header, errMsg}(服务器响应体在 data)
const r = await wadPanelLayer.http.upload('http://192.168.1.10:9000/shot', pngB64, {
contentType: 'text/plain; charset=utf-8'
});
})();改写请求/响应(capture.intercept)
只在逻辑层可用(渲染层同步抛 E_LAYER_ONLY,面板无此方法)。注册的 hook 会在 wx.request / wx.uploadFile / wx.downloadFile 真正执行前后被调,可以改 URL、头、体、响应,也可以只观察。Network 面板脚本(onRequest/onResponse)与插件 hook 是同一条链、同一份契约(见文末)。
// <script type="text/wad-logic"> 内注册
var offReq = wadLogicLayer.capture.intercept.request(function (req) {
// …改 req 或 return 新对象…
});
var offResp = wadLogicLayer.capture.intercept.response(function (resp) {
// …改 resp 或 return 新对象…
});
// 解除注册(两个方法都返回解绑函数)
offReq();req 对象(请求 hook 入参)
| 属性 | 类型 | 只读 | 适用 |
|---|---|---|---|
id | string | ✅ 只读 | 全部(本次请求 id,与 Network 面板/records 对应) |
appid | string | ✅ 只读 | 全部 |
kind | 'request'|'upload'|'download' | ✅ 只读 | 全部 |
url | string | 可写 | 全部 |
method | string | 可写 | request |
header | object | 可写 | 全部 |
data | any | 可写 | request(请求体) |
dataType | string | 可写 | request |
responseType | string | 可写 | request |
timeout | number | 可写 | request |
filePath | string | 可写 | upload / download |
name | string | 可写 | upload(文件字段名) |
formData | object | 可写 | upload(附加表单字段) |
__wadInternal | boolean | ✅ 只读 | true = 宿主/插件自身的内部请求,跳过全部 hook |
resp 对象(响应 hook 入参)
| 属性 | 类型 | 只读 | 说明 |
|---|---|---|---|
id | string | ✅ 只读 | 对应请求 id |
appid | string | ✅ 只读 | |
kind | string | ✅ 只读 | 同 req.kind |
req | object | ✅ 只读 | 发起时的请求快照(上面的 req 对象) |
statusCode | number | 可写 | HTTP 状态码 |
header | object | 可写 | 响应头 |
data | any | 可写 | 响应体(json 解析后的对象或字符串/ArrayBuffer) |
errMsg | string | 可写 | 写回 wx 回调 |
tempFilePath | string | 可写 | download:本地临时文件路径 |
返回值与报错规则
| hook 返回 | 行为 |
|---|---|
undefined / null / '' | 不修改,继续下一个 hook |
| 对象 | 采纳其中可写字段:请求侧 url/method/header/data/dataType/responseType/timeout/filePath/name/formData;响应侧 statusCode/header/data/tempFilePath/errMsg(其余字段忽略) |
| 其他类型(数字/布尔…) | 忽略 + 插件日志警告「返回值不是对象」 |
| hook 内抛异常(请求侧) | 本次请求直接失败:fail({errMsg: 'E_PLUGIN_INTERCEPT', error: {message, stack}}),请求不发出 |
| hook 内抛异常(响应侧) | resp.data 被替换为 {error: {message, stack}},statusCode 保留;错误写进插件日志 |
- 多个 hook 按注册顺序链式执行:前一个采纳的修改会体现在后一个 hook 的入参里;
req.__wadInternal === true的请求整条链跳过;- 想只观察不改:不返回任何值即可(或用
capture.watch/native.observe)。
案例① 改写 JSON 请求与响应(最常用)
// <script type="text/wad-logic">
wadLogicLayer.capture.intercept.request(function (req) {
// 按 content-type 判断 JSON 请求体
var ct = (req.header && (req.header['content-type'] || req.header['Content-Type'])) || '';
if (ct.indexOf('application/json') < 0 || typeof req.data !== 'string') return; // 不改
var body = JSON.parse(req.data);
body.__fromPlugin = true; // 加字段
body.version = 'v2'; // 改字段
return { data: JSON.stringify(body) }; // 采纳
});
wadLogicLayer.capture.intercept.response(function (resp) {
if (typeof resp.data !== 'object' || resp.data == null) return;
// resp.data 已是解析后的对象(dataType json 默认);直接改再整体采纳
if (resp.data.code === 0) resp.data.extra = { mocked: true };
return { data: resp.data, statusCode: resp.statusCode };
});案例② 改写 HTML/文本响应(dataType: 'text' 字符串替换)
// <script type="text/wad-logic">
wadLogicLayer.capture.intercept.request(function (req) {
// 文本响应要在请求侧先声明 dataType: 'text',否则基础库可能把响应当 JSON 解析
if (req.url.indexOf('/page/index') >= 0) return { dataType: 'text' };
});
wadLogicLayer.capture.intercept.response(function (resp) {
if (typeof resp.data !== 'string') return;
var html = resp.data;
if (html.indexOf('<title>') >= 0) {
// 字符串替换:把标题换成自己的
html = html.replace(/<title>[^<]*<\/title>/, '<title>WAD 已改写</title>');
}
// 去掉某段脚本
html = html.split('<script src="//cdn.example.com/track.js"></script>').join('');
return { data: html };
});案例③ 改写表单请求(x-www-form-urlencoded)
// <script type="text/wad-logic">
function parseForm(s) {
var o = {};
String(s || '').split('&').forEach(function (kv) {
if (!kv) return;
var i = kv.indexOf('=');
if (i < 0) { o[kv] = ''; return; }
o[decodeURIComponent(kv.slice(0, i))] = decodeURIComponent(kv.slice(i + 1).replace(/\+/g, ' '));
});
return o;
}
function encodeForm(o) {
return Object.keys(o).map(function (k) {
return encodeURIComponent(k) + '=' + encodeURIComponent(o[k] == null ? '' : o[k]);
}).join('&');
}
wadLogicLayer.capture.intercept.request(function (req) {
var ct = (req.header && (req.header['content-type'] || req.header['Content-Type'])) || '';
if (ct.indexOf('x-www-form-urlencoded') < 0 || typeof req.data !== 'string') return;
var form = parseForm(req.data);
form.uid = '10086'; // 改字段
delete form.debug; // 删字段
form.sign = wadLogicLayer.tools.md5(encodeForm(form)); // 重新签名(用 tools)
return { data: encodeForm(form) };
});案例④ 改写二进制响应(responseType: 'arraybuffer')
// <script type="text/wad-logic">
wadLogicLayer.capture.intercept.request(function (req) {
// 请求侧先要 ArrayBuffer:把 responseType 声明为 arraybuffer
if (req.url.indexOf('/api/binary/pack') >= 0) return { responseType: 'arraybuffer' };
});
wadLogicLayer.capture.intercept.response(function (resp) {
var buf = resp.data;
if (!(buf instanceof ArrayBuffer)) return;
var u8 = new Uint8Array(buf);
// 例:把响应头 16 字节的魔数改成自定义标记
var magic = [0x57, 0x41, 0x44, 0x21]; // 'WAD!'
for (var i = 0; i < magic.length; i++) u8[i] = magic[i];
// 也可以整体扩容:var bigger = new Uint8Array(u8.length + 4); bigger.set(u8, 4); …
return { data: u8.buffer }; // 采纳新的 ArrayBuffer
});案例⑤ 上传与下载
// <script type="text/wad-logic">
wadLogicLayer.capture.intercept.request(function (req) {
if (req.kind === 'upload') {
// 换上传的本地文件、字段名、附加表单与头
return {
filePath: wx.env.USER_DATA_PATH + '/replacement.png',
name: 'image',
formData: { source: 'wad' },
header: { 'x-upload-via': 'wad' }
};
}
if (req.kind === 'download') {
return { url: req.url + '?via=wad' }; // 下载地址追加参数
}
});
wadLogicLayer.capture.intercept.response(function (resp) {
if (resp.kind === 'download') {
// tempFilePath:把下载产物指到别的文件(文件需真实存在)
return { tempFilePath: wx.env.USER_DATA_PATH + '/renamed.bin' };
}
if (resp.kind === 'upload') {
// 上传响应体改写(服务端返回的 url/audit 结果)
if (typeof resp.data === 'string' && resp.data.indexOf('http://') === 0) {
return { data: resp.data.replace('http://', 'https://') };
}
}
});附:GET query 追加(通用小技巧)
// <script type="text/wad-logic">
wadLogicLayer.capture.intercept.request(function (req) {
if (req.method !== 'GET') return;
var join = req.url.indexOf('?') >= 0 ? '&' : '?';
return { url: req.url + join + 'wadTag=1&ts=' + Date.now() };
});声明式改写:capture.addRule
不想写 JS 时,用声明式规则。逻辑层与面板层的 capture.addRule 都可用;规则由宿主在网络栈执行,先于所有 JS hook,重开小程序后仍生效,直到 removeRule 或卸载插件(卸载时宿主清空该插件名下全部规则)。
- 签名:
addRule(rule) → Promise<{id}> - 删除:
removeRule(id) → Promise
(async () => {
// 面板层(逻辑层同形)
const rule = await wadPanelLayer.capture.addRule({
match: {
url: 'contains:/api/pay', // 'contains:xxx' 或 'regex:xxx'(无前缀按 contains 处理)
method: 'POST' // 可选;命中方法(大小写不敏感)
},
request: {
setUrl: 'https://other.host/api/pay', // 重定向
setMethod: 'PUT',
setHeader: { 'x-debug': '1' }, // 改/加头
removeHeader: ['x-token'], // 删头
setBody: '{"forced":true}', // 整体替换请求体
replaceBody: { from: '"price":0', to: '"price":1' } // 替换请求体片段
},
response: {
setStatus: 200,
setHeader: { 'x-wad': 'rewritten' },
setBody: '{"code":0,"vip":true}',
replaceBody: { from: 'false', to: 'true', regex: false } // regex:true 时 from 按全局正则替换
}
});
wadPanelLayer.log('规则已生效', rule.id);
const list = await wadPanelLayer.capture.rules(); // 本插件名下规则
await wadPanelLayer.capture.removeRule(rule.id); // 恢复
})();| 规则字段 | 说明 |
|---|---|
match.url | 'contains:xxx'(子串)或 'regex:xxx'(正则);裸字符串按 contains |
match.method | 可选,HTTP 方法(大小写不敏感) |
request.* | setUrl / setMethod / setHeader{} / removeHeader[] / setBody / replaceBody{from,to,regex?} |
response.* | setStatus / setHeader{} / setBody / replaceBody{from,to,regex?} |
enable: false | 暂时停用该规则(不清除) |
Network 面板脚本与插件 hook 同契约
Network 工具里的自定义脚本(onRequest / onResponse)和插件的 capture.intercept.request/response 跑在同一条 hook 链上:
- 入参是同一份
req/resp对象,返回值契约完全一致(不返回=不改、返回对象=采纳、抛错同规则); - 执行顺序 = 声明式规则 → 全部 Network 脚本 hook(各自注册顺序)→ 全部插件 hook(各自注册顺序);脚本与插件 hook 不混排;
__wadInternal请求对两者都跳过;插件 SDK 自身的内部请求(资产拉取、网关查询、插件自己的http)额外带ignore_wmpf_network_debug=1,同样跳过 hook / 规则并不进网络调试记录。
所以「Network 面板里调通的改写脚本」可以原样搬进插件长期生效,反之亦然。 Network 面板自定义脚本的写法见 免插件 · 网络脚本。