写在前面
在微信小程序开发中,请求/响应拦截是处理加密通信、统一错误处理、日志记录等需求的常见技术手段。
随着数据安全的意识越来越高,客户对数据加密的要求也越来越严格。之前做的几个项目中,都已经要求请求/响应的数据进行全链路加密。在微信小程序端,对于请求/响应的数据也需要加密。但是项目不同,微信小程序的开发环境不同,对微信小程序请求/响应加密的拦截方式就不同(有的直接用Object.defineProperty方式进行全局拦截;有的自定义封装方法对指定请求/响应进行拦截加密),在这几个项目中,有两个比较典型的例子:
- 整个项目全是自己公司做,微信小程序的所有请求/响应的加密/解密方式都一致,此时就可以采用Object.defineProperty方式进行全局拦截加解密;
- 项目由多个公司合作,微信小程序的请求/响应加解密方式不一致,此时为了不影响友商的请求/响应,就采用饿了自定义封装方法对自己的请求/响应进行拦截加解密。
既然用到了这些拦截增强的方式,那就记录一下。
1. Object.defineProperty拦截方式
1.1 实现原理
Object.defineProperty 是 JavaScript 中用于定义对象属性的方法。通过它,我们可以重新定义 wx.request 的行为,在每次调用时插入自定义逻辑。具体步骤如下:
- 创建原始 wx 对象的副本: 为了确保原生 wx.request 方法不会被覆盖,我们先保存一个原始 wx 对象的副本。
- 重定义 wx.request: 使用 Object.defineProperty 重新定义 wx.request,使其在调用时执行我们自定义的逻辑。
- 处理请求参数: 在实际发送请求之前,根据业务需求加密或修改请求参数。
- 处理响应数据: 在接收到响应后,解密或处理响应数据,并调用原始的成功回调函数。
- 确保原始请求执行: 最后,确保原始的 wx.request 方法被执行。
1.2 Object.defineProperty
详细说明
Object.defineProperty 是 JavaScript 中用于直接操作对象属性的内置方法。它允许你以细粒度的方式定义或修改对象的属性,包括控制属性的可写性、可枚举性、可配置性等。这对于创建不可变属性、拦截属性访问和修改等场景非常有用。
- 语法
javascript
Object.defineProperty(obj, prop, descriptor)
- obj:要在其上定义属性的对象。
- prop:要定义或修改的属性名称。
- descriptor:描述符对象,用于描述新属性的行为。
- 描述符类型
描述符有两种主要类型:
- 数据描述符(Data Descriptor):描述一个具有值的属性。
- 存取描述符(Accessor Descriptor):描述一个通过 getter 和 setter 方法访问的属性。
这两种描述符不能同时使用,但可以混合使用不同的属性。
数据描述符的属性
javascript
- value:属性的值,默认为 undefined。
- writable:如果为 true,则属性值可以通过赋值操作改变;否则为只读,默认为 false。
- configurable:如果为 true,则该属性可以被删除,并且它的属性描述符可以被改变;否则为不可配置,默认为 false。
- enumerable:如果为 true,则该属性会在遍历对象时出现(如 for...in 循环),默认为 false。
存取描述符的属性
diff
- get:获取属性值时调用的函数,默认为 undefined。
- set:设置属性值时调用的函数,默认为 undefined。
- configurable:同数据描述符。
- enumerable:同数据描述符。
- 示例
定义一个只读属性
javascript
const obj = {};
Object.defineProperty(obj, 'readOnlyProp', {
value: 'This is read-only',
writable: false,
configurable: false,
enumerable: true
});
console.log(obj.readOnlyProp); // 输出: This is read-only
obj.readOnlyProp = 'Try to change'; // 不会生效
console.log(obj.readOnlyProp); // 输出: This is read-only
使用 getter 和 setter
javascript
const person = {};
Object.defineProperty(person, 'age', {
get: function() {
return this._age;
},
set: function(value) {
if (typeof value === 'number' && value > 0) {
this._age = value;
} else {
console.error('Invalid age');
}
},
configurable: true,
enumerable: true
});
person.age = 25; // 设置年龄
console.log(person.age); // 输出: 25
person.age = -5; // 输出: Invalid age
在使用Object.defineProperty 用于拦截 wx.request 方法并对其进行扩展,以实现请求的加解密处理中:
javascript
const originWx = wx;
wx = Object.create(wx);
Object.defineProperty(wx, "request", {
writable: false,
value: function(e) {
// 拦截逻辑
let url = e.url;
let data = e.data;
let header = e.header;
if (!HttpEncryptTools.isUrlInReqWhiteList(url)) {
if (e.method === 'GET') {
let res = HttpEncryptTools.encryptGetRequestData(url, data, header);
url = res.url;
data = res.paramsData;
header = res.headers;
} else if (e.method === 'POST') {
let res = HttpEncryptTools.encryptPostRequestData(url, data, header);
url = res.url;
data = res.bodyData;
header = res.headers;
}
e.url = url;
e.data = data;
e.header = header;
}
console.log('request start...', e);
const success = arguments[0].success;
const fail = arguments[0].fail;
// 对成功的方法重写定义
arguments[0].success = function(...args) {
console.log('request success');
if (arguments[0].statusCode == 200) {
let response = arguments[0].data;
if (response.data && response.isDecrypt) {
let decryptData = HttpEncryptTools.decryptResponseData(response.imData, response.data);
arguments[0].data = decryptData;
}
}
success(...args);
};
// 对失败的方法重写定义
arguments[0].fail = function(...args) {
console.log('request fail');
fail(...args);
};
// 确保原来的 request 执行
originWx['request'].apply(this, arguments);
}
});
在这个例子中:
- writable: false:确保 wx.request 不能被重新赋值,防止其他地方意外覆盖这个方法。
- value:定义了新的 request 方法的具体实现,包括加密解密逻辑和回调函数的重写。
- originWx['request'].apply(this, arguments):确保原始的 wx.request 方法仍然被执行,保证小程序的其他部分不会受到影响。
1.3 代码示例
javascript
// 接收原对象
const originWx = wx;
// 基于WX创建个新对象
wx = Object.create(wx);
Object.defineProperty(wx, "request", {
writable: false,
value: function(e) {
let url = e.url;
let data = e.data;
let header = e.header;
// 判断是否需要加密
if (!HttpEncryptTools.isUrlInReqWhiteList(url)) {
if (e.method === 'GET') {
let res = HttpEncryptTools.encryptGetRequestData(url, data, header);
url = res.url;
data = res.paramsData;
header = res.headers;
} else if (e.method === 'POST') {
let res = HttpEncryptTools.encryptPostRequestData(url, data, header);
url = res.url;
data = res.bodyData;
header = res.headers;
}
e.url = url;
e.data = data;
e.header = header;
}
console.log('request start...', e);
// 重写成功回调
const success = arguments[0].success;
arguments[0].success = function(...args) {
console.log('request success');
if (arguments[0].statusCode == 200) {
let response = arguments[0].data;
if (response.data && response.isDecrypt) {
let imData = response.imData;
let encryptData = response.data;
let decryptData = HttpEncryptTools.decryptResponseData(imData, encryptData);
arguments[0].data = decryptData;
}
}
success(...args);
};
// 重写失败回调
const fail = arguments[0].fail;
arguments[0].fail = function(...args) {
console.log('request fail');
fail(...args);
};
// 确保原来的request执行
originWx['request'].apply(this, arguments);
}
});
详细解释:
**<font style="background-color:rgb(249, 250, 251);">Object.defineProperty</font>**
****方法 :- 这是 JavaScript 中一个非常强大的方法,用于直接在一个对象上定义一个新属性,或者修改一个对象的现有属性,并返回这个对象。它接受三个参数:要定义属性的对象、要定义或修改的属性的名称以及一个属性描述符对象。
- 在这个例子中,我们要定义的对象是
<font style="background-color:rgb(249, 250, 251);">wx</font>
,属性名称是<font style="background-color:rgb(249, 250, 251);">request</font>
,属性描述符对象包含<font style="background-color:rgb(249, 250, 251);">writable</font>
和<font style="background-color:rgb(249, 250, 251);">value</font>
两个属性。 <font style="background-color:rgb(249, 250, 251);">writable: false</font>
表示这个属性的值不能被重新赋值,这样可以防止后续代码不小心覆盖我们定义的<font style="background-color:rgb(249, 250, 251);">request</font>
方法。<font style="background-color:rgb(249, 250, 251);">value</font>
是一个函数,这个函数就是我们对<font style="background-color:rgb(249, 250, 251);">wx.request</font>
的增强版本。当我们调用<font style="background-color:rgb(249, 250, 251);">wx.request</font>
时,实际上调用的就是这个函数。
- 请求拦截部分 :
- 首先,从传入的请求配置对象
<font style="background-color:rgb(249, 250, 251);">e</font>
中获取<font style="background-color:rgb(249, 250, 251);">url</font>
、<font style="background-color:rgb(249, 250, 251);">data</font>
和<font style="background-color:rgb(249, 250, 251);">header</font>
。 - 然后,判断请求的 URL 是否在白名单中,如果不在白名单中,根据请求方法(GET 或 POST)分别调用不同的加密方法对请求数据进行加密处理,并更新请求的
<font style="background-color:rgb(249, 250, 251);">url</font>
、<font style="background-color:rgb(249, 250, 251);">data</font>
和<font style="background-color:rgb(249, 250, 251);">header</font>
。
- 首先,从传入的请求配置对象
- 响应拦截部分 :
- 保存原始的
<font style="background-color:rgb(249, 250, 251);">success</font>
和<font style="background-color:rgb(249, 250, 251);">fail</font>
回调函数。 - 重写
<font style="background-color:rgb(249, 250, 251);">success</font>
回调函数,在响应返回后,判断响应状态码是否为 200,如果是且响应数据中包含加密报文,则进行解密处理,并更新响应数据。 - 重写
<font style="background-color:rgb(249, 250, 251);">fail</font>
回调函数,在请求失败时进行一些额外的处理(这里只是简单打印日志)。 - 最后,调用原始的
<font style="background-color:rgb(249, 250, 251);">wx.request</font>
方法,确保请求正常发送。
- 保存原始的
1.4 优缺点
优点:
- 全局生效:所有地方使用 wx.request 都会被拦截,无需逐个修改。
- 灵活性高:可以在请求前和响应后插入任意逻辑。
- 兼容性好:不会影响原有代码结构,只需在启动时初始化即可。
缺点:
- 复杂度较高:需要理解 Object.defineProperty 的工作原理,且容易出错。
- 调试困难:由于是全局拦截,一旦出现问题,排查难度较大。
- 侵入性 :修改全局
<font style="color:rgb(64, 64, 64);">wx</font>
对象可能与其他插件或框架冲突。
1.5 适用场景
- 全站接口需统一加密(如金融、政务类小程序)。
- 需要强制实现某些全局逻辑(如自动 Token 刷新)。
2. 自定义封装方法(如 <font style="color:rgb(64, 64, 64);">rlzyHttpAjax</font>
)
2.1 实现原理
与直接修改 wx.request 不同,自定义封装方法是通过封装一个新的请求方法来实现拦截。通过封装一个自定义方法(如rlzyHttpAjax),显示处理加密和请求调用,避免修改原生对象。这种方式更加直观,易于理解和维护。具体步骤如下:
- 创建新的请求函数:定义一个新的函数(如 rlzyHttpAjax),该函数接受与 wx.request 相同的参数。
- 处理请求参数:在实际发送请求之前,根据业务需求加密或修改请求参数。
- 处理响应数据:在接收到响应后,解密或处理响应数据,并调用用户提供的回调函数。
- 调用 wx.request:最终调用 wx.request 发送请求。
2.2 代码示例
javascript
rlzyHttpAjax(data) {
let that = this;
if (!that.isUrlInReqWhiteList(data.url)) {
let res = that.encryptRequestData(
data.method,
data.url,
data.data,
data.header
);
data.url = res.url;
data.data = res.bodyData;
data.header = res.headers;
}
wx.request({
url: data.url,
data: data.data,
header: data.header,
method: data.method,
success: function (res) {
console.log('==================rlzyHttpAjax sucess=====================');
if (res.statusCode == 200) {
let response = res.data;
if (response.data && response.isDecrypt) {
let encryptData = response.data;
var decrypted = that.decrypted(encryptData);
res.data = decrypted;
}
}
data.success(res);
},
fail: function (err) {
console.log('==================rlzyHttpAjax fail=====================');
if (data.fail != null) {
data.fail(err);
} else {
wx.showModal({
title: '请求失败!',
content: err.errMsg,
icon: 'error',
duration: 2000
});
}
wx.hideLoading();
}
});
}
详细解释:
- 请求拦截部分 :
- 首先,判断请求的 URL 是否在白名单中,如果不在白名单中,调用
<font style="background-color:rgb(249, 250, 251);">encryptRequestData</font>
方法对请求数据进行加密处理,并更新请求的<font style="background-color:rgb(249, 250, 251);">url</font>
、<font style="background-color:rgb(249, 250, 251);">data</font>
和<font style="background-color:rgb(249, 250, 251);">header</font>
。
- 首先,判断请求的 URL 是否在白名单中,如果不在白名单中,调用
- 响应拦截部分 :
- 在请求成功的回调函数中,判断响应状态码是否为 200,如果是且响应数据中包含加密报文,则进行解密处理,并更新响应数据。
- 在请求失败的回调函数中,根据是否提供了
<font style="background-color:rgb(249, 250, 251);">fail</font>
回调函数进行不同的处理,如果提供了则调用该回调函数,否则显示一个请求失败的模态框。
2.3 优缺点
优点:
- 灵活性:仅对指定请求生效,不影响其他逻辑。
- 安全性:不修改全局对象,兼容性更好。
缺点:
- 代码侵入性较大 :需要在每个请求处手动调用
<font style="background-color:rgb(249, 250, 251);">rlzyHttpAjax</font>
函数,而不是直接使用<font style="background-color:rgb(249, 250, 251);">wx.request</font>
。 - 全局拦截困难:如果需要对所有请求进行拦截,需要在每个请求处手动调用该函数,不够方便。
2.4 适用场景
- 部分接口需要特殊处理(如求职模块独立加密)。
- 项目已存在复杂请求逻辑,需渐进式改造。
3. 中间件模式(装饰者模式)
3.1 实现原理
通过封装一个请求工具类,在工具类内部实现拦截逻辑。
通过装饰器模式可以动态地为 wx.request 添加功能。装饰器模式的核心思想是将一个函数包装在一个新的函数中,从而在调用时添加额外的功能。
3.2 代码实例
demo1:(中间件)
javascript
class RequestMiddleware {
static async request(options) {
// 请求前处理
const encryptedData = this.encrypt(options.data);
const response = await wx.request({ ...options, data: encryptedData });
// 响应后处理
return this.decrypt(response.data);
}
}
// 业务调用
RequestMiddleware.request({ url: "/api/data", method: "GET" });
优缺点:
- 优点:结构清晰,易于扩展异步逻辑。
- 缺点:需统一调用入口,无法拦截第三方库的请求。
demo2:(中间件)
javascript
// 定义一个请求中间件
function requestMiddleware(requestConfig) {
return new Promise((resolve, reject) => {
if (!isUrlInReqWhiteList(requestConfig.url)) {
// 对请求数据进行加密处理
let encryptedConfig = encryptRequestData(requestConfig);
wx.request({
...encryptedConfig,
success: (res) => {
if (res.statusCode === 200) {
if (res.data.data && res.data.isDecrypt) {
// 对响应数据进行解密处理
let decryptedData = decryptResponseData(res.data);
res.data = decryptedData;
}
resolve(res);
} else {
reject(res);
}
},
fail: (err) => {
reject(err);
}
});
} else {
wx.request({
...requestConfig,
success: resolve,
fail: reject
});
}
});
}
// 使用中间件进行请求
async function makeRequest() {
try {
let response = await requestMiddleware({
url: 'https://example.com/api',
method: 'GET',
data: {},
header: {}
});
console.log('请求成功', response);
} catch (error) {
console.log('请求失败', error);
}
}
详细讲解
- 定义一个
requestMiddleware
函数,该函数接受一个请求配置对象作为参数,并返回一个 Promise。 - 在
requestMiddleware
函数中,首先判断请求的 URL 是否在白名单中,如果不在白名单中,对请求数据进行加密处理,然后调用wx.request
发送请求。 - 在请求成功的回调函数中,判断响应状态码是否为 200,如果是且响应数据中包含加密报文,则进行解密处理,并通过
resolve
方法将处理后的响应数据传递给调用者。 - 在请求失败的回调函数中,通过
reject
方法将错误信息传递给调用者。 - 最后,定义一个
makeRequest
函数,使用async/await
语法调用requestMiddleware
函数进行请求,并处理请求的成功和失败。
优缺点:
- 优点 :
- 代码结构清晰:使用 Promise 或 async/await 可以使代码结构更加清晰,易于理解和维护。
- 可扩展性强:可以方便地添加更多的中间件,对请求和响应进行不同的处理。
- 缺点 :
- 需要对 Promise 或 async/await 有一定的了解:对于不熟悉这些语法的开发者来说,可能会有一定的学习成本。
demo3:(装饰者)
javascript
function requestDecorator(originalRequest) {
return function(options) {
// 处理请求参数
options = processRequestParams(options);
// 调用原函数
return originalRequest(options).then(response => {
// 处理响应数据
return processResponseData(response);
}).catch(error => {
// 处理错误
return handleError(error);
});
};
}
wx.request = requestDecorator(wx.request);
实现步骤:
- 定义装饰器函数:创建一个装饰器函数,接收原函数作为参数。
- 包装原函数:在装饰器函数内部,调用原函数并添加额外逻辑。
- 应用装饰器:将装饰器应用于 wx.request。
优缺点:
- 优点:灵活、可复用
- 缺点:需要理解装饰器模式
4. 代理模式
创建一个代理对象,重写 <font style="color:rgb(64, 64, 64);">wx.request</font>
但不直接修改原对象:
javascript
const wxProxy = new Proxy(wx, {
get(target, prop) {
if (prop === "request") {
return function(options) {
// 插入拦截逻辑
const processedOptions = processRequest(options);
return target[prop](processedOptions);
};
}
return target[prop];
}
});
// 业务中改用 wxProxy
wxProxy.request({ url: "/api/data" });
- 优点:无侵入性,可动态控制拦截逻辑。
- 缺点:Proxy 兼容性需注意(支持微信基础库 2.11.0+)。
5. 自定义拦截器(模拟 Axios 的拦截器)
javascript
// 定义一个请求拦截器数组
const requestInterceptors = [];
// 定义一个响应拦截器数组
const responseInterceptors = [];
// 添加请求拦截器
function addRequestInterceptor(interceptor) {
requestInterceptors.push(interceptor);
}
// 添加响应拦截器
function addResponseInterceptor(interceptor) {
responseInterceptors.push(interceptor);
}
// 自定义请求函数
function customRequest(config) {
let promise = Promise.resolve(config);
// 执行请求拦截器
requestInterceptors.forEach(interceptor => {
promise = promise.then(interceptor);
});
return promise.then(finalConfig => {
return new Promise((resolve, reject) => {
wx.request({
...finalConfig,
success: (res) => {
let responsePromise = Promise.resolve(res);
// 执行响应拦截器
responseInterceptors.forEach(interceptor => {
responsePromise = responsePromise.then(interceptor);
});
responsePromise.then(finalResponse => {
resolve(finalResponse);
}).catch(reject);
},
fail: reject
});
});
});
}
// 使用自定义请求函数
addRequestInterceptor(config => {
if (!isUrlInReqWhiteList(config.url)) {
// 对请求数据进行加密处理
return encryptRequestData(config);
}
return config;
});
addResponseInterceptor(response => {
if (response.statusCode === 200 && response.data.data && response.data.isDecrypt) {
// 对响应数据进行解密处理
let decryptedData = decryptResponseData(response.data);
response.data = decryptedData;
}
return response;
});
customRequest({
url: 'https://example.com/api',
method: 'GET',
data: {},
header: {}
}).then(res => {
console.log('请求成功', res);
}).catch(err => {
console.log('请求失败', err);
});
详细讲解
- 定义两个数组
requestInterceptors
和responseInterceptors
分别用于存储请求拦截器和响应拦截器。 - 定义
addRequestInterceptor
和addResponseInterceptor
函数用于添加拦截器。 - 定义
customRequest
函数,该函数接受一个请求配置对象作为参数,并返回一个 Promise。 - 在
customRequest
函数中,首先将请求配置对象包装成一个 Promise,然后依次执行请求拦截器,对请求配置对象进行处理。 - 调用
wx.request
发送请求,在请求成功的回调函数中,将响应数据包装成一个 Promise,然后依次执行响应拦截器,对响应数据进行处理。 - 最后,通过
then
和catch
方法处理请求的成功和失败。
优缺点
- 优点 :
- 灵活可扩展:可以方便地添加和移除拦截器,对请求和响应进行不同的处理。
- 代码结构清晰:模拟 Axios 的拦截器机制,代码结构清晰,易于理解和维护。
- 缺点 :
- 代码复杂度较高:需要实现拦截器的添加、执行等逻辑,代码相对复杂。
6. 方案对比与最佳实践
方案 | 侵入性 | 灵活性 | 维护成本 | 适用场景 |
---|---|---|---|---|
<font style="color:rgb(64, 64, 64);">Object.defineProperty</font> |
高 | 低 | 低 | 全站统一处理 |
自定义封装方法 | 低 | 中 | 高 | 模块化需求 |
中间件模式 | 中 | 高 | 中 | 需要异步控制的复杂逻辑 |
代理模式 | 低 | 高 | 低 | 高兼容性要求的渐进式增强 |
最佳实践建议:
- 全站加密/鉴权 :优先选择
<font style="color:rgb(64, 64, 64);">Object.defineProperty</font>
,但需测试兼容性。 - 模块化需求:使用自定义封装方法或中间件模式。
- 高可维护性:代理模式结合 TypeScript 类型检查,提升代码健壮性。
7. 总结
微信小程序的请求拦截可通过多种方式实现,选择时需权衡 侵入性 、灵活性 和 维护成本 。对于大多数项目,推荐结合使用 中间件模式 (业务层)和 代理模式 (底层库),在保证灵活性的同时降低耦合度。若需强制全局处理,<font style="color:rgb(64, 64, 64);">Object.defineProperty</font>
仍是高效选择,但务必预留调试接口,避免"黑盒化"过深。