微信小程序之路由拦截器

初次接触到路由拦截器是在Vue-ReSource中,后来使用axiosrestify实现jwt单页面应用用户验证的时候也用到了路由拦截器的功能。在微信小程序中,官方只提供了最基本的wx.request,现在打算对这个接口进行封装,在小程序中实现简单的路由拦截器。

<!--more-->

我对于路由拦截器的理解是:在发送请求之前接收到响应后(处理数据之前)执行的某些操作。整个过程可以看作:

发送请求前——发送请求——接收到响应后——处理响应数据

“发送请求”和“处理响应数据”在每个请求中可能都是不相同的,但是“ 发送请求前”和“接收到响应后”的逻辑处理可能都相似的,举个很常见的需求:发送请求时显示Loading提示,接受到响应后关闭提示。鉴于每次请求都得实现这样的需求,因此必须采取某种措施统一处理,这里的措施指的就是拦截器

1. 修饰函数

看见上面的执行顺序,直接想到的是使用回调函数修饰wx.request

Function.prototype.before = function(beforeFn){
    var _self = this;
    return function(){
        beforeFn.apply(this, arguments);
        _self.apply(this, arguments);
    }
}
Function.prototype.after = function(afterFn){
    var _self = this;
    return function(){
        _self.apply(this, arguments);
        afterFn.apply(this, arguments);
    }
}

var ajax = function(){
    console.log("send request");
}

var modifiyFn = ajax.before(function(){
    console.log("config params...");
}).after(function(){
    console.log("recive some data...");
});

modifiyFn();

上面的代码看起来能达到我们的需求,实现起来也很方便,但是我们忽略了一个很关键的问题:请求是异步的!wx.request的返回数据是通过制定配置参数的success方法处理的

wx.request({
      url,
      data,
      success(res){
          // res.data
      }
})

2. Promise实现

很明显,通过为请求方法添加修饰函数并不能达到我们的需求,因为同步代码总是优于异步代码执行的。转念一想,链式的异步调用Promise来处理刚刚好啊。

2.1. 接口promosie化

前面提到,小程序的请求采用的是指定回调函数的方式而没有进行Promise,不过微信的接口大都比较统一:

  • success指定成功处理函数
  • fail指定失败处理函数

这样我们可以自己将接口封装一下

let wxPromise = function (fn){
    return function (opt = {}) {
        return new Promise((resolve, reject) => {
            opt.success = function (res) {
                console.log(resolve);
                resolve(res);
            };

            opt.fail = function (res) {
                reject(res)
            };

            fn(opt);
        })
    }
};

然后将wx.request进行封装,需要注意这里可能会改变原生wx.request中的this指向,如果内部实现没有使用wx的话,这个问题应该也没啥吧

// 获取Promise风格的wx.request接口
let wxRequest = wxPromise(wx.request);
// 测试
wxRequest({ url, data }).then((res)=>{
      console.log(res.data);
})

2.2. Promise的链式调用

OK,现在我们已经可以使用promise的风格来调用wx.request()了,整个拦截器的基本思路就是依次调用请求拦截器,具体请求和响应拦截器。

不论是拦截器还是具体请求,我们都将他们看作是某个抽象的操作,然后使用一个队列进行维护并依次调用,只要每个操作都返回了正确的值,Promise的链式操作就可以执行下去,之前实现了一个简单的Promise,关于Promisethen方法链式调用,这里就不折腾了。

这个队列应该保存着拦截操作和具体请求,具体请求在前面已经提到了,就是封装了得wx.request,那么拦截操作应该是什么样子呢?无非是回调函数罢了,只不过为了维持Promise的链式调用,每个操作都应该返回值。

// 执行队列
let chain = [(config)=>{
    return wxRequest(config).then((result)=>{
        return result;
    });
}, undefined];

// 拦截器大概就是这个样子的
let interceptors = {
      request: [{
          fulfilled(){},
          rejected(){},
      }],
      response: [{
          fulfilled(){},
          rejected(){},
      }]
};

// 添加请求拦截器到执行队列前方
interceptors.request.forEach((item)=>{
    chain.unshift(item.fulfilled, item.rejected);
});
// 添加响应拦截器到执行队列后方
interceptors.response.forEach((item)=>{
    chain.push(item.fulfilled, item.rejected);
});

// chain现在拥有了基本完整的执行链,除了处理响应数据的处理,这个是需要在最后的then方法中自己注册的

那么,如何将队列中保存这的拦截操作和具体请求(一般都是回调函数)转变成Promise对象呢呢?这个在之前写的一个”NodeJS创建层级目录“的插件(代码写的太烂现在废弃掉了)中尝试过,使用一个while循环就可以了,在axios中发现了更准确的写法

let chain = []; 

let promise = Promise.resolve(config);
while(chain.length){
    // 相邻元素一个为resolve,一个为reject
    promise = promise.then(chain.shift(), chain.shift());
}
// 最后拿promise搞点什么事情
return promise;

2.3. 完整实现

上面并没有提到如何注册拦截器,实际上无非就是向request和response中添加回调函数罢了,下面上完(cou)整(zi)代(shu)码

import CONFIG from "./_config";
import { wxRequest } from "../utils/wxPromise";

class Resource{
    constructor(){
        this.interceptors = {
            request: [],
            response: []
        };

        this.config();
    }

    config(params = CONFIG){
        this.host = params.host ;

        return this;
    }

    before(opt){
        this.interceptors.request.unshift(opt);
        return this;
    }
    after(opt){
        this.interceptors.response.push(opt);
    }

    request(url, method ,params = {}){

        // 还原wx.request的参数格式
        let config = {
            url: this.host + url,
            data: params,
            method: method
        };

        let chain = [(config)=>{
            return wxRequest(config).then((result)=>{
                // 将响应状态和数据绑定到resource实例上
                this.status = result.statusCode;
                this.data = result.data;

                return result;
            });
        }, undefined];

        // 添加中间件到执行链上
        this.interceptors.request.forEach((item)=>{
            chain.unshift(item.fulfilled, item.rejected);
        });
        this.interceptors.response.forEach((item)=>{
            chain.push(item.fulfilled, item.rejected);
        });

        let promise = Promise.resolve(config);

        while(chain.length){
            promise = promise.then(chain.shift(), chain.shift());
        }

        return promise;
    }

}

// 基本方法
['GET', 'POST', 'HEAD', 'DELETE', 'PUT'].forEach((item)=>{
    let method = item.toLowerCase();
    Resource.prototype[method] = function (url, params = {}) {
        return this.request(url, item ,params);
    }
});

let resource = new Resource();

export default resource;

中间还做了一点其他处理,按照HTTP请求方式注册了快捷请求方法(主要是为了与后台实现RESTful接口)。实际上我认为拦截器的实现,最主要的就是理解Promise的链式调用,以及操作队列的正确顺序。

3. 测试

下面来简单测试一下

resource.before({
    fulfilled(config){
        console.log("begin ...");
        wx.showLoading({
            title: '加载中',
        });
        return config;
    }
});

resource.after({
    fulfilled(data){
        wx.hideLoading();
        return data;
    }
});

Page({
   onLoad(){
       resource.post("test", {name:"txm"}).then((data)=>{
           console.log(data);
       })
   }
});

由于都是基于Promise的,因此也可以在拦截器中执行异步操作,并在操作完成之后再进行具体操作。想到之前那个在每个页面都发送重复的用户身份验证真是醉了,尽管不是我写的(/掩面)。 总之,这个拦截器只是在学习并实现Promise之后用来练手的一个小项目,本身跟小程序也没有特别大的关系,一切都是碰巧了,哈哈...