axios 二次封装-拦截器队列

查看axios的源码,会发现拦截器是由简单数组实现,挂载use eject 方法。拦截器注册的Id实际就是数组的编号,实现的核心在request调用前的拦截器队列编排上。满足平常开发,如果想做扩展就有些限制,所以这里希望通过提供一个自定义的拦截器中间层。提供些扩展功能。

目标

  • 自定义标识id
  • 自定义请求端拦截器筛选
  • 执行顺序与绑定顺序一致
  • 链式调用

使用

import { CandyPaper } from './src/core'
import { Interceptor } from './src/interceptor'
const http = new CandyPaper()
const task = {
  key: 'taskId',
  fulfilled: (ctx: AxiosRequestConfig) => {...},
  rejected: (ctx: AxiosResponse) => {...},
  runWhen: (ctx: AxiosRequestConfig) => true
}


// 注册拦截器
http.interceptor.request
// 模式一
.use(
  task.fulfilled
)
// 模式二
.use(
  task.fulfilled,
  task.rejected
)
// 模式三
.use(
  task.key,
  taks.fulfilled,
  taks.rejected
)
// 模式四
.use( task )


// 注销拦截器
 http.interceptor.request
 // 模式一
.eject(
  task.key
)
// 模式二
.eject(
  task.fulfilled
)


// 拦截器筛选


http.request(
  url: '/',
  // 忽略 'task' 拦截器
  $intercepteFilter: interceptor.Interceptor.excluedByKeys([
      task.key
  ])
)


http.request(
  url: '/',
  // 只执行 'task' 拦截器
  $intercepteFilter: interceptor.Interceptor.incluedByKeys([
      task.key
  ])
)

实现

缓存队列

因为需要自定义标识,同时需要保存注册顺序。 这里通过扩展Map类型实现


/**
 * map 扩展
 * @props list 键队列
 * @fn $set 设置新值, 并缓存键
 * @fn $get 获取值, 可接收key数组 
 * @fn $delete 删除值, 并移除键
 * @fn $map 遍历键
 * @fn $clear 清空
 */
export class Lmap<K, V> extends Map<K, V>{
  
  // key 添加记录
  list: K[] = []
  
  constructor(entries?: readonly (readonly [K, V])[] | null){
    super(entries)
    entries && entries.map(([key]) => this.list.push(key))
  }


  $set(key: K, value: V): this {
    super.set(key, value)
    this.list.push(key)
    return this
  }
  
  $delete(key: K): boolean {
    if(!this.has(key)){
      return true
    }
    const status = super.delete(key)


    if(status){
      this.list = this.list.filter(k => k != key)
    }
    
    return status
  }


  $clear(){
    this.clear()
    this.list = []
  }


  $get(keys: K): V
  $get(keys: K[]): V[]
  $get(keys: K | K[]): (undefined | V | V[]) {


    if(!keys){
      return
    }
    
    if(!Array.isArray(keys)){
      return this.get(keys)
    }


    return keys.map(k => this.get(k)).filter(i => !!i) as V[]
  }




  $map<T = any>(cb:(k: K) => T): T[]
  $map<T = any>(cb:(k: K, v?: V) => T):T[] 
  $map<T = any>(cb: (k: K, v?:V) => T): T[]{
    return this.list.map(key => cb(key, this.get(key)))
  }
  
}

扩展aixos类型定义


// ./types/shime.aixos.d
import { AxiosRequestConfig, AxiosInterceptorManager } from 'axios'


declare module 'axios'{


  export interface AxiosRequestConfig{
    // 扩展请求配置参数
    $intercepteFilter?: (keys: IndexKey[]) => IndexKey[]
  }
  
  // 任务拦截
  interface Fulfilled<T = any>{
    (d: T): T | Promise<T>
    // 自定义标识
    key?: IndexKey
  }
  
  // 错误拦截
  interface Rejected{
    (err: any): any
    key?: IndexKey
  }
  
  // 拦截器端,筛选函数
  // @types/aixos未做定义,需要自己补充
  export type RunWhen = (conf:AxiosRequestConfig) => boolean | null
  
  // 拦截器项定义
  export interface InterceptorHandler<V>{
    key?: IndexKey
    fulfilled: Fulfilled<V>
    rejected?: Rejected
    runWhen?: RunWhen
  }
  
  // 扩展拦截器定义
  // @types/aixos未做定义,需要自己补充
  export interface AxiosInterceptorManager<V>{
    handlers: InterceptorHandler<V>[]
  }
}

拦截器中间层

import { 
  AxiosRequestConfig, 
  InterceptorHandler,
  Fulfilled,
  Rejected,
  RunWhen
} from 'axios'
import { Lmap } from './utils'
import  { is } from 'ramda'


interface  InterceptorItem <T = any> extends InterceptorHandler <T> {
  key: IndexKey
}




export interface InterceptorOptions {
  $intercepteFilter?: (keys: IndexKey[]) => IndexKey[]
}


/**
 * 拦截器
 */
export class Interceptor<T = AxiosRequestConfig>{


  // 拦截器队列
  handlers: Lmap<IndexKey, InterceptorItem<T>> = new Lmap()


  // 默认拦截器id生成器
  createDefKey(){
    return new Date().getTime() + this.handlers.list.length
  }


  /**
   * 注册拦截器
   * @param key 标识符
   * @param onFulfilled 任务函数
   * @param onRejected 错误捕获
   * @exmple
   * 模式一
   * use('token',  setToken)
   * 
   * 模式二
   * use(setToken, onError)
   * 
   * 模式三
   * use('token', setToken, onError)
   * 
   * 模式四
   * use({
   *   key: 'token',
   *   onFulfilled: setToken,
   *   onRejected: onError
   *   runWhen: (ctx: AxiosRequestConfig) => {...}
   * })
   */


