表单联动规则引擎
- # vue
- # rule-engine
- # form-state
表单联动规则引擎
做过后台系统的同学应该都有体会——表单联动是前端最繁琐的事情之一。选了 A 字段,B 字段要显示;B 选了某个值,C 又要切换成多选模式……几个字段下来,v-if 和 watch 堆成面条代码,改一处牵一片。
这篇文章分享一个轻量的思路:把联动逻辑从组件里抽出来,交给一个框架无关的规则引擎来驱动。
问题长什么样
假设有这样一个筛选表单:
- 选择「过滤类型」→ 根据类型显示不同字段
- 选择「操作符」(等于/大于/…)→ 切换输入模式(单值 or 多选标签)
- 每种类型对应的参数 key、单位、placeholder 都不一样
如果直接写在组件里,大致会变成这样:
// 典型的面条代码
watch(action, (val) => {
if (val === 'system') {
showSystem.value = true
showOperator.value = false
} else if (val === 'pay') {
showOperator.value = true
currentUnit.value = '元'
currentPlaceholder.value = '请输入金额'
} else if (val === 'role_level') {
showOperator.value = true
currentUnit.value = '级'
currentPlaceholder.value = '请输入角色等级'
}
// ... 更多分支
})
watch(operator, (val) => {
if (val === 'eq' || val === 'neq') {
showMultiSelect.value = true
showSingleInput.value = false
} else {
showMultiSelect.value = false
showSingleInput.value = true
}
})
每加一个字段或条件,都要钻进 watch 里加 if-else。测试的时候漏改一个分支就是 bug。
核心思路
把联动拆成三个概念:
| 概念 | 职责 | 类比 |
|---|---|---|
| 字段 (field) | 被监听的表单字段 | 事件源 |
| 规则 (rule) | 字段值变化时执行的策略函数 | 回调 / watcher |
| 动作 (action) | 策略函数派发的具体操作 | 命令 |
引擎的工作流程:
字段值变化 → 中间件检查 → 匹配规则 → 策略函数执行 → 派发动作 → 处理器执行副作用
引擎实现
下面是完整的引擎类,不到 120 行,零依赖:
/**
* 表单联动规则引擎 —— 框架无关的核心类
*
* 设计原则:
* 1. 引擎本身不依赖任何 UI 框架,所有副作用通过 onAction 注册的处理器派发
* 2. 策略函数接收 (value, formState, dispatch),通过 dispatch 派发联动动作
* 3. 同一字段可注册多条规则,按注册顺序依次执行
*/
export class FormRuleEngine {
private rules = new Map<
string,
Array<
(
value: any,
state: Record<string, any>,
dispatch: FormRuleEngine['dispatchAction'],
) => void
>
>()
private actionHandlers: Record<
string,
(targetField: string, payload: any, engine: FormRuleEngine) => void
> = {}
private middlewares: Array<
(
field: string,
value: any,
prev: any,
state: Record<string, any>,
) => boolean | void
> = []
formState: Record<string, any> = {}
/** 注册联动规则(同一字段可注册多条) */
registerRule(
field: string,
strategyFn: (
value: any,
state: Record<string, any>,
dispatch: FormRuleEngine['dispatchAction'],
) => void,
) {
const list = this.rules.get(field) || []
list.push(strategyFn)
this.rules.set(field, list)
}
/** 注册动作处理器 */
onAction(
actionType: string,
handler: (
targetField: string,
payload: any,
engine: FormRuleEngine,
) => void,
) {
this.actionHandlers[actionType] = handler
}
/** 注册中间件(在策略执行前执行,返回 false 中断) */
use(
middleware: (
field: string,
value: any,
prev: any,
state: Record<string, any>,
) => boolean | void,
) {
this.middlewares.push(middleware)
}
/** 统一的状态更新入口,内部触发联动链条 */
updateField(field: string, value: any) {
const prev = this.formState[field]
this.formState[field] = value
for (const mw of this.middlewares) {
if (mw(field, value, prev, this.formState) === false) {
return
}
}
const strategies = this.rules.get(field)
if (strategies) {
for (const fn of strategies) {
fn(value, this.formState, this.dispatchAction.bind(this))
}
}
}
/** 派发联动动作 */
dispatchAction(targetField: string, actionType: string, payload?: any) {
const handler = this.actionHandlers[actionType]
if (handler) {
handler(targetField, payload, this)
} else {
console.warn(`[RuleEngine] 未注册的动作类型: ${actionType}`)
}
}
/** 批量更新 */
batchUpdate(updates: Record<string, any>) {
for (const [field, value] of Object.entries(updates)) {
this.updateField(field, value)
}
}
/** 重置状态 */
reset() {
this.formState = {}
}
}
三个关键 API
registerRule(field, strategyFn) — 注册规则
字段值变化时,引擎会找到该字段对应的所有策略函数,依次执行。策略函数签名是 (value, state, dispatch),你可以读取全局状态,也可以通过 dispatch 派发动作。
onAction(actionType, handler) — 注册动作处理器
策略函数不直接操作 DOM 或状态,而是通过 dispatch 派发一个动作字符串。处理器负责执行真正的副作用(比如显示/隐藏字段)。这样做的好处是:引擎和 UI 框架解耦——在 Vue 里你可以操作 reactive,在 React 里你可以 setState,引擎本身不需要知道。
use(middleware) — 中间件
中间件在策略执行之前运行,返回 false 可以中断整条链。适合做日志、权限检查、或者「值没变就不触发」之类的短路逻辑。
在 Vue 组件中使用
引擎是框架无关的,但需要在框架里「接线」。以下是关键步骤:
1. 创建引擎实例,注册动作处理器
const engine = new FormRuleEngine()
// 动作:显示/隐藏字段 → 操作 Vue reactive 对象
engine.onAction('SHOW', (target: string) => {
visible[target] = true
})
engine.onAction('HIDE', (target: string) => {
visible[target] = false
})
engine.onAction('HIDE_ALL', () => {
for (const key in visible) visible[key] = false
})
// 动作:动态切换字段配置
engine.onAction('SET_FIELD', (_, config) => {
Object.assign(currentField, config)
})
2. 用声明式的方式注册规则
// 规则:action 变更 → 显隐字段
engine.registerRule('action', (val, _state, dispatch) => {
dispatch('', 'HIDE_ALL')
if (val === 'system') {
dispatch('system', 'SHOW')
return
}
const def = numericFieldDefs[val]
if (def) {
dispatch('operator', 'SHOW')
dispatch('', 'SET_FIELD', { ...def })
}
})
// 规则:operator 变更 → 切换输入模式
engine.registerRule('operator', (val, _state, dispatch) => {
dispatch('valueInput', 'HIDE')
dispatch('valueMultiSelect', 'HIDE')
const isEq = val === 'eq' || val === 'neq'
dispatch(isEq ? 'valueMultiSelect' : 'valueInput', 'SHOW')
})
对比最开始的 watch + if-else 面条代码,规则的声明是扁平的、追加式的——新增一个联动只需要加一条 registerRule,不用去改已有的逻辑。
3. 桥接 v-model 和引擎
function onActionChange() {
engine.updateField('action', filter.value.action)
}
function onOperatorChange() {
engine.updateField('operator', filter.value.parameters.operator)
}
在模板里把 @change 绑到这些桥接函数就行,引擎会自动触发规则链。
DEMO
下面是实际运行效果,选择不同的过滤类型和操作符,观察表单字段的联动变化: