这篇文章大体梳理积梦采用的表单方案作的一些尝试和回顾.
目前从用的方案是 Meson Form, 名字大体来源于 immer json:
https://github.com/jimengio/m...
目前 Meson Form 形态逐渐开始稳定了, 方案上基本仍是可靠的.
过程中的考虑有一些曲折, 大体作一些梳理.html
早先咱们的方案当中其实沿用了一套书写较为简便的方案, 称为 KForm
.
看过 Meson Form 例子的话, 跟 KForm 的写法已经比较类似了,
主要看 items
, 每一个元素定义了表单当中的一项, 这个表单有 3 项:前端
let { dataSource, onSubmitData, formRef } = this.props; let items: IFormItem[] = [ { id: "name", label: lang.lblName, rules: [{ required: true }] }, { id: "code", label: lang.lblSerialNumber, rules: [{ required: true }] }, { id: "description", label: lang.lblDescription }, ]; return <KForm ref={formRef} items={items} data={dataSource} layout={layout} onSubmitData={onSubmitData} />;
粗看这个例子, 可能以为已是比较成熟的表单方案了.
不过深刻使用的话, KForm 存在两个问题,react
第一个问题是没有类型系统的良好支持, 或者说 TypeScript 的良好支持.
KForm 的内部是基于 antd 表单的, 而控件通常都有各自的属性,
KForm 当中添加属性, 须要用 property 手动写, 这个地方是丢失类型的,
这个地方的处理, 对于真实的开发调试来讲不够友好, 没有检查也没有提示,git
let items: IFormItem[] = [ { id: "userGroup", label: lang.lblUserGroup, rules: [{ required: true }], controlType: UserGroupSelectDropdown, controlPropsMapper: (controlProps) => { controlProps.plantId = plantId; // <-- 缺失类型检查 return controlProps; }, }, ];
必定程度上手动添加类型或许能够做为补充的, 可是书写相对繁琐.github
另外一个问题是可变数据, antd 的方案是基于可变数据实现的.
React 当中倾向使用不可变数据来辅助性能优化,
同时另外一方面, 不可变数据也能避免表单的对象被随意修改,
KForm 当中可变对象被传递到多处, 就引起了一些状态改变的 bug.
并且随着咱们愈来愈多使用 immer, 二者之间的不协调就愈来愈明显.typescript
另外还有个遇到的问题是 KForm 封装好之后扩展性不够.
这个就跟具体的实现有关系了, 致使不能应对一些特殊的场景.
好比自定义组件时要修改额外的字段, 就须要组件可以暴露底层操做.
但整体上感受随着遇到不一样的业务, 总以为不够用.json
为了能解决前面说的几个问题, 我基于 immer 开始寻找方案:数组
因为没有想到清晰的方案, 早先我先尝试用简单的函数来抽离复用的代码,
好比表单的渲染, 好比错误校验, 我分离出了一些经常使用的函数,
而后整理出大体一套方案, 完成了我当时遇到的几个表单,
大体的代码好比:
https://gist.github.com/cheny...性能优化
回头来看, 这套代码其实比较零碎, 表单状态被暴露在外部,
也就意味着在父组件当中须要附加上若干状态个方法用于维护校验,
渲染部分至关于只有复用布局, 可是没有作封装, 基本没有限制.
这个写法好处就是没有什么限制, 各类场景要用基本都是能够用上的,
坏处就是.. 代码会比较啰嗦, 错误须要本身绑定到对应位置, 其实挺烦.antd
Immer Form 的写法原本是打算逐步简化的, 可是结果用了挺久的,
一方面是没有找到好的入口, 另外一方面确实业务也消耗着主要的经历,
我跟同事都是有点想念以前老代码当中用的 antd 的, 以及前面这个写法.
我以为用 JSON 结构配置表单是正确的方向, 由于这样描述比较少冗余.
并且以前的 KForm 其实也证实对于简单的业务, JSON 形态彻底够用的.
因此很天然会想到作一个组件, 将 JSON 渲染到 Form, 以及生成简单的逻辑,
以及对于特殊的场景, 提供自定义渲染或者其余配置, 用来特殊处理.
可是中间有个问题, 即使是 JSON 我依然须要保证自动补全能用,
不过, 一个巨大的 JSON 整个在 VS Code 当中错误提示, 很是感人.
好比这样一个结构,
let formItems = [ { type: EMesonFieldType.Input, name: "name", label: "名字", }, { type: EMesonFieldType.Input, name: "name", label: "名字禁用", disabled: true, }, ]
我须要在 name
或者 label
位置填写错误时可以被自动提示,
同事, 对于 disabled
, 我输入 dis
能看到对应的补全.
我大体知道 VS Code 有相似的功能的, 在我描述了 type
的前提下, 相似于,
https://basarat.gitbooks.io/t...
实际使用当中反而预测坑了, 我试了一下, 发现错误提示老是在整个 JSON 数组上.
后来在朋友的帮助下, 终于明确了在变量上直接加类型约束, 能够规避问题, 也就是,
let formItems: IMesonFieldItem[] = [ { type: EMesonFieldType.Input, name: "name", label: "名字", }, { type: EMesonFieldType.Input, name: "name", label: "名字禁用", disabled: true, }, ];
其中 IMesonFieldItem
是借助 Union 关联在一块儿的多个 interface.
这样写以后, 错误提示和自动补全, 都显得相对正常了.
基于上面这种 JSON 的格式, 以及一些字段, 我编写了一个简单的组件,
这样, 就是一个简单的 Meson Form 的结构了.
这是一个例子 http://fe.jimu.io/meson-form/
let formItems: IMesonFieldItem[] = [ { type: EMesonFieldType.Input, name: "name", label: "名字", }, { type: EMesonFieldType.Input, name: "name", label: "名字禁用", disabled: true, }, ]; return ( <div className={cx(row, styleContainer)}> <MesonForm initialValue={form} items={formItems} onSubmit={(form) => { setForm(form); }} /> <div> <SourceLink fileName={"basic.tsx"} /> <DataPreview data={form} /> </div> </div> );
相似地, 对于自定义渲染的需求, 直接用上一个 render 函数插入代码,
Demo http://fe.jimu.io/meson-form/...
let formItems: IMesonFieldItem[] = [ { type: EMesonFieldType.Custom, name: "x", label: "自定义", render: (value, onChange, form, onCheck) => { return ( <div className={row}> <div> Custome input <Input onChange={(event) => { onChange(event.target.value); }} placeholder={"Custom field"} onBlur={() => { onCheck(value); }} /> </div> </div> ); }, }, ];
基于这套写法, 后面又加上了 Select
Switch
等组件和样式,
目前支持的类型比较少, 常常依赖自定义渲染, 后续还要跟随业务扩展.
实际使用当中也提出了须要更多钩子用于状态修改, 慢慢也加上了.
只能说大体知足了经常使用的需求, 加上自定义. 在原来的基础上减小了代码量.
另外一方面早期 KForm 大量的场景是跟 Modal 用在一块儿的,
因此 Meson Form 也加上了 Modal 的封装, 尝试覆盖一些经常使用的需求:
http://fe.jimu.io/meson-form/...
http://fe.jimu.io/meson-form/...
不过整体上说业务每每是多变的, 一个 Form 组件的形态总归是不够的.
好比说我会遇到场景, 没有文字标签, 标错的样式也有区别,
这种场景, 好比就是登陆框了, 一般就不是用 Form 的样式去作的.
可是又比较明确, 它仍是 Form, 有校验, 只是界面和结构有区别.
基于这一点, 咱们再进一步想, 前面的 Form 的封装实际上是有点仓促的,
渲染部分的组件, 实际当中时会有多种可能的, 而不仅仅是一种渲染,
对于 Form 来讲, 更加稳定真实的实际上是数据和校验的部分,
这部分能够超脱 UI 的形态, 可是表单本身基本都会有在表单项还有校验,
那么, 我就想起来用 Hooks 能够分离出表单的状态部分,
这部分包含表单的状态, 校验结构, 还有一些操做,
这部分代码能够超越表单组件自己, 被用到特殊的表单的场景, 核心的 API 好比:
let { formAny, errors, onCheckSubmit, checkItem, updateItem, forcelyResetForm } = useMesonCore({ initialValue: submittedForm, items: formItems, onSubmit: onSubmit, });
这里能获取 form
errors
, 这是渲染表单必备的数据,
而后也暴露出来其余一些用于校验和更新表单数据的函数, 甚至于重置表单的数据,
这样就获得一个例子, 能够沿用 Meson Core, 然而本身定义界面如何渲染,
http://fe.jimu.io/meson-form/...
<div className={styleFormArea}> {formItems.map((item) => { switch (item.type) { case EMesonFieldType.Input: return ( <div className={column} key={item.name}> <input value={form[item.name] || ""} type={item.inputType} placeholder={item.placeholder} onChange={(event) => { let text = event.target.value; updateItem(text, item); }} onBlur={() => { checkItem(item); }} /> {errors[item.name] != null ? <div className={styleError}>{errors[item.name]}</div> : null} </div> ); } })} <div> <button onClick={onCheckSubmit}>Submit</button> </div> </div>
基于这个思路, 当咱们须要一个横向布局的表单的时候, 就能够复用了.
核心的规则和校验逻辑是能够复用的, 渲染部分彻底用不一样的实现,
http://fe.jimu.io/meson-form/...
因此 Meson Form 提供的 API实际上提供了两个不一样的层次,
直接用 Meson Form 能够快速生成简单的 Form, 或者 Core 用于定制.
固然对于业务来讲, 场景多是无穷无尽的, 前面的方案依然未必足够.
同事在使用 Meson Form 时候, 须要用到自有定义的 Footer,
这必定上跟 Meson Form 最初设定的数据流有冲突了,
因而他用 useImperativeHandle
又加上了一层封装,
目的就是为了能把一些事件抛出, 在外部找到地方去触发, 而不收到设计的限制.
另外使用当中发现校验规则不断增多, 逐渐开始有一些明显的重复,
这些规则按理说经过高阶函数仍是能够进一步进行抽象,
或者不用高阶函数, 单纯用 JSON 定义规则的话, 也可以表达.
因此这部分的抽象和简化后面依然须要再补充.
按照 Meson Form 最初设想的, JSON 的格式本来极为通用,
社区有别人的例子, 用 JSON 定义表单的格式, 而后前端直接渲染,
这样若是还能在中台把表单抽象称为服务的话, 还能分担前端的工做量.
即使不能替代前端开发表单, 若是说能在必定程度上生成代码, 也是有效果的.
因为 toB 的属性自己就具有大量的表单, 这方面会有不小的需求.
总之 Meson Form 仍是须要继续扩充很完善, 用来应对更多业务场景.
其余关于积梦前端的模块和工具能够查看咱们的 GitHub 主页 https://github.com/jimengio .
目前团队正在扩充, 招聘文档见 GitHub 仓库 https://github.com/jimengio/h... .