React 和 Vue 都离不开的表单验证库 async-validator 之策略模式的应用 (中)

实现错误信息收集

现在我们希望通过以下方式对验证策略进行调用:

复制代码
validator.validate({ username: username.value }, (errors) => {
    if (errors.length) {
       alert(errors[0])
    }
})

我们希望通过 validate 的函数的第二个参数进行传递一个回调函数,回调函数的参数 errors 就是验证结果的错误信息。

接着我们对 validate 函数进行如下修改:

我们原来是执行 validate 函数之后返回一个布尔值进行判断是否验证成功,现在我们通过执行一个回调函数,并把收集到的验证错误信息传递回去。

接着我们对验证策略对象中的策略函数进行相应的修改:

复制代码
const rules = {
  username: {
    validator(rule, value, cb, source) {
      if (
        Object.prototype.hasOwnProperty.call(source, rule.field) &&
        (value === "" || value === undefined || value === null)
      ) {
        cb("请输入用户名");
      } else {
        cb();
      }
    },
  },
  password: [
    {
      validator(rule, value, cb, source) {
        if (
          Object.prototype.hasOwnProperty.call(source, rule.field) &&
          (value === "" || value === undefined || value === null)
        ) {
          cb("请输入密码");
        } else {
          cb();
        }
      },
    },
    {
      validator(rule, value, cb, source) {
        if (
          Object.prototype.hasOwnProperty.call(source, rule.field) &&
          (value.length < 6 || value.length > 18)
        ) {
          cb("密码长度必须大于6位小于18位");
        } else {
          cb();
        }
      },
    },
  ],
};

这样我们就实现了对验证错误信息的收集:

实现异步验证

因为有可能我们需要在一些验证中进行请求后端进行校验,这样一来我们的校验需要实现异步验证了。这其实就相当于我们有很多 HTTP 的请求,我们需要把每个 HTTP 的请求结果都收集起来进行返回。

那么结合我们的验证业务需求,我们是先循环需要验证的字段策略,再循环字段中的所有规则数组。我们需要等待所有的验证都完成后才能将结果返回,我们可以使用 Promise 进行实现异步处理,我们这里先可以使用计算器的方式进行判断是否已经完成所有的验证。

修改后的 validate 函数代码如下:

复制代码
  // 调用策略
  validate(source_, callback) {
    const source = source_;
    const errors = [];
    // 最终保存验证数据的集合
    const series = {};
    const keys = Object.keys(this.rules);
    keys.forEach((z) => {
      // 字段中验证规则数组
      const arr = this.rules[z];
      // 对应的字段值
      const value = source[z];
      arr.forEach((r) => {
        const rule = r;
        // 在规则中添加对应的字段记录
        rule.field = z;
        series[z] = series[z] || [];
        // 为每个验证策略配置对应的上下文内容,从而可以获取验证的规则,验证字段的值
        series[z].push({
          rule,
          value,
          source, // 添加需要校验的数据源
        });
      });
    });

    return new Promise((resolve, reject) => {
      const objArrKeys = Object.keys(series);
      const objArrLength = objArrKeys.length;
      // 需要验证的字段总数
      let total = 0;
      // 遍历执行验证每一个字段策略
      objArrKeys.forEach((key) => {
        const arr = series[key];
        // 每个字段需要验证的策略总数
        let arrTotal = 0;
        // 遍历字段策略中的策略
        arr.forEach((a) => {
          const rule = a.rule;
          function cb(e = []) {
            arrTotal++;
            if (arrTotal === arr.length) {
              // 等待每个字段策略中策略全部验证完毕再计算下一个字段的验证
              total++;
            }
            const errorList = Array.isArray(e) ? e : [e];
            errors.push(...errorList);
            // 当 total === objArrLength 的时候就是字段策略循环验证完毕的时候
            if (total === objArrLength) {
              // 同时执行回调函数,兼容不同的写法需求
              callback && callback(errors);
              // 如果存在错误则 reject 错误信息,否则就 resolve 表示成功
              errors.length ? reject(errors) : resolve();
            }
          }
          // 执行校验策略函数的时候把对应的规则和需要校验的数据源也传递过去
          rule.validator(rule, a.value, cb, a.source);
        });
      });
    });
  }

