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

1、 前文回顾

上篇小作文我们讨论 testMethod 和 travere 方法:

  1. testMethod 方法用于校验 http method 是否和符合预期。基于这种路数,我们甚至可以扩展一种校验请求头信息的功能,当然如果你有兴趣可以尝试一下~

  2. traverse 方法用于遍历 schema 对 finalParam 中的值进行校验;另外我么还讨论了几个细节问题:为啥遍历 schema 而不去遍历 finalParam、既然设计了简写方式为什么还要抹平简写方式 这两个问题;

2、预处理器

2.1 预处理器是啥?

这个设计我可以骄傲的说是这个工具中最好玩儿的设计了,先来看一个场景:我们下单接口需要传递一个这样的参数:

json 复制代码
es_id_list: "[{"pro": "sss", "e_id": "fds;jklf;jdssdjkeje"}, {"pro": "sss", "e_id": "fds;jklf;jdssdjkeje"}, {"pro": "sss", "e_id": "fds;jklf;jdssdjkeje"}, {"pro": "sss", "e_id": "fds;jklf;jdssdjkeje"}]"

从上面的代码可以看出来,这是个 JSON 格式的字符串,这个东西如果要校验改是个什么类型呢?

  1. 当个字符串校验?可以里面的 pro、e_id 也是毕传的,这个需要校验的;
  2. 当个数组校验?这个东东是个字符串,数组咋整?
  3. 写个 assert 函数先转 JSON 对象,再校验,这要是有 10 个,就要写是个 assert 函数,这也不太合理啊?

当然上面的三种方案肯定是最后一种最合理了,但是这种方案不优雅,怎么办呢?如果框架层面支持一种能力,可以在校验之前对数据进行一定的转换,然后再去校验这个不就优雅多了吗?

当然这就是预处理了,这个我们这一篇详细展开讨论!

我们的业务中有不少复杂参数都是这么传递的,所以我们想到一种预处理器机制,与 webpack 的行内 loader 很像:

  1. 我们使用 @ 符号表示预处理器,支持若干内建的预处理器;
  2. 支持多个同时使用,多个同时使用时从右向左依次调用;

2.2 预处理器示例

js 复制代码
// 这个类型对象表示先转成 JSON,
// 转完的结果应该是数组,数组项的类型为 esInterface 
const schema = {
  'some/api/name': {
    es_id_list: {
      type: 'array[interfact=esInterface]@toJson' // @toJson 即为预处理器
      esInterface: {
        pro: 'string'
        e_id: /\S{18}/g
      }
    }
  }
}

2.3 内建预处理器

  1. toJSON:JSON.parse 的语法糖,尝试把参数值变成 JSON 对象;
  2. toString: 尝试将参数值转成字符串;
  3. toNumber:尝试将参数转成数字;

2.4 自定义预处理器

自定义预处理器是一个函数,接收源数据,返回转换后的数据;

自定义预处理器需要和 type 同级别声明的同名方法,例如示例中的 myToJson 方法:

js 复制代码
const schema = {
  'some/api/name': {
    es_id_list: {
      type: 'array[interfact=esInterface]@myToJson' // @myToJson 即自定义预处理器
      myToJson (d) {
      // d: es_id_list 对应的原始值
        return JSON.parse(d);
      },
      esInterface: {
        pro: 'string'
        e_id: /\S{18}/g
      }
    }
  }
}

3、核心实现全部源码

js 复制代码
// assert (param, ctx, inject)

// value

// preProcessor @toJson&diyModifier1&modifier2&modifier3

// type =
// RegExp

// string number null undefined boolean enum

// enum enumRange[e1, e2, e3]

// interface=interfaceName

// array = array[any] array[string/number/null/undefined/boolean/interface=interfaceName]

import builtInProcessors from './builtin-processor'
import { BASIC_MAP } from './constants'
import { type, deepCopy, getBasicType } from './utils'
import { genAssertFailed, genRegExpTestFailed, genValueErr, genMethodErr, genTypeError } from './error-helper'

let timer = null

class RuleSet {
  constructor (xfetch, schema) {
    // 全局参数
    this.commonParams = schema['*']
    this.schema = schema
    this.errors = []
    this.queue = []

    this.utils = {
      getTypeOf: this.getTypeOf.bind(this)
    }

    this.doInterceptor(xfetch)
  }

  doInterceptor (xfetch) {
    if (!xfetch || !xfetch.interceptors) return console.error('no Xfetch instance or xfetch.interceptor is not defefined !')

    xfetch.interceptors.request.use((cfg) => {
      if (timer) clearTimeout(timer)

      timer = setTimeout(() => {
        this.queue.forEach(itm => this.exec(itm))
        this.queue.length = 0
        if (this.errors.length) {
          this.errorHander ? this.errorHander(this.errors) : console.error(this.errors)
        }
        this.errors = []
        timer = null
      }, 1000)

      // add into a queue for two factors
      // 1. this interceptor added too early to get params later added
      // 2. better performance, do not block xfetch request
      this.queue.push(cfg)
      return cfg
    })
  }

  addErrHandler (handler) {
    if (typeof handler !== 'function') {
      return console.error('the addErrHandler is not a function, fallback handler to console.error!')
    }
    this.errorHander = handler
  }

  splitTypeAndProcessor (schema) {
    if (schema.processors || /\[[^@]+@[^@]+\]/g.test(schema.type)) return
    let processors = /@([^?]+)/g.exec(schema.type)
    let s = processors && processors[1]?.split('&')
    if (s?.length) {
      schema.type = schema.type.replace(/@([^?]+)/g, '')
      schema.processors = s.map(p => {
        let ex
        if ((ex = (schema[p] || builtInProcessors[p])) && typeof ex === 'function') return ex
        console.warn(`${ex} pre-processor is not defined, which fallback to "(x) => x"`)
        return (noop) => noop
      })
      s = null
    }
  }

  getTypeOf (p) {
    return type(p)
  }

  testMethod (u, s, m) {
    // 检测 http 方法类型
    if (s.method && s.method.toLowerCase() !== m.toLowerCase()) {
      this.errors.push(genMethodErr(s, m))
    }
  }

  testValue (api, schema, key, value) {
    if (schema.value !== value) {
      this.errors.push(genValueErr(api, schema, key))
    }
  }

  testAssert (api, schema, key, val, inject) {
    // 调用断言
    let r = schema.assert(val, { api, ...this.utils }, inject)
    if (this.getTypeOf(r) === BASIC_MAP.boolean && r) return void 0
    else this.errors.push(genAssertFailed(api, schema, key, r))
  }

  preProcessor (schema, params) {
    // 预处理参数
    // 拿到类型@符号后面的方法名按照 & 拆开,
    // 从后向前调用,后一个的返回值作为下一个的入参
    let p = params
    let s = schema.processors
    if (s?.length) {
      p = s.reduceRight((pre, cur) => {
        try {
          return cur(pre)
        } catch (e) {
          console.warn('preProcessor calling failed and ignored, please check')
          return pre
        }
      }, params)
    }
    return p
  }

  testType (api, schema, key, value) {
    let type = schema.type

    // type enum
    if (type === 'enum' && !schema.enumRange?.includes(value)) {
      return this.errors.push(genTypeError(api, schema, key, value))
    }

    // type instanceof RegExp
    if (type instanceof RegExp && !type.test(value)) {
      return this.errors.push(genRegExpTestFailed(api, schema, key, value))
    }

    // not regExp
    let excludeProcessorExecResult = (/^[^@]+/g).exec(type)
    let typeWithoutPreProcessor = excludeProcessorExecResult ? excludeProcessorExecResult[0] : type
    let getBasic = getBasicType(typeWithoutPreProcessor)

    if (getBasic.length) {
      return !getBasic.includes(this.getTypeOf(value)) ? this.errors.push(genTypeError(api, schema, key, value)) : void 0
    } else {
      // complex type: array[type/any] / interface / array[interface]
      let interfaceReg = /^interface=([^@]+)/
      let arrayReg = /^array(?:\[([^\]]+)\])?/

      let interfaceResult = interfaceReg.exec(type)
      let interfaceName = interfaceResult ? interfaceResult[1] : ''

      let arrayResult = arrayReg.exec(type)
      let arrayResultDetail = arrayResult ? arrayResult[1] : ''

      if (interfaceName) {
        this.traverse(api, schema[interfaceName], value)
      } else if (arrayResult) {
        if (!Array.isArray(value)) {
          return this.errors.push(genTypeError(api, schema, key, value))
        }
        if (arrayResultDetail) {
          if (getBasicType(arrayResultDetail, 1)) {
            value.forEach((vItem, vIdx) => {
              let rebuiltKey = `${key}[${vIdx}]`
              let rebuiltSchema = {
                [rebuiltKey]: {
                  type: arrayResultDetail
                }
              }
              let rebuiltValue = {
                [rebuiltKey]: vItem
              }

              this.traverse(api, rebuiltSchema, rebuiltValue)
            })
          } else {
            // array[interface=someInterface]
            let tryInterfaceNameResult = interfaceReg.exec(arrayResultDetail)
            let interfaceName = tryInterfaceNameResult ? tryInterfaceNameResult[1] : ''
            value.forEach((itm, idx) => this.traverse(api, schema[interfaceName], itm))
          }
        }
      }
    }
  }

  traverse (api, schema, finalParam) {
    for (let key in schema) {
      if (!schema.hasOwnProperty(key)) continue
      let keySchema = schema[key]
      let withoutTypeOrValue = ['type', 'value'].every((itm) => typeof keySchema[itm] === 'undefined')
      schema[key] = withoutTypeOrValue && !keySchema.assert ? { type: keySchema } : keySchema

      keySchema = schema[key]

      // simplify type
      this.splitTypeAndProcessor(keySchema)

      let keyParam = finalParam[key]

      // assert
      if (keySchema.assert) {
        // construct provide
        let provide = {}
        keySchema.provide?.forEach((k) => {
          provide[k] = finalParam[k]
        })
        this.testAssert(api, keySchema, key, keyParam, provide)
        continue
      }

      // 处理 value
      if (this.getTypeOf(keySchema.value) !== BASIC_MAP.undefined) {
        this.testValue(api, keySchema, key, keyParam)
        continue
      }

      // preProcessor before type check
      let processedParam = this.preProcessor(keySchema, keyParam)

      // type check
      this.testType(api, keySchema, key, processedParam)
    }
  }

  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
    // mixin common params
    apiSchema = Object.assign({}, apiSchema, this.schema['*'])

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

    // deep cp params avoid polluting the origin
    let finalParam = deepCopy({ ...cfg.params, ...cfg.data })

    this.traverse(api, apiSchema, finalParam)
  }
}

