自定义校验规则在前端表单中的实战应用与优化技巧

伟伟 ☘︎ 组件 阅读 2,913
赞 16 收藏
二维码
手机扫码查看
反馈

我的写法,亲测靠谱

做表单校验这么多年,我早就放弃了用框架自带的那套“看起来很美”的规则。尤其是自定义校验,很多人一上来就写个 validator: (value) => value.length > 5,结果上线后一堆边界问题。我自己折腾过好几轮,现在基本固定了一套写法,稳定、可维护、还能复用。

自定义校验规则在前端表单中的实战应用与优化技巧

核心思路就一点:把校验逻辑和 UI 解耦,做成纯函数。这样测试方便,复用也简单。比如手机号校验,别在每个表单里都写一遍正则,抽成一个函数:

// utils/validators.js
export const isMobile = (value) => {
  if (!value) return false;
  const reg = /^1[3-9]d{9}$/;
  return reg.test(value);
};

export const validateMobile = (value) => {
  if (!value) return '手机号不能为空';
  if (!isMobile(value)) return '手机号格式不正确';
  return true;
};

然后在表单里直接用:

// 表单组件
const rules = {
  phone: [
    { validator: validateMobile, trigger: 'blur' }
  ]
};

这种写法的好处是:校验逻辑集中管理,改一次全站生效;单元测试也好写,直接测 validateMobile('13800138000') 就行;而且返回值统一(true 或错误信息),不容易出错。

这几种错误写法,别再踩坑了

我见过太多人在这几个地方栽跟头,自己也踩过,列出来给大家避雷。

  • 在 validator 里直接操作 DOM 或调用 this:比如有人写 validator: function(value) { return this.someMethod(value) }。这在 Vue 2 的 Options API 里可能“碰巧”能跑,但一旦换到 Composition API 或 React 里就炸了。而且这种写法根本没法单元测试。记住:validator 必须是纯函数,不要依赖上下文。
  • 异步校验不处理 loading 状态:比如用户名是否重复,要发请求。很多人只写了 async validator(value) { const res = await api.check(value); return res.available; },但没考虑用户快速切换输入框时,前一个请求还没回来,后一个又发了,导致状态错乱。正确的做法是加个防抖,或者至少在校验开始时清掉上一次的 loading 状态。
  • 返回值混乱:有的 validator 返回 false,有的返回字符串,有的抛异常。框架(比如 Element Plus)通常要求返回 true / false / Promise / 错误信息字符串。混用会导致 UI 不显示错误,或者控制台报错。我建议统一:同步校验返回 true 或错误字符串;异步校验返回 Promise<true | string>

举个反面例子:

// ❌ 千万别这么写
const badValidator = (rule, value, callback) => {
  if (value === 'admin') {
    callback(new Error('不能使用 admin'));
  } else {
    callback(); // 这里应该传 null 或不传,但有些框架要求必须传
  }
};

这种基于 callback 的写法不仅啰嗦,还容易漏掉 callback() 导致校验卡住。现代框架基本都支持 Promise 或直接返回值,没必要用 callback。

实际项目中的坑

上周刚修了一个线上 bug:用户注册时,密码强度校验在 Safari 上失效。查了半天,发现是因为校验函数里用了 Array.prototype.includes,但没 polyfill,而项目最低兼容到 iOS 9。所以自定义校验函数里的 JS 语法,一定要考虑兼容性。现在我都会在 validator 里只用最基础的语法,或者确保构建工具已经处理了。

另一个坑是异步校验的触发时机。比如邮箱唯一性校验,如果设成 trigger: 'change',用户每敲一个字就发请求,服务器压力大,体验也差。我一般设成 blur,或者配合防抖:

import { debounce } from 'lodash';

const debouncedCheckEmail = debounce(async (email, callback) => {
  try {
    const res = await fetch(https://jztheme.com/api/check-email?email=${email});
    const data = await res.json();
    if (data.exists) {
      callback(new Error('邮箱已被注册'));
    } else {
      callback();
    }
  } catch (err) {
    callback(new Error('校验失败,请稍后重试'));
  }
}, 500);

const validateEmailUnique = (rule, value, callback) => {
  if (!value) return callback();
  debouncedCheckEmail(value, callback);
};

不过注意,防抖的 validator 要手动管理 debounce 实例的销毁,否则可能内存泄漏。在 React 里用 useRef 存 debounce 函数,在 Vue 里用 onBeforeUnmount 清理。

还有个小细节:校验函数里别写 console.log。听起来很蠢,但真有人在生产环境留了 log,导致敏感信息泄露。现在我会在 validator 里加个环境判断,或者干脆不用 log,用 Sentry 捕获异常。

复杂场景怎么搞?

有时候校验不是单字段的,比如“结束时间必须大于开始时间”。这时候别硬塞进单个字段的 validator,而是用表单级别的校验。以 Element Plus 为例:

const formRef = ref();

const validateForm = async () => {
  try {
    await formRef.value.validate();
    // 再校验跨字段逻辑
    if (form.endTime <= form.startTime) {
      // 手动设置错误
      formRef.value.validateField('endTime', (err) => {
        if (!err) {
          // 如果 endTime 本身格式正确,但逻辑不对,需要手动报错
          formRef.value.setFields([
            { prop: 'endTime', message: '结束时间必须晚于开始时间' }
          ]);
        }
      });
      return false;
    }
    return true;
  } catch {
    return false;
  }
};

虽然有点麻烦,但比把 startTime 作为参数传给 endTime 的 validator 要清晰。后者会导致 validator 签名膨胀,而且难以测试。

另外,对于特别复杂的业务规则(比如保险投保的几十条校验),我会把校验规则配置化。比如:

const insuranceRules = [
  { field: 'age', min: 18, max: 65, message: '年龄需在18-65岁' },
  { field: 'income', min: 10000, message: '年收入不低于1万元' }
];

// 然后写个通用校验器
const validateByRules = (formData, rules) => {
  const errors = {};
  for (const rule of rules) {
    const value = formData[rule.field];
    if (rule.min !== undefined && value < rule.min) {
      errors[rule.field] = rule.message;
    }
    // ... 其他规则
  }
  return errors;
};

这样产品经理改规则,前端改个 JSON 就行,不用动代码。当然,前提是规则不太动态,否则还是得写死逻辑。

最后的小建议

自定义校验别追求一步到位。我一般先写最简版本,上线后根据用户反馈再迭代。比如一开始只校验非空,后面再加格式、再加异步唯一性。毕竟很多校验规则业务自己都说不清,边做边调更实际。

另外,校验文案要友好。别写“格式错误”,写“请输入11位手机号”。用户不知道什么是“格式”,但知道要输11位数字。

以上是我个人对自定义校验的完整讲解,有更优的实现方式欢迎评论区交流。这个技巧的拓展用法还有很多,后续会继续分享这类博客。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论