2

来自产品MM的需求

  1. 首先看一个简单的表单需求,下面是一个简单数据收集表的一部分,选择“立即生效”后出现
    "生效日期"且必填。

用Vue + Element UI很容易实现这个需求,开动VSCode一顿常规操作,10分钟收工。

  1. 第二天产品MM又找过来了,需求有一丢丢改动,如下图示:
活动类型为冲单或回馈时,支持生效条件;条件支持活动人数、活动天数(二者为且的关系)

肿么办?启动VSCode,又是一通常规操作,这次改动麻烦一点,一个小时收工,当然要发布上线的话,还免不了推送、构建、测试、打包、重新部署。

  1. 然鹅,游戏并没有over,过两天产品MM又来找你了,因为需求疏漏了一个地方:生效条件需要加上“峰值”判断,如下图所示:

如此循环往复,需求永远在不停迭代,前端开发疲于奔命,终于有一天你变成了这般模样:

解决之道

针对上述这种非常多变的表单需求,简单分析一下,变动的主要是表单控件和逻辑判断,所以首先想到的就是开发一个配置式的表单,设计一个全新的表单schema规范,然后根据schema编写表单JSON对象,最后由表单JSON动态生成表单。下面就是一段常见的基于schema的表单JSON代码:

{
      title: '活动类型',
      key: 'act_type',
      type: 'radio',
      props: {
        options: { 1: '拉新', 2: '冲单', 3: '回馈' }
      }
},

这种思路已经非常成熟了,有非常多成熟的开源表单项目采用了这种思路,但这个方案有两个比较明显的缺点:

1. 使用者需要学习表单schema规范;

2. 难于实现复杂的表单交互逻辑。

为了解决第一个schema规范学习成本的问题,可以基于表单schema开发一个拖拽式的所见即所得的在线表单设计器,这个也有非常多的开源项目实现了,各种form generator、form creator等等,鄙人不才也搞了一个VForm,有兴趣的童鞋可以尝尝鲜:

VForm,一个Vue动态表单设计器,==>>点此体验

为了解决第二个问题,如何实现动态表单的复杂交互逻辑,也是本文的主要目标,本文的解决思路是——为动态表单增加可编程接口,即通过组件的交互事件和API方法实现交互逻辑,JS代码才是王道。

实现表单的可编程接口

从第一部分的表单需求来分析,要实现表单交互逻辑,第一步是暴露组件的交互事件,比如“活动类型”组件点击改变后触发的onChange事件;第二步就是在事件中对组件进行精确操控,比如显示或隐藏某些组件、设置组件必填属性、设置组件禁用状态、添加或移除组件CSS样式等等。
第一步非常简单,只要给表单schema增加组件的自定义事件属性即可,下面schema给input组件增加了7个自定义事件:

  {
    type: 'input',
    icon: 'text-field',
    formItemFlag: true,
    options: {
      name: '',  //组件基本属性
      label: '',
      labelAlign: '',
      type: 'text',
      defaultValue: '',
      placeholder: '',
      //-------------------
      onCreated: '',  //自定义事件
      onMounted: '',
      onInput: '',
      onChange: '',
      onFocus: '',
      onBlur: '',
      onValidate: '',
    },
  },

接下来需要加入一个支持语法高亮、代码提示的代码编辑器组件,这里选择成熟、久经考验的AceEditor,GitHub有一个打包好的ace-builds,安装使用十分简单:

安装ace:npm i ace-builds

然后基于ace封装一个简单的JS代码编辑器,截取部分代码如下所示:

<template>
  <div class="ace-container">
    <!-- 官方文档中使用id,这里禁止使用,在后期打包后容易出现问题,使用 ref 或者 DOM 就行 -->
    <div class="ace-editor" ref="ace"></div>
  </div>
</template>