我们先返回一个 Promise,然后在 Promise 中进行循环执行各个字段中的规则函数,每执行一个规则策略函数 rule.validator() 的时候,就相当于进行了一次 HTTP 请求,请求的结果就是验证的返回信息。当全部字段验证完毕的时候,我们再去判断是否存在错误验证信息,没有则执行 resolve 方法代表验证通过,否则执行 reject 方法,并把错误信息也进行返回,代表验证失败。

我们知道同时发出很多 HTTP 请求的时候,HTTP 的响应是有先后顺序的,我们怎么知道所有的响应都已经完毕了呢?我们在上文已经提到了使用计算器的方式进行判断,具体操作如下。

我们将字段验证进度的计算变量设置为 total ,初始值为 0 ,当完成一个字段的验证 total 的值就增加 1 。当 total 的值等于需要验证的字段数量时,则把验证的结果返回。

我们把字段中策略验证进度的计算变量设置为 arrTotal ,初始值为 0 ,当完成一个字段策略的验证 arrTotal 的值就增加 1 。当 arrTotal 的值等于当前验证字段的策略数量时,则代表完成了一个字段的验证,所以需要把 total 的值增加 1

由于修改后的 validate 函数返回的是一个 Promise,所以我们可以进行以下方式进行调用 validate 函数了。

复制代码
const validator = new Schema(rules);
const handleSubmit = () => {
  validator
    .validate({ username: username.value, password: password.value })
    .then(() => {
      alert("提交成功");
    })
    .catch((errors) => {
      console.error(errors);
      alert("提交失败");
    });
};

而这种调用方式正是我们 Element Plus 中使用的方式

重构异步验证

我们上一小节中实现的异步验证的那一坨代码,其实职责是很不清晰的,也不利于后续的维护,所以我们需要对它进行重构。软件应该是"自描述"的,代码除了给机器看之外,也要给人看。我们希望写的代码更易读,让代码可以更好地表达自己的意图。

我们上一小节中实现异步验证的那一坨代码中,首先是职责不清晰 ,其次是变量 errorstotalarrTotal 在此模块中相当于是全局变量,在底下的模块中任何角落都可以对它们进行随意修改,整个代码结构显得非常松散。

提炼函数

首先职责不清,我们可以通过提炼函数,通过函数名称来知道我们的程序的业务结构,而提炼函数这个方法是《重构》这本书中介绍的一种代码重构手段 。我们把实现异步的代码进行提取,封装成一个叫 asyncMap 的函数,表明这是一个处理异步业务逻辑的函数。修改后的代码如下:

复制代码
/**
 * 异步验证函数
 * @param objArr series
 * @param callback 回调函数
 */
function asyncMap(objArr, callback) {
  const errors = [];
  const objArrKeys = Object.keys(objArr);
  const objArrLength = objArrKeys.length;
  // 需要验证的字段总数
  let total = 0;
  return new Promise((resolve, reject) => {
    // 遍历执行验证每一个字段策略
    objArrKeys.forEach((key) => {
      const arr = objArr[key];
      // 每个字段需要验证的策略总数
      let arrTatal = 0;
      // 遍历字段策略中的策略
      arr.forEach((a) => {
        const rule = a.rule;
        function cb(e = []) {
          arrTatal++;
          if (arrTatal === arr.length) {
            // 等待每个字段策略中策略全部验证完毕再计算下一个字段的验证
            total++;
          }
          const errorList = Array.isArray(e) ? e : [e];
          errors.push(...errorList);
          // 当 total === objArrLength 的时候就是字段策略循环验证完毕的时候
          if (total === objArrLength) {
            // 同时执行回调函数,兼容不同的写法需求
            callback && callback(errors);
            // 如果存在错误则 reject 错误信息,否则就 resolve 表示成功
            errors.length ? reject(errors) : resolve();
          }
        }
        // 执行校验策略函数的时候把对应的规则和需要校验的数据源也传递过去
        rule.validator(rule, a.value, cb, a.source);
      });
    });
  });
}

