前端批量校验还能这么写?函数式校验器组合太香了!

✨ 引言

无论你是开发中后台系统、前端工具平台,还是表单组件或 AI 编辑器,都会遇到这样的问题:

"用户点击一个按钮前,我需要判断一堆条件,它能不能执行?"

一开始你可能写几个简单的 if 就搞定了,但很快你会发现:

  • 条件越来越多,if 越来越嵌套
  • 有些判断逻辑在多个地方重复出现
  • 改一个规则,影响好几处
  • 测试困难、协作混乱、维护如炼狱

于是你开始思考:

💭有没有一种方式,能优雅组织这些"操作前校验"?

💭能集中定义、方便管理、还能像乐高一样自由组合?

今天就带你看看我在项目中实践的一套思路 ------
函数式校验器组合(Validator Pattern) ,用数组 + 函数的方式写判断逻辑,干净、灵活、可复用,再也不写 if 地狱!

在开发中你一定写过类似的判断:

js 复制代码
if (!selected.length) return message.warning('请至少选一条数据');
if (hasInvalidState(selected)) return message.warning('当前状态不支持操作');
// ...

从表格批量操作,到表单提交前置条件,再到组件交互逻辑判断,前端的"操作前校验"无处不在。而一旦条件多了,这些判断就很容易变成 if 地狱:代码臃肿、逻辑混乱、难以复用和维护。

💡 为什么我们需要"函数式校验器"?

你可能会遇到这些痛点:

  • 多条件判断逻辑散落在各个函数、组件、事件中
  • 有些逻辑重复但不好复用
  • 业务经常变更,每次改一个逻辑都怕动到其他判断
  • 难以写单测,因为函数里混着判断与 UI 逻辑

👉 函数式校验器的目标是:

把所有判断逻辑拆成小函数集中管理,用组合方式执行,结构清晰、易复用、可拓展。

🛠️ 实现方式

1️⃣ 定义 validator 结构

js 复制代码
type Validator = {
  check: (ctx: any) => boolean; // 不满足条件返回 true
  message: string;              // 不通过时的提示信息
};

2️⃣ 编写校验器数组

以一个批量操作场景为例:

js 复制代码
const validators: Validator[] = [
  {
    check: (ctx) => ctx.rows.length === 0,
    message: '请至少选择一条数据!',
  },
  {
    check: (ctx) => !ctx.canOperate,
    message: '当前数据不支持操作',
  },
  {
    check: (ctx) => ctx.type === 'rollback' && ctx.rows.some(r => r.executed),
    message: '已执行的数据不支持回退',
  },
  {
    check: (ctx) => ctx.type === 'refund' && ctx.rows.some(r => !r.paid),
    message: '未付款数据不支持退款',
  },
];

3️⃣ 校验执行逻辑

js 复制代码
function validate(ctx: any): string | null {
  for (const v of validators) {
    if (v.check(ctx)) return v.message;
  }
  return null;
}

4️⃣ 在任何地方调用它

js 复制代码
function onClick() {
  const ctx = {
    rows: selectedRows,
    type: currentAction,
    canOperate: checkPermission(),
  };

  const msg = validate(ctx);
  if (msg) return message.warning(msg);

  // 校验通过,继续执行
  executeAction();
}

🌍 哪些场景可以用?

✅ 表格操作前判断

  • 是否选择行
  • 状态是否支持操作
  • 当前操作按钮是否允许点击

✅ 表单提交前校验(不属于表单控件的逻辑)

  • 必须先选完策略再提交
  • 勾选了某个选项就必须填附加信息
  • 用户角色限制提交权限

✅ 流程引导/分步骤交互

  • 多步骤表单 next() 之前判断当前页是否满足跳转条件
  • 组件嵌套表单时拆分校验逻辑

✅ 移动端按钮防误触前判断

  • 限购校验、权限判断、本地缓存条件

✅ AI 工具 / 图形编辑器 / 上传流程

  • 检查是否加载模型 / 文件尺寸是否符合要求 / 参数是否设置完整
  • 用户是否勾选隐私条款再启动处理流程

✨ 优势一览

优点 说明
✅ 解耦 把 UI 操作逻辑与校验逻辑分离
✅ 易维护 每个校验器都是独立函数,改一条不怕影响全局
✅ 可测试 每条 validator 都是纯函数,天然单测友好
✅ 灵活组合 可按模块拆分校验器列表,如 refundValidatorsformStepValidators
✅ 易扩展 轻松扩展异步校验(返回 Promise),支持国际化提示文案等

🔍 图示:执行流程一目了然

js 复制代码
[ 用户点击按钮 ]
         ↓
[ 构造上下文 ctx ]
         ↓
[ 执行校验器数组 validate(ctx) ]
         ↓
[ 有问题?弹出提示 ]
         ↓
[ 校验通过?继续执行操作 ]

🆚 传统写法 vs 函数式校验器组合

对比维度 传统写法(if 嵌套) 函数式校验器组合
结构清晰性 判断散落在函数内部,逻辑耦合严重,难以一眼看清 所有校验集中统一管理,一眼看完所有逻辑
可维护性 每次修改都要翻开业务代码,容易改坏 校验规则独立配置、增删查改互不干扰
复用性 无法重用,复制粘贴多处出现 校验器是函数,可按需组合重用(比如退款、回退各一组)
可测试性 难测试,依赖上下文和副作用 每条 validator 都是纯函数,可直接单测
扩展性 逻辑复杂时越写越乱,难支持异步、权限、角色等逻辑 支持异步校验、权限条件、链式扩展
代码美观 if/else 嵌套、return 穿插,影响阅读体验 类似数据结构(数组对象)清晰易读
声明性思维 更偏命令式:"怎么做" 更偏声明式:"规则是什么"

🧱 传统写法:

js 复制代码
if (!rows.length) {
  return message.warning('请选择数据');
}
if (!canOperate) {
  return message.warning('不允许操作');
}
if (type === 'refund' && rows.some(row => !row.paid)) {
  return message.warning('未付款不能退款');
}
if (rows.some(row => !['待处理', '处理中'].includes(row.state))) {
  return message.warning('状态不符');
}
// ...

🧩 函数式校验器组合写法:

js 复制代码
const msg = validate({ rows, type, canOperate });
if (msg) return message.warning(msg);
// 校验通过,执行操作

🧩 进阶玩法(可拓展方向)

当前的函数式校验器组合,已经能很好地解决大多数前端"操作前校验"的痛点。但在更复杂、工程化的场景中,我们可以进一步升级它的能力,打造一个更强大的校验系统。以下是几个常见进阶方向:

1️⃣ 支持异步校验器(如判断账号是否被冻结、余额是否充足)

有些判断逻辑需要依赖异步数据,比如:

  • 判断某账号是否被冻结(需要请求服务端)
  • 检查某个资源是否还可操作(依赖后端实时状态)

这时只需让 check 支持返回 Promise<boolean>

js 复制代码
type Validator = {
  check: (ctx: any) => boolean | Promise<boolean>;
  message: string;
};

async function validateAsync(ctx: any): Promise<string | null> {
  for (const rule of validators) {
    const failed = await rule.check(ctx);
    if (failed) return rule.message;
  }
  return null;
}

2️⃣ 条件式启用 validator(动态控制规则是否生效)

有些规则并不是所有时候都生效,比如:

  • "已付款校验"只在退款操作时才需要
  • "是否已执行"只影响回退操作

我们可以引入 enable 字段作为条件开关:

js 复制代码
type Validator = {
  enable?: (ctx: any) => boolean;
  check: (ctx: any) => boolean;
  message: string;
};

function validate(ctx: any): string | null {
  for (const v of validators) {
    if (v.enable && !v.enable(ctx)) continue;
    if (v.check(ctx)) return v.message;
  }
  return null;
}

3️⃣ 支持返回多个错误信息(一次性提示用户)

默认校验器在遇到第一个不通过就终止执行。但在部分场景(如表单校验、批量导入)下,可能希望:

✅ 一次性把所有问题都提示出来,用户不用来回试错。

改造如下:

js 复制代码
function validateAll(ctx: any): string[] {
  return validators
    .filter(v => !v.enable || v.enable(ctx))
    .filter(v => v.check(ctx))
    .map(v => v.message);
}

// 使用方式
const errors = validateAll(ctx);
if (errors.length) return message.warning(errors.join(';'));

4️⃣ 封装成自定义 hook 或工具函数(提高复用)

当你的校验器在多个地方使用时,可以封装为 Hook 或工具方法:

js 复制代码
// React 示例
function useValidators(ctx, validatorList) {
  return {
    validate: () => {
      for (const v of validatorList) {
        if (v.check(ctx)) return v.message;
      }
      return null;
    },
  };
}

或者做成一个工具类:

js 复制代码
const validatorRunner = createValidator(validators);
validatorRunner.validate(ctx);

5️⃣ 拓展属性:优先级、分组、国际化提示等

为更高级用法,还可以考虑:

  • priority: 控制执行顺序
  • group: 支持多个业务模块使用不同校验分组
  • message 使用 i18n key 实现多语言提示

例如:

js 复制代码
{
  check: xxx,
  message: 'validation.refund.unpaid',
  priority: 1,
  group: 'refund',
}

配合分组执行、提示国际化,即可用于低代码平台或中大型 SaaS 项目。

🧩 6️⃣ 与表单控件校验结合,统一数据校验体系

除了通用按钮操作判断,函数式校验器还可与传统表单控件的校验规则组合使用。通过将控件的 rules 负责字段级基本验证,而将函数式校验器负责"跨字段、权限、角色、业务逻辑"等更高层次的操作前判断,能够构建一套统一、可扩展的校验体系,提升表单系统的健壮性和复用性。

💭 为什么要结合?

在实际项目中,表单控件自带的校验机制(如 rules 配置)往往只能覆盖 字段级别的规则,比如:

  • 是否必填
  • 长度限制
  • 格式校验(如邮箱、手机号)

但许多业务逻辑却无法仅靠字段规则判断,比如:

  • 提交前必须选择某项策略(依赖其他字段)
  • 用户角色不符合时禁止提交(与权限系统有关)
  • 某类产品选中后必须填写额外信息(跨字段逻辑)

这些"跨字段、跨角色、跨组件的操作前校验",就适合用我们上文介绍的"函数式校验器组合"方式来统一处理。

🔗 如何结合使用?

✅ 表单控件层:继续使用内置 rules

vue 复制代码
<Form.Item
  name="email"
  rules={[{ required: true, message: '请输入邮箱地址' }]}
>
  <Input />
</Form.Item>

✅ 表单提交时:调用 validate() 增加业务逻辑判断

js 复制代码
async function onSubmit(values) {
  const ctx = {
    ...values,
    userRole: currentUser.role,
    selectedPolicy: policyId,
    // 可加入其他依赖数据
  };

  const msg = await validate(ctx);
  if (msg) {
    return message.warning(msg);
  }

  // 校验通过
  submitForm(values);
}

✅ 校验器结构支持更复杂的规则

js 复制代码
const formValidators: Validator[] = [
  {
    check: (ctx) => ctx.selectedPolicy == null,
    message: '请先选择策略方案',
  },
  {
    check: (ctx) => ctx.productType === 'X' && !ctx.extraInfo,
    message: 'X类产品必须填写附加信息',
  },
  {
    check: (ctx) => ctx.userRole !== 'admin',
    message: '当前角色无权限提交',
  },
];

🧰 使用技巧

场景 建议做法
只校验字段格式 使用表单控件自带的 rules
校验多个字段之间逻辑 使用函数式校验器 check(ctx) 组合跨字段逻辑
与用户角色、权限相关 把角色、权限注入 ctx,做上下文判断
与后端接口有关的校验 check() 支持异步,调用接口判断
提交前统一拦截 提交按钮前统一调用 validate() 拦截

🧱 推荐结构设计

你可以将表单字段校验与操作前业务校验明确区分:

js 复制代码
// 表单字段层校验 rules(Ant Design 等)
const fieldRules = {
  email: [{ required: true, message: '请输入邮箱' }],
  password: [{ min: 6, message: '密码至少6位' }],
};

// 操作前业务校验 validator(组合逻辑)
const formValidators: Validator[] = [...];

这样可以清晰地划分"字段正确性"与"业务允许性"两个维度,有利于团队协作与维护。

💬 总结一下(进阶能力总览)

进阶能力 实现方式 应用场景
异步校验 check 支持返回 Promise<boolean> 权限判断、余额判断、接口校验等依赖后端状态的逻辑
条件激活 enable(ctx) 控制校验器是否启用 根据操作类型/用户角色动态决定某些规则是否生效
多错误返回 返回所有不通过校验的 message 列表 一次性展示多个问题,减少用户试错,提升体验
工具化 / Hook 化 封装为函数或 Hook(如 useValidator() 多页面、多组件、平台工具中共享复用校验逻辑
国际化与配置化 message 使用 i18n key 或配置项 多语言支持、SaaS 平台配置化提示,适配多租户需求
与表单控件结合 表单字段用 rules,操作前用 validate(ctx) 构建统一数据校验体系,兼顾字段格式校验与跨字段/业务/权限判断

💡 你可以根据项目复杂度,逐步引入这些能力,打造一个真正"通用型、可拓展、工程化"的前端操作校验系统。

🧠 背后的设计理念

虽然这套"函数式校验器组合"看起来只是代码结构优化,但其实背后融合了多个现代编程范式和设计理念,让它既优雅又强大:

1️⃣ 函数式编程(Functional Programming)

每个校验器都是一个纯函数 ------ 输入一致、输出一致,无副作用。所有逻辑以函数的方式组合,不依赖全局状态,天然解耦且易测试。

✅ 纯函数

✅ 函数组合

✅ 不可变数据

✅ 声明式思维

2️⃣ 责任链模式(Chain of Responsibility)

所有校验器按顺序组成一条"责任链",逐一判断是否通过。一旦某个环节失败,就立即终止执行并返回提示。

✅ 每个校验器只关注"自己该管的事"

✅ 遇错即止,自动中断

✅ 支持链式插拔、扩展灵活

3️⃣ 数据驱动设计(Data-Driven Design)

校验器不是写死在代码里的 if/else,而是用数据结构(数组对象)描述出来,可以配置、组合、重用。

✅ 结构化规则定义

✅ 更适合做配置型校验系统

✅ 可拓展为 DSL 或策略表驱动

4️⃣ 声明式编程(Declarative Programming)

你不是在命令程序"怎么一步步判断",而是在声明一组规则 ------ 哪些条件不满足时不允许继续,逻辑更直观、更贴近业务语言。

✅ 更易读,更易与产品/测试沟通

✅ 易于文档化和可视化(未来甚至可以生成 UI)

🧩 思维方式的转变

思维方式 命令式 if/else 函数式校验器组合
逻辑位置 分散在各处 集中统一管理
可测试性 难以测试 天然纯函数
可维护性 修改易引发连锁问题 每条规则互不影响
适用范围 一次性小功能 通用可复用设计

总的来说,这是一种 现代前端组件化、函数式、可配置化开发思想的落地实践。它不仅能帮你写出更漂亮的代码,也能在架构、协作、迭代中带来巨大收益。

相关推荐
知否技术3 分钟前
知道这10个npm工具包,开发效率提高好几倍!第2个大家都用过!
前端·npm
希希不嘻嘻~傻希希39 分钟前
CSS 字体与文本样式笔记
开发语言·前端·javascript·css·ecmascript
石小石Orz1 小时前
分享10个吊炸天的油猴脚本,2025最新!
前端
爷_2 小时前
Nest.js 最佳实践:异步上下文(Context)实现自动填充
前端·javascript·后端
爱上妖精的尾巴2 小时前
3-19 WPS JS宏调用工作表函数(JS 宏与工作表函数双剑合壁)学习笔记
服务器·前端·javascript·wps·js宏·jsa
草履虫建模2 小时前
Web开发全栈流程 - Spring boot +Vue 前后端分离
java·前端·vue.js·spring boot·阿里云·elementui·mybatis
—Qeyser2 小时前
让 Deepseek 写电器电费计算器(html版本)
前端·javascript·css·html·deepseek
楼台的春风2 小时前
【Linux驱动开发 ---- 2.1_深入理解 Linux 内核架构】
linux·c++·人工智能·驱动开发·嵌入式硬件·ubuntu·架构
掘金-我是哪吒3 小时前
分布式微服务系统架构第150集:JavaPlus技术文档平台日更
分布式·微服务·云原生·架构·系统架构
UI设计和前端开发从业者3 小时前
从UI前端到数字孪生:构建数据驱动的智能生态系统
前端·ui