微信小程序对请求/响应拦截增强全解析

写在前面

在微信小程序开发中,请求/响应拦截是处理加密通信、统一错误处理、日志记录等需求的常见技术手段。

随着数据安全的意识越来越高,客户对数据加密的要求也越来越严格。之前做的几个项目中,都已经要求请求/响应的数据进行全链路加密。在微信小程序端,对于请求/响应的数据也需要加密。但是项目不同,微信小程序的开发环境不同,对微信小程序请求/响应加密的拦截方式就不同(有的直接用Object.defineProperty方式进行全局拦截;有的自定义封装方法对指定请求/响应进行拦截加密),在这几个项目中,有两个比较典型的例子:

  1. 整个项目全是自己公司做,微信小程序的所有请求/响应的加密/解密方式都一致,此时就可以采用Object.defineProperty方式进行全局拦截加解密;
  2. 项目由多个公司合作,微信小程序的请求/响应加解密方式不一致,此时为了不影响友商的请求/响应,就采用饿了自定义封装方法对自己的请求/响应进行拦截加解密。

既然用到了这些拦截增强的方式,那就记录一下。

1. Object.defineProperty拦截方式

1.1 实现原理

Object.defineProperty 是 JavaScript 中用于定义对象属性的方法。通过它,我们可以重新定义 wx.request 的行为,在每次调用时插入自定义逻辑。具体步骤如下:

  1. 创建原始 wx 对象的副本: 为了确保原生 wx.request 方法不会被覆盖,我们先保存一个原始 wx 对象的副本。
  2. 重定义 wx.request: 使用 Object.defineProperty 重新定义 wx.request,使其在调用时执行我们自定义的逻辑。
  3. 处理请求参数: 在实际发送请求之前,根据业务需求加密或修改请求参数。
  4. 处理响应数据: 在接收到响应后,解密或处理响应数据,并调用原始的成功回调函数。
  5. 确保原始请求执行: 最后,确保原始的 wx.request 方法被执行。

1.2 Object.defineProperty详细说明

Object.defineProperty 是 JavaScript 中用于直接操作对象属性的内置方法。它允许你以细粒度的方式定义或修改对象的属性,包括控制属性的可写性、可枚举性、可配置性等。这对于创建不可变属性、拦截属性访问和修改等场景非常有用。

  1. 语法
javascript 复制代码
Object.defineProperty(obj, prop, descriptor)
  • obj:要在其上定义属性的对象。
  • prop:要定义或修改的属性名称。
  • descriptor:描述符对象,用于描述新属性的行为。
  1. 描述符类型

描述符有两种主要类型:

  1. 数据描述符(Data Descriptor):描述一个具有值的属性。
  2. 存取描述符(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:同数据描述符。
  1. 示例

定义一个只读属性

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 优缺点

优点:

  1. 全局生效:所有地方使用 wx.request 都会被拦截,无需逐个修改。
  2. 灵活性高:可以在请求前和响应后插入任意逻辑。
  3. 兼容性好:不会影响原有代码结构,只需在启动时初始化即可。

缺点:

  • 复杂度较高:需要理解 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),显示处理加密和请求调用,避免修改原生对象。这种方式更加直观,易于理解和维护。具体步骤如下:

  1. 创建新的请求函数:定义一个新的函数(如 rlzyHttpAjax),该函数接受与 wx.request 相同的参数。
  2. 处理请求参数:在实际发送请求之前,根据业务需求加密或修改请求参数。
  3. 处理响应数据:在接收到响应后,解密或处理响应数据,并调用用户提供的回调函数。
  4. 调用 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>
  • 响应拦截部分
    • 在请求成功的回调函数中,判断响应状态码是否为 200,如果是且响应数据中包含加密报文,则进行解密处理,并更新响应数据。
    • 在请求失败的回调函数中,根据是否提供了 <font style="background-color:rgb(249, 250, 251);">fail</font> 回调函数进行不同的处理,如果提供了则调用该回调函数,否则显示一个请求失败的模态框。

2.3 优缺点

优点:

  1. 灵活性:仅对指定请求生效,不影响其他逻辑。
  2. 安全性:不修改全局对象,兼容性更好。

缺点:

  1. 代码侵入性较大 :需要在每个请求处手动调用 <font style="background-color:rgb(249, 250, 251);">rlzyHttpAjax</font> 函数,而不是直接使用 <font style="background-color:rgb(249, 250, 251);">wx.request</font>
  2. 全局拦截困难:如果需要对所有请求进行拦截,需要在每个请求处手动调用该函数,不够方便。

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);

实现步骤:

  1. 定义装饰器函数:创建一个装饰器函数,接收原函数作为参数。
  2. 包装原函数:在装饰器函数内部,调用原函数并添加额外逻辑。
  3. 应用装饰器:将装饰器应用于 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);
});

详细讲解

  • 定义两个数组 requestInterceptorsresponseInterceptors 分别用于存储请求拦截器和响应拦截器。
  • 定义 addRequestInterceptoraddResponseInterceptor 函数用于添加拦截器。
  • 定义 customRequest 函数,该函数接受一个请求配置对象作为参数,并返回一个 Promise。
  • customRequest 函数中,首先将请求配置对象包装成一个 Promise,然后依次执行请求拦截器,对请求配置对象进行处理。
  • 调用 wx.request 发送请求,在请求成功的回调函数中,将响应数据包装成一个 Promise,然后依次执行响应拦截器,对响应数据进行处理。
  • 最后,通过 thencatch 方法处理请求的成功和失败。

优缺点

  • 优点
    • 灵活可扩展:可以方便地添加和移除拦截器,对请求和响应进行不同的处理。
    • 代码结构清晰:模拟 Axios 的拦截器机制,代码结构清晰,易于理解和维护。
  • 缺点
    • 代码复杂度较高:需要实现拦截器的添加、执行等逻辑,代码相对复杂。

6. 方案对比与最佳实践

方案 侵入性 灵活性 维护成本 适用场景
<font style="color:rgb(64, 64, 64);">Object.defineProperty</font> 全站统一处理
自定义封装方法 模块化需求
中间件模式 需要异步控制的复杂逻辑
代理模式 高兼容性要求的渐进式增强

最佳实践建议

  1. 全站加密/鉴权 :优先选择 <font style="color:rgb(64, 64, 64);">Object.defineProperty</font>,但需测试兼容性。
  2. 模块化需求:使用自定义封装方法或中间件模式。
  3. 高可维护性:代理模式结合 TypeScript 类型检查,提升代码健壮性。

7. 总结

微信小程序的请求拦截可通过多种方式实现,选择时需权衡 侵入性灵活性维护成本 。对于大多数项目,推荐结合使用 中间件模式 (业务层)和 代理模式 (底层库),在保证灵活性的同时降低耦合度。若需强制全局处理,<font style="color:rgb(64, 64, 64);">Object.defineProperty</font> 仍是高效选择,但务必预留调试接口,避免"黑盒化"过深。

相关推荐
anyup_前端梦工厂2 小时前
了解几个 HTML 标签属性,实现优化页面加载性能
前端·html
前端御书房2 小时前
前端PDF转图片技术调研实战指南:从踩坑到高可用方案的深度解析
前端·javascript
2301_789169542 小时前
angular中使用animation.css实现翻转展示卡片正反两面效果
前端·css·angular.js
风口上的猪20153 小时前
thingboard告警信息格式美化
java·服务器·前端
程序员黄同学3 小时前
请谈谈 Vue 中的响应式原理,如何实现?
前端·javascript·vue.js
爱编程的小庄4 小时前
web网络安全:SQL 注入攻击
前端·sql·web安全
宁波阿成5 小时前
vue3里组件的v-model:value与v-model的区别
前端·javascript·vue.js
柯腾啊5 小时前
VSCode 中使用 Snippets 设置常用代码块
开发语言·前端·javascript·ide·vscode·编辑器·代码片段
weixin_535854225 小时前
oppo,汤臣倍健,康冠科技,高途教育25届春招内推
c语言·前端·嵌入式硬件·硬件工程·求职招聘
扣丁梦想家5 小时前
设计模式教程:装饰器模式(Decorator Pattern)
java·前端·装饰器模式