✨ 引言
无论你是开发中后台系统、前端工具平台,还是表单组件或 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 都是纯函数,天然单测友好 |
✅ 灵活组合 | 可按模块拆分校验器列表,如 refundValidators 、formStepValidators |
✅ 易扩展 | 轻松扩展异步校验(返回 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 | 函数式校验器组合 |
---|---|---|
逻辑位置 | 分散在各处 | 集中统一管理 |
可测试性 | 难以测试 | 天然纯函数 |
可维护性 | 修改易引发连锁问题 | 每条规则互不影响 |
适用范围 | 一次性小功能 | 通用可复用设计 |
总的来说,这是一种 现代前端组件化、函数式、可配置化开发思想的落地实践。它不仅能帮你写出更漂亮的代码,也能在架构、协作、迭代中带来巨大收益。