1、 前文回顾
上篇小作文我们讨论 testMethod 和 travere 方法:
-
testMethod 方法用于校验 http method 是否和符合预期。基于这种路数,我们甚至可以扩展一种校验请求头信息的功能,当然如果你有兴趣可以尝试一下~
-
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 格式的字符串,这个东西如果要校验改是个什么类型呢?
- 当个字符串校验?可以里面的 pro、e_id 也是毕传的,这个需要校验的;
- 当个数组校验?这个东东是个字符串,数组咋整?
- 写个 assert 函数先转 JSON 对象,再校验,这要是有 10 个,就要写是个 assert 函数,这也不太合理啊?
当然上面的三种方案肯定是最后一种最合理了,但是这种方案不优雅,怎么办呢?如果框架层面支持一种能力,可以在校验之前对数据进行一定的转换,然后再去校验这个不就优雅多了吗?
当然这就是预处理了,这个我们这一篇详细展开讨论!
我们的业务中有不少复杂参数都是这么传递的,所以我们想到一种预处理器机制,与 webpack 的行内 loader 很像:
- 我们使用 @ 符号表示预处理器,支持若干内建的预处理器;
- 支持多个同时使用,多个同时使用时从右向左依次调用;
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 内建预处理器
- toJSON:JSON.parse 的语法糖,尝试把参数值变成 JSON 对象;
- toString: 尝试将参数值转成字符串;
- 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
于北京·海淀