这一次重构,我们原来 errors 的变量的作用域是整个 validate 函数的作用域的缩小到了只在 asyncMap 函数内了。此外 validate 函数的结构也变得瘦小了,结构也更清晰了。

重构后的 validate 函数:

复制代码
  // 调用策略
  validate(source_, callback) {
    const source = source_;
    // 最终保存验证数据的集合
    const series = {};
    const keys = Object.keys(this.rules);
    keys.forEach((z) => {
      // 字段中验证规则数组
      const arr = this.rules[z];
      // 对应的字段值
      const value = source[z];
      arr.forEach((r) => {
        const rule = r;
        // 在规则中添加对应的字段记录
        rule.field = z;
        series[z] = series[z] || [];
        // 为每个验证策略配置对应的上下文内容,从而可以获取验证的规则,验证字段的值
        series[z].push({
          rule,
          value,
          source, // 添加需要校验的数据源
        });
      });
    });
    return asyncMap(series, callback);
  }

这时大家肯定发现按照我们上面说的重构方法与逻辑,我们的 asyncMap 函数还是职责不够清晰,所以我们继续对 asyncMap 函数进行重构。

首先我们通过常规的重构手法,提炼函数,让修改看得见,具体就是把对字段验证进度和字段策略验证进度都分别进行封装成不同的函数。

复制代码
function asyncMap(objArr, callback) {
  const errors = [];
  const objArrKeys = Object.keys(objArr);
  const objArrLength = objArrKeys.length;
  // 需要验证的字段总数
  let total = 0;
  return new Promise((resolve, reject) => {
    // 计算字段的验证进度,同时如果字段验证完毕则把相关结果返回
    const next = (error) => {
      errors.push(...error);
      // 等待每个字段策略中策略全部验证完毕再计算下一个字段的验证
      total++;
      // 当 total === objArrLength 的时候就是字段策略循环验证完毕的时候
      if (total === objArrLength) {
        // 同时执行回调函数,兼容不同的写法需求
        callback && callback(errors);
        // 如果存在错误则 reject 错误信息,否则就 resolve 表示成功
        errors.length ? reject(errors) : resolve();
      }
    };
    // 遍历执行验证每一个字段策略
    objArrKeys.forEach((key) => {
      const arr = objArr[key];
      // 每个字段需要验证的策略总数
      let arrTatal = 0;
      const results = [];
      // 计算字段策略的验证进度
      const count = (error) => {
        results.push(...(error || []));
        arrTatal++;
        // 等待每个字段策略中策略全部验证完毕再计算下一个字段的验证
        if (arrTatal === arr.length) {
          next(results);
        }
      };
      // 遍历字段策略中的策略
      arr.forEach((a) => {
        const rule = a.rule;
        function cb(e = []) {
          const errorList = Array.isArray(e) ? e : [e];
          count(errorList);
        }
        // 执行校验策略函数的时候把对应的规则和需要校验的数据源也传递过去
        rule.validator(rule, a.value, cb, a.source);
      });
    });
  });
}

现在我们通过提炼函数把对字段验证进度和字段策略验证进度都分别进行封装成不同的函数,这样我们的代码结构也进一步得到了优化。接下我们还继续进一步优化。

从业务角度来说,我们有并行校验规则 ,也就是我们目前的实现的代码,等待所有的规则都校验完成后才进行返回结果,但我们还有串行校验规则,就是有错就中断后面的规则校验,马上返回结果。

