1. 为什么你的小程序需要请求拦截做微信小程序开发尤其是涉及到支付、用户隐私或者企业数据的时候你肯定遇到过这样的需求所有发出去的请求数据要加密所有收到的响应数据要解密。这可不是为了炫技而是实打实的安全刚需。我经历过好几个项目甲方爸爸上来第一句话就是“数据必须全程加密明文传输绝对不行。”但问题来了怎么加在哪加难道要在每个调用wx.request的地方都手动写一遍加密解密的逻辑吗那代码得臃肿成什么样后期维护简直就是灾难。这时候请求/响应拦截这个技术就派上用场了。简单说它就像在你小程序的网络通道上设了一个“海关”所有进出的“货物”数据都要经过这里统一检查、打包加密或拆包解密。想象一下你有一百个页面都在发请求如果每个地方都写一遍加密代码哪天加密算法变了你得改一百个地方。而用拦截器你只需要在一个地方修改所有请求自动生效这就是它的核心价值统一处理一劳永逸。接下来我会结合我踩过的坑和实战经验带你从最“霸道”的全局拦截到最“优雅”的模块化方案彻底搞懂怎么选、怎么用。2. 方案一Object.defineProperty —— 全局拦截的“手术刀”当你需要给整个小程序的所有网络请求“动手术”时Object.defineProperty就像一把精准的手术刀。它能直接修改wx.request这个原生方法的行为实现全局无感拦截。2.1 原理深度拆解它如何“劫持”wx.requestObject.defineProperty是 JavaScript 的一个底层 API它能直接定义一个对象的属性并精确控制这个属性的行为比如能不能被修改、能不能被枚举。我们用它的目的不是添加一个新属性而是替换掉wx对象上的request方法。这里有个关键技巧我们不能直接覆盖wx.request否则会丢失原始方法导致请求发不出去。正确的做法是“偷梁换柱”。首先把原始的wx对象保存下来然后基于它创建一个新对象作为我们新的wx。最后在新对象上用Object.defineProperty定义request属性其值就是我们包含了拦截逻辑的新函数。这样我们既保留了原方法又能在新函数里“为所欲为”。// 第一步保存原始的 wx 对象这是我们的“救命稻草” const originWx wx; // 第二步基于原始wx创建一个新对象作为我们操作的“替身” wx Object.create(wx); // 第三步给“替身”的request属性动手术 Object.defineProperty(wx, request, { writable: false, // 锁死这个属性防止被其他代码意外覆盖 value: function(config) { // 这里就是全新的、带拦截功能的request方法 // --- 请求拦截区在这里加密 --- console.log(拦截到请求发出:, config.url); if (needEncrypt(config.url)) { config.data myEncrypt(config.data); } // 关键重写成功回调插入解密逻辑 const originalSuccess config.success; config.success function(res) { console.log(拦截到响应返回:, res); // --- 响应拦截区在这里解密 --- if (res.statusCode 200 res.data.isEncrypted) { res.data myDecrypt(res.data.ciphertext); } // 别忘了调用用户最初传入的成功回调并把处理后的res传给它 originalSuccess originalSuccess(res); }; // 第四步请“本尊”出山用处理过的参数执行原始请求 originWx.request.call(this, config); } });这段代码执行后你的小程序里任何地方调用wx.request()实际上都是在调用我们这个增强版函数。它先加工请求再加工响应最后把结果交给业务代码业务层完全感知不到加密解密的存在。2.2 实战代码与避坑指南光看原理不够我们来看一个包含完整加密解密逻辑的生产级代码示例。假设我们的加密工具类叫SecurityHelper。// security-interceptor.js const originWx wx; wx Object.create(wx); Object.defineProperty(wx, request, { writable: false, value: function(config) { // 1. 备份原始回调这是关键否则用户回调会丢失 const userSuccess config.success; const userFail config.fail; const userComplete config.complete; // 2. 请求拦截参数加密 const processedConfig processRequest(config); // 3. 重写success回调嵌入响应解密 processedConfig.success function(res) { try { const decryptedRes processResponse(res); // 解密响应 // 将解密后的数据“伪装”成原始响应结构 res.data decryptedRes; userSuccess userSuccess(res); } catch (error) { // 解密失败走失败逻辑 userFail userFail({ errMsg: 响应解密失败: ${error.message}, originalRes: res }); } }; // 4. 重写fail和complete回调确保用户回调被执行 processedConfig.fail function(err) { console.error(请求失败:, err); userFail userFail(err); }; processedConfig.complete function(res) { userComplete userComplete(res); }; // 5. 调用原始请求 return originWx.request.call(this, processedConfig); } }); // 请求处理函数 function processRequest(config) { const { url, data, method } config; // 示例非白名单URL且为POST请求时对data进行加密 if (!isWhiteList(url) method POST data) { return { ...config, data: SecurityHelper.encryptData(data), header: { ...config.header, X-Encrypted: true // 可以加个标记告诉服务端 } }; } return config; } // 响应处理函数 function processResponse(response) { if (response.statusCode 200) { const resData response.data; // 根据服务端返回的标记判断是否需要解密 if (resData resData.encrypted) { return SecurityHelper.decryptData(resData.ciphertext); } } // 其他情况如非200状态码、非加密数据原样返回 return response.data; } // 白名单判断例如登录接口可能不需要加密 function isWhiteList(url) { const whiteList [/api/login, /api/public/config]; return whiteList.some(whiteUrl url.includes(whiteUrl)); }避坑指南一定要备份用户回调这是最容易出错的地方。如果你直接修改config.success而不备份用户传入的回调函数就丢了请求结果无法传递到业务页面。错误处理要完善解密过程可能失败比如数据被篡改、密钥不对。一定要用try...catch包住并在失败时通过用户传入的fail回调告知业务方而不是吞掉错误。注意this指向在调用originWx.request.call(this, processedConfig)时使用call并传入this能确保在某个 Page 或 Component 内部调用wx.request时其上下文this被正确传递虽然wx.request本身不依赖this但这是好习惯。白名单机制不是所有接口都需要加密比如获取公开配置、图片上传等。务必设计白名单避免不必要的性能开销和潜在问题。2.3 优缺点与适用场景优点全局生效一劳永逸只要在 app.js 引入这个拦截器整个项目所有请求自动加密解密开发体验极其统一。对业务代码零侵入业务页面里写的还是最普通的wx.request({...})没有任何加密解密的痕迹代码干净。强制性强适合有严格安全合规要求的场景确保“一个明文都跑不出去”。缺点调试困难请求一旦出问题你是在一个“黑盒”里调试。需要加详细的日志或者设计一个开关能在开发环境关闭拦截。风险较高直接修改全局对象如果拦截器代码有 Bug可能导致整个小程序的网络请求瘫痪。而且如果与其他同样修改wx的第三方库共存可能会冲突。不够灵活如果项目里只有部分模块需要加密比如只有支付模块用这个方案就有点“杀鸡用牛刀”了。适用场景最适合全站接口统一加密的场景比如金融、政务、涉及核心商业秘密的企业内部小程序。整个项目由你们团队完全掌控加密规则一致。3. 方案二自定义封装函数 —— 灵活精准的“狙击枪”如果全局拦截是“地毯式轰炸”那自定义封装函数就是“精准狙击”。它不碰全局的wx.request而是你自己写一个函数比如叫myRequest在这个函数内部处理加密解密然后手动调用原始的wx.request。3.1 如何封装一个健壮的请求函数封装的核心思想是创建一个更好用的“轮子”来替代原生的“轮子”。这个“轮子”对外提供和wx.request几乎一样的接口但内部帮你干了加密解密的脏活。// my-request.js class MyRequest { /** * 自定义请求方法 * param {Object} options - 与wx.request参数基本一致 */ static async request(options) { // 合并默认参数 const config { url: , method: GET, data: {}, header: { content-type: application/json }, timeout: 10000, ...options // 用户传入的参数覆盖默认值 }; // 1. 请求拦截加密 const encryptedConfig this._processRequest(config); // 2. 使用Promise包装方便使用async/await return new Promise((resolve, reject) { wx.request({ ...encryptedConfig, success: (res) { // 3. 响应拦截解密 const decryptedRes this._processResponse(res); // 这里可以统一处理一些业务逻辑比如token过期 if (decryptedRes.code 401) { this._handleTokenExpired(); reject(new Error(Token已过期)); return; } resolve(decryptedRes); }, fail: (err) { console.error(网络请求失败:, err); // 可以在这里统一处理网络错误比如提示用户 wx.showToast({ title: 网络开小差了, icon: none }); reject(err); } }); }); } // 静态方法处理请求 static _processRequest(config) { const { url, data, method } config; if (this._needEncrypt(url)) { return { ...config, data: SecurityHelper.encryptData(data), header: { ...config.header, X-Encrypted: true } }; } return config; } // 静态方法处理响应 static _processResponse(response) { if (response.statusCode 200) { const resData response.data; if (resData resData.encrypted) { response.data SecurityHelper.decryptData(resData.ciphertext); } // 你可以在这里统一处理数据格式比如返回 {code, data, msg} 的标准结构 return response.data; } else { // 处理HTTP错误状态码 throw new Error(HTTP ${response.statusCode}: ${response.errMsg}); } } static _needEncrypt(url) { // 你的白名单逻辑 return !url.includes(/api/public); } static _handleTokenExpired() { // 跳转到登录页或静默刷新token console.log(处理token过期逻辑); } } // 导出单例或类 export default MyRequest;在业务页面中你就不再使用wx.request而是使用自己封装的这个函数// pages/index/index.js import MyRequest from ../../utils/my-request.js; Page({ async onLoad() { try { const result await MyRequest.request({ url: https://api.example.com/user/data, method: POST, data: { userId: 123 } }); console.log(获取到的数据:, result); this.setData({ userData: result }); } catch (error) { console.error(请求出错:, error); } } });3.2 进阶打造功能丰富的请求库上面的封装已经能用但一个成熟的请求库还需要更多功能。我们可以很容易地扩展它自动重试在网络不稳定或特定失败时自动重试。请求队列控制并发防止短时间内过多请求。缓存机制对某些GET请求结果进行本地缓存。请求取消在页面卸载时取消未完成的请求。更完善的拦截器可以设计成支持多个前置拦截器和后置拦截器。这里给一个支持前置/后置拦截器的简单扩展思路class EnhancedRequest { constructor() { this.requestInterceptors []; this.responseInterceptors []; } useRequestInterceptor(interceptor) { this.requestInterceptors.push(interceptor); } useResponseInterceptor(interceptor) { this.responseInterceptors.push(interceptor); } async request(config) { // 执行所有请求拦截器 let processedConfig config; for (const interceptor of this.requestInterceptors) { processedConfig await interceptor(processedConfig); } // 发送请求 const rawResponse await this._rawRequest(processedConfig); // 执行所有响应拦截器 let processedResponse rawResponse; for (const interceptor of this.responseInterceptors) { processedResponse await interceptor(processedResponse); } return processedResponse; } _rawRequest(config) { return new Promise((resolve, reject) { wx.request({ ...config, success: resolve, fail: reject }); }); } } // 使用 const http new EnhancedRequest(); http.useRequestInterceptor((config) { console.log(请求拦截器1: 添加Token); config.header { ...config.header, Authorization: Bearer xxx }; return config; }); http.useRequestInterceptor((config) { console.log(请求拦截器2: 数据加密); if (this._needEncrypt(config.url)) { config.data SecurityHelper.encryptData(config.data); } return config; }); http.useResponseInterceptor((response) { console.log(响应拦截器1: 数据解密); // ... 解密逻辑 return response; });3.3 优缺点与适用场景优点灵活可控想给哪个模块用就给哪个模块用可以轻松实现不同模块不同的加密策略。安全无副作用完全不修改全局对象不会影响其他库也不会导致全局性故障。功能强大易扩展可以很方便地集成各种高级功能打造团队专属的请求库。易于调试和测试因为是一个独立的函数或类你可以单独对它进行单元测试逻辑清晰。缺点有侵入性你需要改变开发习惯在所有需要加密的地方调用MyRequest.request而不是wx.request。如果老项目改造替换工作量较大。依赖团队规范如果团队成员不遵守规范还是直接用了wx.request那么加密就失效了。适用场景多团队协作项目你们只负责其中几个模块的加密其他模块由其他团队负责互不干扰。渐进式改造的老项目可以先在新增模块中使用自定义请求函数逐步替换旧代码。需要高度定制化网络层的项目比如需要复杂的重试、缓存、队列等策略。4. 方案三中间件与装饰器模式 —— 优雅的“组合拳”如果你觉得方案一太“野”方案二又太“散”那么中间件Middleware或装饰器Decorator模式可能更适合你。它们追求的是高内聚、低耦合和可插拔的优雅架构。4.1 中间件模式像流水线一样处理请求中间件模式的思想是把请求处理过程看作一条流水线。一个请求会依次经过多个“处理站”中间件每个处理站干一件特定的事比如加密、加Token、日志最后才真正发送出去。响应回来也同理。// middleware-engine.js class MiddlewareEngine { constructor() { this.requestMiddlewares []; this.responseMiddlewares []; } // 添加请求中间件 useRequestMiddleware(middleware) { this.requestMiddlewares.push(middleware); } // 添加响应中间件 useResponseMiddleware(middleware) { this.responseMiddlewares.push(middleware); } // 执行请求 async executeRequest(config) { let currentConfig { ...config }; // 顺序执行请求中间件 for (const middleware of this.requestMiddlewares) { // 中间件可以是同步函数也可以是返回Promise的异步函数 currentConfig await Promise.resolve(middleware(currentConfig)); } // 发送网络请求 const rawResponse await this._sendRequest(currentConfig); let currentResponse { ...rawResponse }; // 顺序执行响应中间件逆序执行也很常见看设计 for (const middleware of this.responseMiddlewares) { currentResponse await Promise.resolve(middleware(currentResponse)); } return currentResponse; } _sendRequest(config) { return new Promise((resolve, reject) { wx.request({ ...config, success: (res) resolve(res), fail: (err) reject(err) }); }); } } // 使用示例 const engine new MiddlewareEngine(); // 1. 添加一个加密中间件 engine.useRequestMiddleware((config) { if (config.url.includes(/secure/)) { console.log(加密中间件执行); return { ...config, data: SecurityHelper.encryptData(config.data), header: { ...config.header, X-Encrypted: true } }; } return config; }); // 2. 添加一个日志中间件 engine.useRequestMiddleware((config) { console.log([${new Date().toISOString()}] 请求: ${config.method} ${config.url}); return config; }); // 3. 添加一个解密中间件 engine.useResponseMiddleware((response) { if (response.header[X-Encrypted]) { console.log(解密中间件执行); response.data SecurityHelper.decryptData(response.data); } return response; }); // 业务代码调用 async function fetchUserData() { try { const response await engine.executeRequest({ url: https://api.example.com/secure/user, method: POST, data: { id: 1 } }); console.log(最终数据:, response.data); } catch (error) { console.error(请求失败:, error); } }这种模式的魅力在于你可以动态地增删中间件。比如在开发环境你可以移除加密中间件直接看明文数据在生产环境再加上。每个中间件职责单一易于测试和维护。4.2 装饰器模式给函数动态“穿衣服”装饰器模式是另一种思路它不改变函数本身而是返回一个包装过的函数。在JavaScript中我们可以用高阶函数轻松实现。// decorator.js // 一个基础的请求装饰器 function createRequestDecorator(originalRequest) { return function decoratedRequest(config) { // 前置处理加密 const processedConfig encryptRequest(config); // 调用原始函数 return originalRequest(processedConfig).then(response { // 后置处理解密 return decryptResponse(response); }); }; } // 假设我们有一个基础的request函数可以是wx.request的Promise封装 function baseRequest(config) { return new Promise((resolve, reject) { wx.request({ ...config, success: resolve, fail: reject }); }); } // 应用装饰器 const decoratedRequest createRequestDecorator(baseRequest); // 使用装饰后的函数 decoratedRequest({ url: /api/data, method: GET }) .then(data console.log(data)) .catch(err console.error(err));你甚至可以组合多个装饰器实现功能的叠加function withEncrypt(originalRequest) { return function(config) { /* 加密逻辑 */ return originalRequest(config); }; } function withLogging(originalRequest) { return function(config) { /* 日志逻辑 */ return originalRequest(config); }; } function withAuth(originalRequest) { return function(config) { /* 认证逻辑 */ return originalRequest(config); }; } // 组合装饰一个同时具备认证、加密、日志功能的请求函数 const superRequest withLogging(withEncrypt(withAuth(baseRequest)));4.3 优缺点与适用场景优点结构清晰高内聚低耦合每个中间件或装饰器只关心一件事代码易于理解和维护。可插拔灵活度高可以像搭积木一样组合功能轻松应对变化的需求。易于测试每个中间件都可以独立进行单元测试。缺点概念有一定门槛需要开发者理解函数式编程、高阶函数、Promise链等概念。可能会引入性能开销经过的中间件越多调用栈越深理论上会有微小的性能损耗通常可忽略不计。错误处理需要精心设计中间件链中任何一个环节出错都需要有机制能捕获并传递错误。适用场景中大型复杂项目需要清晰的架构来管理众多横切关注点Cross-cutting Concerns如日志、监控、加密、认证等。需要高度可配置和可扩展性的项目不同环境、不同客户可能需要不同的功能组合。5. 方案对比与最佳实践选择聊了这么多方案到底该选哪个别急我帮你整理了一个对比表格并附上我的实战选择建议。方案侵入性灵活性维护成本调试难度适用场景Object.defineProperty高修改全局对象低全局规则难以特例低改一处全生效高全局黑盒全站统一加密、强安全合规项目自定义封装函数中需替换调用方式高可针对不同模块中需统一团队规范低逻辑集中清晰多团队协作、渐进式改造、模块化需求中间件/装饰器低不修改原生包装调用极高动态组合中高架构设计复杂中链路清晰中大型复杂项目、需要高度可配置和可扩展性我的实战选择建议新手项目或快速原型如果你的项目不大对加密要求明确且统一我建议直接从自定义封装函数开始。它简单直观功能足够也能为后续扩展打下基础。在utils目录下建一个http.js把封装好的请求函数放进去全项目导入使用即可。成熟的中大型项目全栈可控如果项目是你们公司全权负责且安全要求极高必须确保无一遗漏那么Object.defineProperty全局拦截是值得考虑的。但务必做好两件事一是设计完善的白名单和开发环境开关方便调试二是写好详细的错误日志和监控一旦出问题能快速定位。架构复杂的平台型产品如果你们在做的是一个需要支持多种业务、多种配置的开放平台或复杂应用中间件模式是你的好朋友。它能让你的网络层变得非常灵活未来新增认证方式、更换加密算法、添加性能监控等都只需要增删中间件即可核心业务代码几乎不用动。混合场景推荐组合拳在实际项目中我经常采用一种混合策略。底层使用一个轻量的、基于Promise封装的自定义请求库方案二它提供最基础的请求、拦截器能力。在应用启动时根据配置动态注入全局拦截器方案一的思路或中间件方案三的思路。这样既保证了基础功能的稳定和清晰又获得了全局处理的能力。例如在app.js中判断如果是特定版本或环境就向请求库中注入全局的加密解密中间件。最后无论选择哪种方案一定要记住没有银弹。最适合的方案取决于你的团队规模、项目阶段、安全要求和技术栈。从小处着手选择一个当前最适合的并随着项目演进不断重构优化这才是工程实践的正道。我在多个项目中实践过这些方案最大的体会是代码的清晰度和可维护性往往比追求极致的技巧更重要。先把功能做稳把逻辑写清之后再考虑如何让它变得更优雅。