<script>
  import ace from 'ace-builds'
  /* 启用此行后webpack打包回生成很多动态加载的js文件,不便于部署,故禁用!!
     特别提示:禁用此行后,需要调用ace.config.set('basePath', 'path...')指定动态js加载URL!!
   */
  //import 'ace-builds/webpack-resolver'

  import 'ace-builds/src-min-noconflict/theme-sqlserver' // 新设主题
  import 'ace-builds/src-min-noconflict/mode-javascript' // 默认设置的语言模式
  import 'ace-builds/src-min-noconflict/mode-json' //
  import 'ace-builds/src-min-noconflict/mode-css' //
  import 'ace-builds/src-min-noconflict/ext-language_tools'
  import {ACE_BASE_PATH} from "@/utils/config";

  export default {
    name: 'CodeEditor',
    props: {
      value: {
        type: String,
        required: true
      },
      readonly: {
        type: Boolean,
        default: false
      },
      mode: {
        type: String,
        default: 'javascript'
      },
      userWorker: {  //是否开启语法检查,默认开启
        type: Boolean,
        default: true
      },

    },
    mounted() {
      ace.config.set('basePath', ACE_BASE_PATH)
    },
    //省略methods方法...
  }

给CodeEditor增加代码提示:

      addAutoCompletion(ace) {
        let acData = [
          {meta: 'VForm API', caption: 'getWidgetRef', value: 'getWidgetRef()', score: 1},
          {meta: 'VForm API', caption: 'getFormRef', value: 'getFormRef()', score: 1},
          //TODO: 待补充!!
        ]
        let langTools = ace.require('ace/ext/language_tools')
        langTools.addCompleter({
          getCompletions: function(editor, session, pos, prefix, callback) {
            if (prefix.length === 0) {
              return callback(null, []);
            }else {
              return callback(null, acData);
            }
          }
        })
      }

封装后的CodeEditor效果如下(此处使用VForm演示):

交互事件有了,接下来实现组件的操控API方法,这里又分两步走:

1. 获取到组件ref;
2. 调用组件的methods属性中的方法;

首先给表单增加一个refList的provider属性,在组件中inject注入,当每个组件创建时将本组件实例注入refList对象:

    //...此处省略
    inject: ['refList'],
    //...此处省略
    created() {
      this.registerToRefList()
    },
    methods: {
      registerToRefList() {
        this.refList[this.field.options.name] = this
      },
      //...此处省略
    }

接下来,再封装一个简单的getWidgetRef方法,该方法通过组件名称获取到组件实例:

    getWidgetRef(widgetName, showError) {
      let foundRef = this.refList[widgetName]
      if (!foundRef && !!showError) {
        this.$message.error('Ref not found')
      }
      return foundRef
    },

通过组件实例即可调用methods属性中的组件方法,组件方法可以任意扩充。
实现一个简单的点击事件交互:

/* 下述代码在“喜欢喝酒还是饮料?”单选按钮onChange事件中执行 */
var alcoholChkWidget = this.getWidgetRef('alcoholChk')
var drinkChkWidget = this.getWidgetRef('drinkChk')

if (value === 1) {
  alcoholChkWidget.setHidden(false)  //setHidden是自定义的API方法,控制组件显示或隐藏
  drinkChkWidget.setHidden(true)
} else {
  alcoholChkWidget.setHidden(true)
  drinkChkWidget.setHidden(false)
}

交互效果如下:

需要提醒的是,交互事件中的js代码是在表单运行期间执行,代码不会被Babel编译,具体执行过程是:通过Function生成一个匿名函数,传入指定参数后执行,没有使用低效且不安全的eval方法。

如果要调试交互JS代码也非常简单,只需要在代码中需要下断点的位置加入"debugger",代码执行到此处会进入调试状态(Chrome内核的浏览器都支持)。

结论

综上所述,实现一个可编程的Vue动态表单,大体实现思路如下:

1.设计一个表单schema规范,基于该schema规范开发一个拖拽式的表单设计器;

2.暴露组件的交互事件,并按需求实现组件的API方法;

3.提供一个语法高亮、有代码提示功能的JS代码编辑器;

4.通过在交互事件中调用组件API方法,即可实现表单复杂交互逻辑;

5.完美实现预定目标。

鄙人的新轮子VForm完全实现了上述功能,有兴趣的小伙伴可以体验尝试。
点此立即体验

详细使用文档:VForm专栏

GitHub仓库链接:https://github.com/vdpadmin/VFormBuilds

Gitee备份仓库:https://gitee.com/vdpadmin/VFormBuilds

vformAdmin
13 声望0 粉丝