神奇代码篇(一): 一文教你js函数变成ts函数

313 阅读5分钟

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;

        };

    };
};

总结:上述配合装饰器实现了验证函数参数功能,个人感觉还是十分方便的,如有别的思路可以在评论区探讨下。

感谢大家观看,希望能点下赞,你的支持是我前进的动力