ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

表单引擎的留白:把规则、依赖和渲染分开

表单引擎的留白:把规则、依赖和渲染分开

表单配置里最难维护的,常常不是字段多,而是一条规则同时承担了计算、网络请求和界面切换。后来换一套组件库,连优惠计算也跟着重写。所谓“留白”,在这里是给各层明确的责任:规则说明数据怎样变化,依赖图决定计算顺序,视图负责让人操作。

WebMCP的声明式API尝试把HTML表单描述成Agent可理解的工具。这仍是提案与试验中的能力。它让一个老问题变得更突出:表单的业务意义如果全藏在组件内部,人和Agent都只能猜。

从三行金额计算开始

假设商品总价和优惠金额都是已经确认的整数分值。总价变化,先计算优惠,再计算应付。界面输入不能直接改应付金额;应付也不应偷偷发起下一次请求。

图中A→B→C表示一条无环计算依赖。底部回路是“用户修改状态后重新计算”的应用流程,与计算图中的循环依赖是两回事。

import assert from 'node:assert/strict'; function calculate(input) { for (const key of ['subtotal', 'discount']) { if (!Number.isSafeInteger(input[key]) || input[key] < 0) throw new TypeError('amount must be non-negative integer cents'); } const appliedDiscount = Math.min(input.discount, input.subtotal); return Object.freeze({ subtotal: input.subtotal, appliedDiscount, payable: input.subtotal - appliedDiscount }); } function present(result) { return { payableText: (result.payable / 100).toFixed(2), warning: result.appliedDiscount < 100 ? '优惠未达到示例额度' : '' }; } const state = { subtotal: 12345, discount: 100 }; const result = calculate(state); assert.equal(present(result).payableText, '122.45'); assert.deepEqual(state, { subtotal: 12345, discount: 100 }); assert.equal(calculate({ subtotal: 50, discount: 100 }).payable, 0); assert.throws(() => calculate({ subtotal: NaN, discount: 0 }), TypeError); console.log('headless form rule checks passed');

规则函数不读DOM,也不依赖当前语言;展示函数把金额变成字符串。这个拆分不代表规则天然能够“100%跨端共享”:税制、舍入方式、时区、权限和服务端定价仍可能让两端职责不同。前端计算只辅助预览,最终金额应由交易后端确认。

把扩展点放在变化真正发生的地方

组件库差异放到渲染适配器;异步联动交给带取消和版本判断的控制层;业务规则保留输入输出契约。需要表达式时,使用明确的领域语言及允许的操作,不能用一个eval换取表面上的灵活。

10月16日的WebMCP命令式API文档还描述了工具的生命周期与取消。这里可以借用同一个工程习惯:作用域结束时解除注册,任务取消时停止无用工作。这个原则有用,但是否支持某个浏览器API仍需特性检测。

本例在Node v26.3.0验证了金额边界、输入不变性和展示输出;未运行组件库、浏览器表单或交易后端。它只是能单独检查的一小块规则,并不是一套已经上线的表单平台。

返回列表