合作QA是大聪明?撸个接口校验工具保命(5)

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的29天,点击查看活动详情

一、 背景 & 前情回顾

上一篇小作文开始实现我们的 RuleSet 类型,逐步分析每个步骤的要求,目前主要讲述了以下方法:

  1. 构造函数,初始化实例属性,调用 doInterceptor 方法注册拦截器;
  2. addErrHander,兜底 console.error 抛出异常,支持自定义的方式;
  3. doIntercetpor,注册拦截器,拦截器收集每次请求时的 config 对象的引用到 this.queue,网络空闲时进行校验;

本篇小作文的重点将放在校验部分的核心实现方法 this.exec 方法。

二、 doInterceptor 为什么搞成异步的?

书接上文,先来回顾一下 RuleSet.prototype.doInterceptor 方法:

拦截器收集 config 到 this.queue;网络空闲时 调用 this.exec 进行参数校验; 队列保存 + 异步校验确保参数都可以被校验;这么做的好处无关乎参数的添加时机;异步校验可以不阻塞请求的发出,更好的性能表现;

设计成异步主要有以下两方面的考虑:

  1. 确保请求拿到的参数是最终的参数

根据经验来看,这种校验类工具一般都会在框架初始化之初就要加入到工具中,以确保第一个请求发出前这个工具添加的拦截器(下称检验拦截器)就已经生效。

但是这也有一个巨大的副作用,大家都知道,在你的项目中拦截器会有很多个,其中很多不乏都是用来对某个接口中的参数进行修改、添加的(下称修改拦截器)。

一旦这种修改拦截器注册时机晚于咱们的校验拦截器,这就会导致检验拦截器进行校验时,参数尚未经过修改拦截器的改动而没有得到最终的参数值,这就会引发校验拦截器的误报。这么说有点抽象,我们举个例子:

// schema
const schema = {
  '/some/api/name': {
     param1: {
       type: /^iso/g
     }
  }
}


// main.js 
import axios from 'axios';
createRuleSetInstance(axios, schema); // 创建 RuleSet 实例的同时就会注册校验拦截器

// 下单页面的 js 模块,在经过下面的拦截器前,
// 假设 some/api/name 中没有 param1 参数
// order.js
import axios from 'axios';
axios.interceptors.request.use((cfg) => {
  if (cfg.url.includes('some/api/name')) {
    cfg.params.param1 = 'iso900000';
  }
  return cfg
})

上面的例子中可以看出,校验拦截的注册早于真正的模块拦截器,如果此时校验就会得到一个错误,因为 param1 参数还没经过初始化,值是 undefined 无法通过正则校验;

为什么变成异步就可以了?变成异步的意义在于检验拦截器执行时,并不会立刻执行校验,而只是吧 config 的引用收集到队列中,这就完成了,当后续的拦截器执行时修改 config 对象中的参数时,正因为队列中放的是引用,当校验时就可以拿到最终发送请求时的参数值;

  1. 性能更好

发送请求的过程中如果做阻塞的同步校验当接口参数过多、或者参数结构过于复杂时就会导致发送请求时机被延后,这是没有意义的;我们选择网络空闲时触发校验,并不阻塞;

三、 RuleSet.prototype.exec

方法参数:cfg,网络库中拦截器接收到的配置对象,其中包含了本次请求的全部信息,例如 url、method(http method,GET/POST/PUT/OPTIONS/HEADERS/DELETE...)、params(GET 请求或URL末尾携带的参数)、data(POST 方法请求参数)、当然还有头信息;

方法作用:该方法作为一个校验的入口方法,主要做了以下工作;

  1. 获取 cfg 的 url 和 method;
  2. 从 url 中截取接口名,不包含域名,当然这里我们默认了不同域名下不存在相同接口名,如果有这方面的诉求,可以接收一个配置,是否匹配域名等;
  3. 从 this.schema 中获取当前接口的 schema 对象,紧接着混入(mixin)公参 schema(schema[*])。这里就是公参只需要声明一次就可以校验各个接口的公参的原理;
  4. 调用 this.getMethod 方法校验当前请求的 http method 是否符合 schema 的设定;
  5. 深复制请求参数;为什么这么做?是因为我们后面的校验操作会对部分参数进行修改,为了很好的隔离,不应该再原有的 config 基础上进行操作,确保校验工具没有副总用;
  6. 调用 this.traverse 进行遍历开始校验当前接口的参数是否符合 schema 的设定;
class RuleSet {
  constructor (xfetch, schema) {}

  exec (cfg) {
    // 1. 校验方法
    // 2. 调用预处理器
    // 3. 开始匹配并收集错误
    let { url, method } = cfg
    let api = /https://(?:[a-zA-Z-.])+/([^?]+)/g.exec(url)[1]
    let apiSchema = this.schema[api]
    if (!apiSchema) return cfg
    
    // 混入公参 schema
    // mixin common params
    apiSchema = Object.assign({}, apiSchema, this.schema['*'])

    // 检验 HTTP METHOD
    // match HTTP method
    this.testMethod(api, apiSchema, method)

    // 深复制原有 config 上的参数,确保校验的工作不会污染源数据
    // deep cp params avoid polluting the origin
    let finalParam = deepCopy({ ...cfg.params, ...cfg.data })
   
    // 递归遍历校验参数
    this.traverse(api, apiSchema, finalParam)
  }
}

四、总结

本篇小作文主要讨论了以下两个问题:

  1. RuleSet.prototype.doInterceptor 方法创建的拦截器为什么没有同步阻塞的执行校验逻辑?

    • 1.1 主要是防止校验拦截器过早注册但是后续的修改拦截器对参数进行额外修改时获取不到最终的数据结果;
    • 1.2 另一方面就是这么做可以有更好的性能表现;
  2. RuleSet.protoype.exec 方法的细则问题,主要工作就是校验方法:

    • 2.1 解析 config.url 和 config.method 备用;
    • 2.2 将公参 schema['*'] 混入到各个接口的 schema 中;
    • 2.3 执行 this.testMethod 校验方法;
    • 2.4 深复制 config.param/data 到 finalParam 排除校验对源数据的影响;
    • 2.5 调用 this.traverse 进行参数参数校验