  use(key: Fulfilled<T>):Interceptor<T>
  use(key: InterceptorItem<T>): Interceptor<T>
  use(key: IndexKey, onFulfilled: Fulfilled<T>): Interceptor<T>
  use(key: Fulfilled<T>, onFulfilled: Rejected): Interceptor<T>
  use(key: IndexKey, onFulfilled: Fulfilled<T>, onRejected: Rejected): Interceptor<T>
  use(key?: IndexKey | Fulfilled<T> | InterceptorItem<T>,  onFulfilled?: Fulfilled, onRejected?: Rejected ){


    let runWhen: RunWhen | undefined
    
    if(!key){
      return this
    }


    if(is(Function, key)){ 
      onRejected = onFulfilled
      onFulfilled = key
      key = onFulfilled.key || this.createDefKey()
    }
    
    if(is(Object, key)){
      const options = key as InterceptorItem<T>
      key = options.key
      onFulfilled = options.fulfilled
      onRejected = options.rejected
      runWhen = options.runWhen
    }


    if(this.handlers.has(key)){
      throw new Error(`拦截器已注册: ${String(key)}`)
    }


    if(onFulfilled){
      this.handlers.$set(key, {
        key,
        fulfilled: onFulfilled,
        rejected: onRejected,
        runWhen
      })


      onFulfilled.key = key
    }


    return this
  }




  /**
   * 注销拦截器
   * @param key 注册标识或注册函数
   * @example
   * 模式一
   * const task = () => {}
   * interceptor.use(
   *   task
   * )
   * interceptor.eject(task)
   * 
   * 模式二
   * interceptor.use(
   *  'taskId',
   *  task
   * )
   * interceptor.eject(task)
   * or
   * interceptor.eject('taskId')
   */
  eject(key: IndexKey):void
  eject(key: Fulfilled):void
  eject(key: IndexKey | Fulfilled){
    let _k: IndexKey | undefined
    if(is(Function, key)){
        _k = (key as Fulfilled).key
    }else{
      _k = key
    }
    
    _k && this.handlers.$delete(_k)
    
  }


  // 清空拦截器队列
  clean(){
    this.handlers.$clear()
  }


  // 构建任务队列
  queupUp(ctx: InterceptorOptions){


    // 如果拦截器筛选为空, 则应用所有已注册拦截器
    const filter =  ctx.$intercepteFilter ? ctx.$intercepteFilter : (keys: IndexKey[]) => keys


    // 筛选可用队列
    return filter(this.handlers.list).reduce((acc, next) => {
      const handler = this.handlers.get(next)
      return handler ? [
        ...acc,
        {
          ...handler
        }
      ] : acc
    }, [] as InterceptorHandler<T>[])
   
  }


  // 预设排除筛选
  static excluedByKeys(exclueds: IndexKey[]){
    return (handlersKeys: IndexKey[]) => handlersKeys.filter(key => !exclueds.includes(key)) 
  }


  // 预设包含筛选
  static incluedByKeys(inclueds: IndexKey[]){
    return (handlersKeys: IndexKey[]) => handlersKeys.filter(key => inclueds.includes(key)) 
  }


}

请求包装

import axios from 'axios'
import type { AxiosRequestConfig, AxiosInstance, AxiosResponse } from 'axios'
import { Interceptor, InterceptorOptions } from './interceptor'
import { is } from 'ramda'




export class CandyPaper{
  
  candy: AxiosInstance


  interceptor = {
    request: new Interceptor<AxiosRequestConfig>(),
    response: new Interceptor<AxiosResponse>()
  }   


  /**
   * @param config axios实例 or axios配置对象
   * @example
   * 1. new CandyPaper({ baseUrl: '/' })
   * 2. new CandyPaper(axios.create({baseUrl: '/'}))
   */
  constructor(config?: AxiosInstance)
  constructor(config?: AxiosRequestConfig)
  constructor(config?: AxiosRequestConfig | AxiosInstance){
    this.candy =  is(Function, config) ? config : axios.create(config)
  }


  // 重组请求拦截器列表
  protected resetInterceptors(ctx: InterceptorOptions){
    const interceptorRequests = this.candy.interceptors.request.handlers.filter(i => !i.key)
    const interceptorResponses = this.candy.interceptors.response.handlers.filter(i => !i.key)


    const middleRequests  = this.interceptor.request.queupUp(ctx).sort(() => -1)
    this.candy.interceptors.request.handlers = [
      ...interceptorRequests,
      ...middleRequests
    ].sort(() => -1) // 反序, 保证执行顺序与注册顺序一致
    
    const middleResponses = this.interceptor.response.queupUp(ctx)
    this.candy.interceptors.response.handlers = [
      ...interceptorResponses,
      ...middleResponses
    ]
  }


  request(options: AxiosRequestConfig){
    // 请求前重置拦截器队列
    this.resetInterceptors(options)
    return this.candy(options)
  } 
}

参考

axios 如何设计拦截器

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 199,902评论 5 468
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 84,037评论 2 377
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 146,978评论 0 332
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 53,867评论 1 272
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 62,763评论 5 360
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,104评论 1 277
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,565评论 3 390
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,236评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,379评论 1 294
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,313评论 2 317
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,363评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,034评论 3 315
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,637评论 3 303
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,719评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,952评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,371评论 2 346
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,948评论 2 341

推荐阅读更多精彩内容