javascript作为弱类型语言,确实有它自身的短处,js代码有时会感觉到难以阅读。所以很多开发者都会选择ts提高其可维护性和阅读性,先说一下引入ts可能会带来的问题(个人观点):
1、ts尚未普及成为前端开发者必备的语言,所以团队可能存在前端开发者对ts不熟悉,降低开发效率。
2、项目太小,引入ts反而臃肿。
3、一些第三方依赖包和ts未能结合得十分完美。
但是ts的阅读性还是相当优雅的,本文主要介绍在函数上如何模拟出ts函数,提高代码的阅读性。
功能和用法
先上用法:
import Schema from "../utils/funValidate";
/**
* params: Array 函数传入形参类型
* params: String 函数应该返回的数据类型
* params: Boolean true是生产环境也校验,默认false
**/
@Schema(["number/string", "string", "number"], 'object', false)
handleData(name, code, id) {
console.log({
name,
code,
id
});
return {name, code, id}
}
上面代码我们用了装饰器,装饰器可以增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改函数或者类的功能,有关装饰器的用法 可以看 阮一峰老师 装饰器
功能:
1、明确传入参数类型和返回类型
2、校验传入参数是否符合传入的数据类型,当数据类型未通过时,抛出错误给开发者,更快定位问题
3、支持开发模式和生产模式,默认:开发模式下校验,生产模式不校验
用法
// 基本用法
@Schema(['string'], 'string')
test(name) {
return name;
}
// 传入的参数可能是多种数据类型
@Schema(['string/number'], 'string')
test(name) {
return name + '';
}
// 传入参数没确定数据类型
@Schema('', 'string')
test(name) {
return name + '';
}
// 或者
@Schema(['', 'number'])
test(name, id) {
// 无返回值
}
// 对象用法
@Schema([{name: 'string', id: 'number'}], 'object')
test({name, id}) {
// 传入的对象中name必须是string类型,id必须是number类型
return {
name,
id
}
}
// 网络请求,检验传进来的params.code是否是string类型
@Schema([{code: "string"}], "promise")
getList(params) {
return new Promise(resolve => {
resolve([111]);
});
}
实现分析
先看一下装饰器在方法的使用,例子如下:
class Person {
@readonly
name() { return `${this.first} ${this.last}` }
}
function readonly(target, name, descriptor){
// descriptor对象原来的值如下
// {
// value: specifiedFunction,
// enumerable: false,
// configurable: true,
// writable: true
// };
descriptor.writable = false;
return descriptor;
}
readonly(Person.prototype, 'name', descriptor);
// 类似于
Object.defineProperty(Person.prototype, 'name', descriptor);
装饰器第一个参数是类的原型对象,上例是Person.prototype,装饰器的本意是要“装饰”类的实例,但是这个时候实例还没生成,所以只能去装饰原型(这不同于类的装饰,那种情况时target参数指的是类本身);第二个参数是所要装饰的属性名,第三个参数是该属性的描述对象。
另外,上面代码说明,装饰器(readonly)会修改属性的描述对象(descriptor),然后被修改的描述对象再用来定义属性。
由上述使用知道,Shema本质是一个高阶函数,配合装饰器的使用
function (...types) {
// startType 传入的参数类型
// endStartType 返回的参数类型
// devValid 生产环境是否验证
let {0: startType, 1: endType, 2: devValid = process.env.NODE_ENV === "development"} = types;
// 验证器,用于判断数据类型
const validator = {
number: {
expression: value => typeof value === "number",
},
string: {
expression: value => typeof value === "string",
},
boolean: {
expression: value => typeof value === "boolean",
},
array: {
expression: value => Object.prototype.toString.call(value) === "[object Array]",
},
object: {
expression: value => Object.prototype.toString.call(value) === "[object Object]",
},
promise: {
expression: value => Object.prototype.toString.call(value) === "[object Promise]",
}
};
return function (target, propertyKey, descriptor) {
}
}
获取函数
return function (target, propertyKey, descriptor) {
// 获取函数
let fn = target[propertyKey];
// 如果是生产环境不开启验证,不做处理,直接执行函数
if (!isProValid) {
return fn.apply(this, args);
}
}
如果是需要验证的情况下,思路:获取传进来的参数,进行遍历,是否和一开始传进来的数据类型一一匹配。如果匹配没问题,则对返回的值再进行匹配,代码如下:
return function (target, propertyKey, descriptor) {
// 获取函数
let fn = target[propertyKey];
// 如果不是开发环境,不做处理,直接执行函数
if (!devValid) {
return fn.apply(this, args);
}
// 包装函数
descriptor.value = function (...args) {
// 验证传进来的和设置数据类型类型是否匹配
const validType = (types, value, index) => {
if (!types || !types.length) {
return true;
}
const currentType = types[index];
// 处理下数据结构
let type = handleType(currentType);
// 只要有一个符合就通过,针对['string/number']形式
return type.some(item => {
return validator[item].expression(value);
});
};
// 返回参数进行验证
const endFun = () => {
// 获取函数返回的参数
const value = fn.apply(this, args);
// 如果没有传入返回类型,就直接返回value
if (!endType) {
return value;
}
// 验证传出来的参数和返回值数据类型是否匹配
const isPass = validType([endType], value, 0);
// 验证通过
if (isPass) {
return value;
} else {
// 验证不通过,抛出错误
throw `${propertyKey} 函数返回结果要求是` + endType + "类型而不是" + Object.prototype.toString.call(value) + "类型";
}
};
// 遍历传进来的参数
args.forEach((item, index) => {
let isPass = false;
// 错误参数
let errorObj = {};
// 根据对应的数据类型,如果是object,就对object进行特殊的处理
const isobj = validator.object.expression(startType[index]);
if (isobj) {
let noPass = false;
// 如果传进来的参数不是对象
if (!validator.object.expression(item)) {
// 直接不通过
noPass = true;
// 直接设置错误信息
errorObj = {
message: `${propertyKey} 函数第${index + 1}个参数传递的是 ${item} 不属于[object Object]类型`
};
} else {
// 查询是否有没通过的
noPass = Object.keys(startType[index]).some(key => {
// 如果没有设置,则跳过,跳过遍历下一位
if (!startType[index][key]) return false;
// 获取输入的输入类型
let valueType = startType[index][key];
// 获取传递进来的值
let value = item[key];
// 进行验证
const bol = validType([valueType], value, 0);
// 验证不通过
if (!bol) {
errorObj.key = key;
errorObj.valueType = valueType;
errorObj.value = value;
}
return !bol;
});
}
isPass = !noPass;
} else {
// 不是对象,直接验证
isPass = validType(startType, item, index);
}
// 如果传进来的参数验证都通过
if (isPass) {
// 最后一项验证返回参数
if (index === args.length - 1) {
result = endFun();
}
} else {
// 弹出错误信息给开发者
if (!isobj)
throw `${propertyKey} 函数第${index + 1}个参数传递的是 ${showValue(item)} ` + "不属于" + startType[index].replace("/", "或者") + "类型";
else
throw errorObj.message ? errorObj.message : `${propertyKey} 函数第${index + 1}传递个参数对象中的${errorObj.key}传递的是 ${showValue(errorObj.value)} ` + "不属于" + errorObj.valueType.replace("/", "或者") + "类型";
}
});
}
优化
上述实现了@Shema的使用,但这样,我们仍然在执行函数的时候都要判断一下生产环境环境是否验证,我们希望不影响原来的代码,我这边的思路webpack的loader打包的时候将@Schema替换掉,代码如下
// chear-shema.js
module.exports = function (src) {
const isPro = process.env.NODE_ENV === "production";
if (isPro)
src = src.replace(/@Schema.*/g, "");
return src;
};
使用该loader:
// vue.config.js
module.exports = {
chainWebpack: config => {
// GraphQL Loader
config.module
.rule()
.test(/\.vue$|\.js$/)
.use("clear-schema")
.loader("./src/utils/clear-schema")
.end();
}
};
贴下全部代码
export default function (...types) {
let {0: startType, 1: endType, 2: devValid = process.env.NODE_ENV === "development"} = types;
const validator = {
number: {
expression: value => typeof value === "number",
},
string: {
expression: value => typeof value === "string",
},
boolean: {
expression: value => typeof value === "boolean",
},
array: {
expression: value => Object.prototype.toString.call(value) === "[object Array]",
},
object: {
expression: value => Object.prototype.toString.call(value) === "[object Object]",
},
promise: {
expression: value => Object.prototype.toString.call(value) === "[object Promise]",
}
};
return function (target, propertyKey, descriptor) {
let fn = target[propertyKey];
//如果是生产环境,不做处理,直接执行函数
if (!devValid) {
return fn.apply(this, args);
}
descriptor.value = function (...args) {
let result = undefined;
const showValue = (value) => {
if (value && typeof value === "object") {
return JSON.stringify(value);
}
return value + "";
};
const handleType = (type) => {
if (type.includes("/")) {
type = type.split("/");
} else {
type = [type];
}
return type;
};
//验证传进来的和设置数据类型类型是否匹配
const validType = (types, value, index) => {
if (!types || !types.length) {
return true;
}
const currentType = types[index];
let type = handleType(currentType);
return type.some(item => {
return validator[item].expression(value);
});
};
const endFun = () => {
const value = fn.apply(this, args);
if (!endType) {
return value;
}
const isPass = validType([endType], value, 0);
if (isPass) {
return value;
} else {
throw `${propertyKey} 函数返回结果要求是` + endType + "类型而不是" + Object.prototype.toString.call(value) + "类型";
}
};
args.forEach((item, index) => {
let isPass = false;
let errorObj = {
key: "",
valueType: "",
value: ""
};
const isobj = validator.object.expression(startType[index]);
// 如果是要求是object
if (isobj) {
let noPass = false;
//如果传进来的参数不是object
if (!validator.object.expression(item)) {
//直接不通过
noPass = true;
//直接设置错误信息
errorObj = {
message: `${propertyKey} 函数第${index + 1}个参数传递的是 ${item} 不属于[object Object]类型`
};
} else {
//查询是否有没通过的
noPass = Object.keys(startType[index]).some(key => {
//如果没有设置,则跳过,跳过遍历下一位
if (!startType[index][key]) return false;
//获取输入的输入类型
let valueType = startType[index][key];
//获取传递进来的值
let value = item[key];
//进行验证
const bol = validType([valueType], value, 0);
if (!bol) {
errorObj.key = key;
errorObj.valueType = valueType;
errorObj.value = value;
}
return !bol;
});
}
isPass = !noPass;
} else {
//直接验证
isPass = validType(startType, item, index);
}
if (isPass) {
//最后一项调用结束函数
if (index === args.length - 1) {
result = endFun();
}
} else {
//弹出错误信息给开发者
if (!isobj)
throw `${propertyKey} 函数第${index + 1}个参数传递的是 ${showValue(item)} ` + "不属于" + startType[index].replace("/", "或者") + "类型";
else throw errorObj.message ? errorObj.message : `${propertyKey} 函数第${index + 1}传递个参数对象中的${errorObj.key}传递的是 ${showValue(errorObj.value)} ` + "不属于" + errorObj.valueType.replace("/", "或者") + "类型";
}
});
return result;
};
};
};
总结:上述配合装饰器实现了验证函数参数功能,个人感觉还是十分方便的,如有别的思路可以在评论区探讨下。