DEEPEXI 大前端
常人道,一入开发深似海,技术学习无止境。在新技术层出不穷的前端开发领域,有一群身怀绝技的开发,他们在钻研前沿技术的同时,也不忘分享他们的成果,回馈社区。下面,就由小水滴带大家看一下滴普大前端对新版 el-form-renderer 表单渲染器的硬核解析吧~
DEEPEXI是滴普科技公司面向企业数字化领域打造的云原生智能平台,定位于企业数字化引擎,为企业提供数字化全栈解决方案。滴普科技致力于互联网/大数据/人工智能/物联网领先技术产品解决方案的研发和实施,是领先的企业数字化建设者。
el-form-renderer与表单
表单面临的难题
表单本质上是什么?表单用来承载业务需求的交互逻辑,表单的最终目的是提交一些特定格式的数据。
但在日常工作中,公司的业务不会停下,需求必然要跟着业务而不断演化,一个看似简单的页面表单也会变得越来越臃肿。
表单变得难以维护
拿项目交接举例子。项目交接了N手,各人都按各自的风格写代码;产品也交接了好几手,各按各的套路提需求。根本原因是代码的维护成本与业务不断变化之间的矛盾。当表单中出现联动的需求,或者跨行之间发生制约关系时,表单代码的复杂度就会上升。随着业务需求的演变,如果代码处理的不好,会变得越来越难维护。
为什么要用el-form-renderer
el-form-renderer是基于 element-ui 封装的表单渲染器,但不局限于 element-ui 组件。在完整继承了 element 的form表单属性的基础上进行了简单扩展,一些非表单组件或者封装的自定义组件,如图片上传、富文本等也可进行整合,从而用户能够通过使用一段预设的数据渲染出一个完整的表单。
用一句话来概括,el-form-renderer 让表单的构建与维护变得更简单了。
el-form-renderer 1.14.0 重磅发布
el-form-renderer 1.14.0 : 支持 v-model
版本更新主要带来了 v-model 功能,并加入了 cypress 做 e2e 测试来护航。下面一起来看看开发小哥此时版本迭代的心路历程吧~
功能:v-model
这次 feature 版本更新,主要是为了 v-model 功能开展的。其可以解决以下问题:
- 不再需要对 ref 操作 updateForm、getFormValue 来与数据交互。
- 可以直接观察当前的表单数据变更,来进行特定操作。
后续对 valid 状态,也通过 prop + sync 的形式管理,基本就可以脱离对 ref 的依赖。
提升代码质量
由于 el-form-renderer 已经是历时两年多的开源项目,许多代码经多人转手,已经存在以下问题:
- 不合适的写法。比如大量使用 render 函数;
- 大量使用 mixin 和字符串拼接来调用功能,开发者难以定位调用链条。比如在生成 option 数组时;
- 废弃的功能。比如不太好用的 enableWhen 等等;
- 没有注释的 hack。比如尝试兼容 select, radio & checkbox 用法的代码(在1.14.1版本修复)。
所以提高维护性也是本次迭代的重中之重。开发从此开始。
测试
第一步,加入自动化测试。先前,项目中已经引入了 jest 做单元测试。单元测试可以保证一些核心逻辑的稳定性,比如一些数据处理、转换函数的输入输出意图。但是其没有办法测试真实示例使用起来时的稳定性,比如用户真正在表单中输入值、点击重置按钮的行为。
此前,行为的稳定性只能依赖开发者和 review 人员手动对示例进行测试。现在,项目使用 cypress 来对每个示例的行为进行自动化测试。这样开发者可以放心重构和添加功能,同时轻松维护过往表现的一致性。
如何写行为测试
重点应当关注用户行为层面,而不是代码输出层面。
比如,应当测试:
- 用户输入文本。
- 确认文本框中有该内容。
- 用户点击提交按钮。
- 确认窗口内显示成功信息。
而不是:
- 用户点击按钮。
- 等待某个接口有返回内容(测试按钮有没绑定到函数,和接口是否正常)。
重构
重构的主要成果是:
- 用 template 重构了所以 render 函数。代码更符合 vue 规范写法。
- 移除了 mixin。将相关功能转化成纯函数,并补充单元测试用例。
- 层级调整。vue 组件移到 components 目录下;js 文件移到 util 目录下。
- 优化函数命名。比如使用具体的 removeDollarInKey 替代 transformItem。
- 梳理了关键复杂点的逻辑,并提取成纯函数,补充单元测试。
- 补全了所有关键示例的端测试。
其中补充的单元测试包括:transformInputFormat(用 inputFormat 处理初始和 updateForm 的值);transformOutputFormat (用 outputFormat 处理 getFormValue 的值);collect (从 content 中搜集 options 和初始 value。
inputFormat & outputFormat
这次重构的最大难点,就是处理 inputFormat、 outputFormat、group 与表单值 value 的逻辑。直接给出定义的话:
- 每个表单项的 inputFormat 接受当前 这一层(group) 的值,返回 当前项的值。
- 每个表单项的 outputFormat 接受 当前项的值 ,返回的值先判断是不是对象:
- 如果不是,返回值视作 当前项的值。
- 如果是,返回值将整体覆盖到 这一层(group) 的值。
具体看如下示例。使用到的同学请务必注意其用法。
inputFormat
重构前:
/ content 配置{id: 'a',inputFormat: v => v + 1 // 接受传入值,返回新值}//this.$refs.form.updateForm({a: 1})this.$refs.form.getFormValue() // {a: 2}
重构后:
// content 配置{id: 'a',inputFormat: formValue => formValue.notA // 接受整个 formValue 来处理!}//this.$refs.form.updateForm({notA: 1})this.$refs.form.getFormValue() // {a: 1}
outputFormat
重构前:
// content 配置[{id: 'a',default: 1,outputFormat: a => a + 1 // 接受内部值,返回处理过的值},{id: 'b',default: {c: 2},outputFormat: b => (b.c += 1, b)},]//this.$refs.form.getFormValue() // {a: 2, b: {c: 3}}
重构后:
// content 配置[{id: 'a',default: 1,outputFormat: a => a + 1 // 这个的理解是对的},{id: 'b',default: {c: 2},outputFormat: b => (b.c += 1, b) // 当返回值是对象时,会整体覆盖到上一层!},]//this.$refs.form.getFormValue() // {a: 2, c: 3}
开发功能:v-model
开发过程
- 新增属性 form,作为对外 v-model 的属性。
- 在 watch 里对接数据流水线:
- 监听 form、content 的变更 -> 搜集初始值 --inputFormat--> value
- 监听 value 的变更 --outputFormat--> form
- 写新的示例 v-model.md 和测试 v-model.spec.js(参考 basic 即可)。
测试流程
- 输入各种 input、select、radio。
- 检查输入后 v-model 的状态。
- 点击 reset 按钮,v-model 回到初始状态。
发现问题
- 在模拟点击 reset 按钮时,当 cypress 测试里点击重置按钮, reset 未生效。
- 手动在 cypress 的调试浏览器中点击按钮,reset 生效。
- 在普通浏览器中点击按钮,reset 生效 。
- 在 reset 函数里打点,此时 reset 函数在以上情况中 都有被正常调用。
- 尝试在 cypress 代码中先等待1秒再点击按钮,reset 未生效。
- 尝试在 cypress 代码中连续写两次 click 按钮操作,reset 生效。
发现有可能是 cypress 的问题,标明注释说明手动测试成功,连点两次是 hack 写法。
解决问题
review 了下原本的 resetFields 逻辑,发现了如下问题:
- reset 后 el-select 报错的 bug。
- reset 后,value 监听器监听不到改变,因为 el-form 的实现机制中改了 value 后没有 emit input 事件。
- 如果在监听器上加 deep: true,则会发现新值和旧值相同。原因暂时不明。
在发现以上问题基础上,对代码进行如下修改:
- 在 form、value 监听器中用 lodash 的 isequal 判断前后的值,有变更时才 emit input 事件。
- 参考 el-form 机制在内部实现了 resetFields:用的是 mounted 时传入的值,然后清除校验错误信息。
解决了用例异常的问题,有如下总结:
- 仍没有弄清楚为什么 cypress 的模拟操作代码调用的 resetFields 没有正常运作。
- 疑似 cypress 的问题导致 resetFields 的其他问题被发现。
- 重视测试的心以一种特别的方式让组件得到了回报。
关于初始状态
值得一提的是,目前 resetFields 所认定的 初始状态,是在组件 mounted 阶段时 v-model 的值。这点与 element 一致,因为一些 element 表单组件,会在 created 阶段,在传入的 value 值不合法时会重新 emit 一个正确的初始值给到 v-model。比如:
- 开启了 multiple 的 el-select,会修正初始值为 [] 。
- el-checkbox,会修正初始值为 [] (由 el-form-renderer 实现,原生是没有的)。
更新文档
此次更新顺带修复了一些老示例的错误用法。可能不少 el-form-renderer 的用户还不了解,el-form-renderer 的 disabled 属性,统一在配置最上层级设置:
// content 定义[{disabled: true, // 在这里设置el: {disabled: true // 无效}}]
在写 v-model 数据流水线时,因为把 content 也融入在其中,所以现在可以在运行时修改 content,并观测表单变化。
结语
el-form-renderer 虽然一路遭遇到吐槽很多,但更多的是忠实粉丝们孜孜不倦地为其提交代码。直到今天,可以看到其相对于原生 element,开发体验已经有了长足的进步。现在有了端对端测试,开发和维护功能更是如虎添翼。这里感谢已为开源作出贡献的开发者们,也欢迎更多小伙伴加入我们 DEEPEXI 开源生态中哦~
“作家不靠灵感写字✍️”
作者:沈扬东
更多内容请点击左下方“了解更多”