export default function createApiParamCheckInstance (xfetch, apiSchema) {
  return new RuleSet(xfetch, apiSchema)
}

4、引用示例

以下为校验目录 api-scheme 的结构

text 复制代码
.
├── common.js
├── estimate.js
├── home.js
└── index.js

入口为 index.js 模块:

js 复制代码
import common from './common'
import estimate from './estimate'
import home from './home'
export default {
  ...home,
  ...common,
  ...estimate
}

其中 common.js 为公参模块的校验规则:

这里给出了一个 白名单机制,作为某些接口不要公参的出口。

  • common.js
js 复制代码
// whiteList but absolutely not recommended
// it decreases the Constraint of checking params
let commonParamsWhiteList = [
  // 'xx/xx'
]
const isInWhiteList = k => commonParamsWhiteList.includes(k)
export default {
  '*': {
    xx: {
      assert (param, ctx, inject) {
        if (isInWhiteList(ctx.api)) return true
        return ctx.getTypeOf(param) === 'String' ? true : 'xx is a CommonParam, please check'
      }
    },
    ee: {
      assert (param, ctx, inject) {
        if (isInWhiteList(ctx.api)) return true
        if (param && param.length > 0) {
          return true
        } else {
          return 'TypeError: ee should be tested ~~~~~~'
        }
      }
    },
    rr: {
      assert (param, ctx, inject) {
        if (isInWhiteList(ctx.api)) return true
        if (/^\d+$/.test(typeof param === 'string' ? param : param + '')) return true
        return 'channel should be tested by /^\\d+$/'
      }
    },
    // zzzz: {
    //   assert (param, ctx, inject) {
    //     if (isInWhiteList(ctx.api)) return true
    //     return xxxxdddd === 'wx' ? 28 : 29
    //   }
    // },
    zzz: {
      type: 'enum',
      enumRange: [28, 29]
    }
  }
}

5、总结

这里是这个小系列的终篇了,这个工具的出发点是合作的 QA 质量太差什么也保证不了,本着求诸人不如求诸己的心态写了这么个东西,在业务也落地了,收效还不错。

一年多没写小作文了,但是草稿箱里都是干货。

这一年多收获颇丰,不过不好写出来了,总之是向前又迈进了一大步。

新坑早就开好了,终于做好准备向 webpack 源码出发了,新的一年,预计明年一整年的目标就是把 webpack5 源码小作文写完,希望各位看官老爷们共勉啊!

预祝各位看官老爷早日成为尊贵的奥迪、奔驰、宝马、保时捷、布加迪车主!

(谢谢,我是布加迪的兄弟不加糖

(PS: 我去看了我心心念念的奥迪A6L 45TFSI,现在下行周期,不敢花钱,没舍得40W😂,有哪位提了,也替我高兴一下,爱你们!)

2023.01.11

于北京·海淀

相关推荐
qbbmnnnnnn7 分钟前
【CSS Tricks】如何做一个粒子效果的logo
前端·css
唐家小妹8 分钟前
【flex-grow】计算 flex弹性盒子的子元素的宽度大小
前端·javascript·css·html
涔溪10 分钟前
uni-app环境搭建
前端·uni-app
安冬的码畜日常14 分钟前
【CSS in Depth 2 精译_032】5.4 Grid 网格布局的显示网格与隐式网格(上)
前端·css·css3·html5·网格布局·grid布局·css网格布局
洛千陨14 分钟前
element-plus弹窗内分页表格保留勾选项
前端·javascript·vue.js
小小199216 分钟前
elementui 单元格添加样式的两种方法
前端·javascript·elementui
完球了35 分钟前
【Day02-JS+Vue+Ajax】
javascript·vue.js·笔记·学习·ajax
前端没钱36 分钟前
若依Nodejs后台、实现90%以上接口,附体验地址、源码、拓展特色功能
前端·javascript·vue.js·node.js
爱喝水的小鼠42 分钟前
AJAX(一)HTTP协议(请求响应报文),AJAX发送请求,请求问题处理
前端·http·ajax
dgiij42 分钟前
AutoX.js向后端传输二进制数据
android·javascript·websocket·node.js·自动化