需求背景
目前开发的标注项目, 用户先配置工具,资源和预识别数据生成任务, 然后进入任务开始标注作业.
遇到的问题:
- 预识别的数据格式和标注模版需要的数据结构不一致,导致模版功能无法正常使用.
- 用户标注作用过程中,因为某些原因(比如报错导致部分逻辑中止),导致数据结构错误.
- 标注作业保存的标注结果错误,任务流转的时候,可能导致的丢框问题.
每次进入标注任务和保存的时候,校验数据,能更快的定位到问题发生的位置.如果没有校验,用户没办法感知到出现里问题,有问题的数据流转下去,等发现问题的时候,排查日志就需要很大的成本.
npm 及 git 地址
check-necessary-fields - npm (npmjs.com)
mingju-90/check-necessary-fields: 校验数据类型(只校验定义的字段类型) (github.com)
实现过程
递归遍历对象,对比需要校验的数据
刚开始的时候想通过遍历对象,判断期望的数据类型和需要校验的数据,可是如果需要校验联合类型的时候,就会有问题产生。
const line = {
type: 'line',
coordinates: [['number']]
}
const point = {
type: 'point',
coordinates: ['number']
}
const geometry // 为 line 或者 point 的时候, 就没办法校验了
将传入的数据(需要根据一定的规则定义)进行转化成更方便操作的格式,然后进行校验
interface checkData {
type: string | string[], // union 代表当前类型为联合类型,只要 value 中有一个类型符合就为true; object 代表当前类型为对象,需要 value 中每个字段都符合才为 true; 字符串数组代表当前类型为基础数据类型或者为指定字符串,只要符合其中一个为 true; array 代表当前为数组,需要每个元素都符合 value 类型;
key: string, // 表示需要校验对象的 key
value: undefined | checkData | checkData[] // undefined 表示当前数组没有制定元素类型;checkData 为数组元素的类型; checkData[] 为对象字段类型
}
遇到的问题,如果定义的类型是递归类型,比如,小明是 People 的实例,他有个属性 friends, 为 People[] 类型,这个时候转换为校验类型的时候,就会报错,所以需要记录已经转换过的数据。
这种类型的数据
class People {
name: string
age: number
friends: People[]
constructor({name, age}) {
this.name = name
this.age = age
this.friends = []
}
}
这个时候, people已经转换过,所以转换friends的时候,就需要终止递归.
const People = {
name: 'string',
age: 'number'
}
People.friends = [People]
将数据类型转换为方便校验的对象结构,为了处理递归类型,需要保存已经转换过的对象
const transformationType = (data, key = "", map = new Map()) => {
if (typeof data === "object" && map.has(data)) return map.get(data);
const result = { key, type: "" };
map.set(data, result);
const type = getType(data);
if (type === "string") {
result.type = data.split("|").map((item) => item.trim());
} else if (type === "object") {
result.type = "object";
result.value = Object.keys(data).map((key) => {
return transformationType(data[key], key, map);
});
} else if (type === "array") {
result.type = "array";
result.value = data[0]
? transformationType(data[0], "", map)
: undefined;
} else if (type === "function") {
const functionResult = data();
result.type = "union";
result.value = functionResult.map((item) => {
return transformationType(item, "", map);
});
} else {
result.type = [data];
}
return result;
};
获取数据类型,数字类型需要特别校验 NaN,Infinity
当校验数字类型的时候, 大部分时候希望校验的是正常的数据, NaN, Infinity 希望不能通过,所以细分了数字类型
- number
- NaN
- Infinity
- -Infinity
let getType = (data) =>
Object.prototype.toString
.call(data)
.match(/\[object (.*)\]/)[1]
.toLocaleLowerCase();
const numberType = (type, data) => {
if (type !== "number") return type;
if (Number.isNaN(data)) return "NaN";
if (data > Number.MAX_SAFE_INTEGER) return "Infinity";
if (data < Number.MIN_SAFE_INTEGER) return "-Infinity";
return type;
};
const afterFn = (target, after) => {
return (...args) => {
const result = target(...args);
return after(result, ...args);
};
};
getType = afterFn(getType, numberType);
校验数据的方法
const check = (checkData, data) => {
const dataType = getType(data);
// 如果是 type 是数组, 代表 data 应该是基础数据类型, 直接判断
let result = false;
if (getType(checkData.type) === "array") {
result = checkData.type.includes(dataType) || checkData.type.includes(data);
} else if (checkData.type === "union") {
result = checkData.value.some((item) => check(item, data));
} else if (checkData.type !== dataType) {
return false;
} else if (dataType === "object") {
result = checkData.value.every((item) => check(item, data[item.key]));
} else if (dataType === "array") {
result = checkData.value
? data.every((item) => check(checkData.value, item))
: true;
}
return result;
};
const checkNecessaryFields = (type, data) => {
const typeData = transformationType(type);
return check(typeData, data);
};
使用方法
/**
* 校验数据类型, 返回布尔值
* @param {*} type 期望的数据类型
* @param {*} data 需要被校验的数据
* @returns {boolean}
*/
checkNecessaryFields(type, data) // boolean
基本数据类型校验
- 字符串: 'string'。
- 数字: 'number', 'NaN', '-Infinity', 'Infinity', 日常使用中,指定数字类型,一般不希望为NaN等,所以做了更细致的类型区分。
- undefined: 'undefined'
- null: 'null'
- boolean: 'boolean'
- 联合基本数据类型: 'string | number | null', 将需要校验的数据类型通过 | 分隔。
- 指定值: () => [1, '1', false, ''], 校验数据是否是指定的某个值。
- 制定字符串: 'str | name | 123', 校验数据是否为指定的字符串,将多个字符串通过 | 分隔。
引用类型校验
- 基本类型数组: ['string'], 数组的类型为第一个元素定义的类型。
- 引用类型数组: [{name: 'string'}], [['string | number']], 第一个元素可以为对象或者数组。
- 联合类型数组: [() => [123, 'string', {name: 'string'}]], 数组中的每个元素可能是不同的类型,通过使用联合类型来定义。
用例
基本类型
checkNecessaryFields('string', 'src') // true
checkNecessaryFields('string', '') // true
checkNecessaryFields('string', '123') // true
checkNecessaryFields('number', '123') // false
checkNecessaryFields('number', 123) // true
checkNecessaryFields('number', 0) // true
checkNecessaryFields('number', NaN) // false
指定值和联合类型
const Point = {
type: 'Point',
coordinates: ['number']
}
const Line = {
type: 'Line'
coordinates: [
['number']
]
}
// 点或者线
const PointAndLine = () => [Point, Line]
const point = {
type: 'Point',
coordinates: [100, 100]
}
checkNecessaryFields(Point, point) // true
checkNecessaryFields(PointAndLine, point) // true
// 修改点坐标的结构
point.coordinates = [
[100, 100]
]
checkNecessaryFields(PointAndLine, point) // false
// 修改点的类型为line
point.type = 'Line'
// 先校验点的类型不通过,然后校验线的类型,通过
checkNecessaryFields(PointAndLine, point) // true
// 校验的值必须符合下面几个值
const list = () => [1, 2, '3', false]
checkNecessaryFields(list, 1) // true
checkNecessaryFields(list, '1') // false
checkNecessaryFields(list, '3') // true
checkNecessaryFields(list, '') // false
checkNecessaryFields(list, false) // true
复杂类型
const peopleType = {
name: 'string',
age: 'number',
}
peopleType.friends = [peopleType]
const a = {
name: 'a'
age: 11,
friends: [
{name: 'b', age: 12, friends: []}
]
}
checkNecessaryFields(peopleType, a) // true
a.friends = undefined
checkNecessaryFields(peopleType, a) // false
后期调整
- 返回错误字段得路径: 因为需要校验得数据比较复杂, 单纯得返回布尔值, 不方便找到错误得字段, 所以添加了返回错误字段路径;
- 调整使用方式: 如果接收得是一个参数, 通过柯里化, 返回一个校验对方结构得函数, 如果接收得是两个参数, 直接返回校验结果;