我们主要遍历循环并执行字段策略函数,第一次遍历的是字段策略,第二次遍历的是字段中的规则数组,那么从单一职责上来说我们需要让一个函数做尽可能少的事情。再者从重构手法上来说,我们需要把一些全局变量迁移封装到一个类或模块中,只允许模块内的代码使用它,从而尽量控制其作用域。

我们先把我们目前实现的代码进行封装成一个并行校验规则的函数。

复制代码
/**
 * 并行校验规则
 * @param arr 字段策略数组
 * @param callback 计算字段验证进度函数回调
 */
function asyncParallelArray(arr, callback) {
  // 每个字段需要验证的策略总数
  let tatal = 0;
  const results = [];
  // 计算字段策略的验证进度
  const count = (error) => {
    results.push(...(error || []));
    tatal++;
    // 等待每个字段策略中策略全部验证完毕再计算下一个字段的验证
    if (tatal === arr.length) {
      callback(results);
    }
  };
  // 遍历字段策略中的策略
  arr.forEach((a) => {
    const rule = a.rule;
    function cb(e = []) {
      const errorList = Array.isArray(e) ? e : [e];
      count(errorList);
    }
    // 执行校验策略函数的时候把对应的规则和需要校验的数据源也传递过去
    rule.validator(rule, a.value, cb, a.source);
  });
}

异步验证函数 asyncMap 中的调用 asyncParallelArray 函数:

此次重构完之后异步验证函数 asyncMap 的职责也变得非常单一了,就只是计算字段的验证进度然后返回对应的结果。

继续优化并行校验规则 函数 asyncParallelArray。asyncParallelArray 函数所做的事情就是遍历的是字段中的策略并执行规则验证函数,我们上面提到我们还有一个顺序校验规则 ,可以预想到的是在这两个函数中我们都需要执行字段中的策略验证函数。那么这两块是重复的代码,所以我们需要把它进行提炼函数以达到复用的目的。

提炼验证每一个规则的函数

复制代码
// 验证每一个规则的函数
function sigleValidator(data, doIt) {
  const rule = data.rule;
  function cb(e = []) {
    const errorList = Array.isArray(e) ? e : [e];
    doIt(errorList);
  }
  // 执行校验策略函数的时候把对应的规则和需要校验的数据源也传递过去
  rule.validator(rule, data.value, cb, data.source);
}
复制代码

值得注意的是,sigleValidator 函数我们并没有将它和 asyncMap 函数和 asyncParallelArray 函数那么放到外面,而是把它放在 validate 函数中,之后当成一个参数进行传递,最后在 asyncParallelArray 函数中进行调用。

在 asyncMap 函数中当参数传递。

最终在 asyncParallelArray 函数进行调用。

把 sigleValidator 函数放在 validate 函数中当成参数传递的好处是,sigleValidator 函数可以访问到 validate 函数中的变量,这样可以达到减少参数传递的目的。这一巧妙设计,是不是很让人叹为观止呢?这就是闭包函数妙用,也是 JavaScript 这门语言的动态特性,函数是一等公民。

相关推荐
郑州光合科技余经理1 小时前
从零到一:构建UberEats式海外版外卖系统
java·开发语言·前端·javascript·架构·uni-app·php
强子感冒了2 小时前
JavaWeb学习笔记:动静态Web、URL、HTTP
前端·笔记·学习
阿珊和她的猫2 小时前
Session 与 Cookie 的对比:原理、使用场景与最佳实践
前端·javascript·vue.js
Fantasy丶夜雨笙歌2 小时前
Web 服务基石 Nginx
运维·前端·nginx
敲代码的小吉米2 小时前
Element Plus 表格中的复制功能使用指南
前端·javascript·elementui
Purgatory0012 小时前
CSS 访问服务器
服务器·前端·css
昊坤说不出的梦2 小时前
梳理 Spring Boot Web 开发的几个概念
前端·spring boot·后端
We་ct2 小时前
LeetCode 103. 二叉树的锯齿形层序遍历:解题思路+代码详解
前端·算法·leetcode·typescript·广度优先