《前端那些事》从0到1开发动态表单

简介: 前沿:中后台应用中表单需求颇多,左手一个表单,右手又是一个表单,无穷无尽,如果用模版一个个来写,不单写起来费时费力,而且看起来也是天花乱坠,于是这个时候你会去设想,那有没有什么方式可以去替换琐碎的手写表单模版的方式呢?让表单是“配出来”的,而不是撸出来的,让你轻松解决 form 表单,也不再为表单而烦恼。答案就是:动态表单

微信截图_20220512164551.png

前沿:中后台应用中表单需求颇多,左手一个表单,右手又是一个表单,无穷无尽,如果用模版一个个来写,不单写起来费时费力,而且看起来也是天花乱坠,于是这个时候你会去设想,那有没有什么方式可以去替换琐碎的手写表单模版的方式呢?让表单是“配出来”的,而不是撸出来的,让你轻松解决 form 表单,也不再为表单而烦恼。答案就是:动态表单


1.传统表单模版


一个表单需要什么?无疑是包含了form数据的收集、验证及提交等等功能,让我们看看下面这个基于iview组件库的form表单


微信截图_20220512164612.png


这个简单的表单,如果我们用手写模版的方式撸出来,模版部分就是如下所示👇


微信截图_20220512164621.png


数据初始化定义和验证提交逻辑如下


微信截图_20220512164643.png


以上就完成一个具备数据收集、验证、提交、重制的表单,但是相对应问题也来了,这里用模板并不是最好的选择,代码过于冗长,也存在重复代码,如果我的项目中十几个表单甚至更多,我岂不是都要去写怎么多代码去维护这类表单,会不会显得太冗余,接下来进入我们今天的主角:动态表单,让我们看看怎么让他“动”💃起来


2 动态表单


2.1 我所期望的表单


我期望的表单是可以配出来的,通过JSON来动态渲染生成相应的表单,表单中涉及的组件(比如Input、Select)可以通过获取JSON的配置所需的去渲染,上一小节提到的模版渲染显然就不适用这次场景了,虽然vue官方推荐在绝大多数情况下使用模板来创建你的temlate,但是一些场景还是需要用到渲染函数render 官方文档点我👈


2.2 关于渲染函数


我们先看看这个例子,Vue.js 的 mount 函数,将h()生成的VNode节点函数,渲染成真实 DOM 节点,并挂载到根节点上



微信截图_20220512164657.png


这个h()函数本质上是createElement 函数,这个函数的作用就是生成一个 VNode节点(虚拟节点),它不是一个实际的 DOM 元素。叫createNodeDescription(创建节点描述),我们是通过它所包含的信息会来告诉 Vue 页面上需要渲染什么样的节点,再通过diff算法可以追踪dom的变化


拓展:你可能会好奇为啥是叫h()函数,而不是createElement()的简称c()


h出自hyperscript首字母,最原始的定义是“Create HyperText with JavaScript”,而HyperText则是出自我们熟悉的则HTML 是 hyper-text markup language 的缩写(超文本标记语言),所以可以理解为Hyperscript是指生成HTML的 script 脚本


createElment函数接受三个参数,分别是:


  • 参数一:标签名、组件的选项对象、函数等等(必选);
  • 参数二:设置这个对象的样式、属性、传的组件的参数、绑定事件等(可选);
  • 参数三:该节点下的其他节点,即子级虚拟节点,可以是字符串形式或数组形式,也需要使用createElement构建。


下面用一个简单例子说明渲染函数的使用👇


微信截图_20220512164729.png


面例子的模版渲染和渲染函数渲染的结果是一样的,当然模板本质上也是通过 Compile 编译 得到 渲染函数render(),所以说其实渲染函数更高效,更快,减少了编译的时间,关于编译可以看这篇vue 编译过程,由templete编译成render函数


渲染函数render与模板template的区别


  • render(高)的性能要比tempate(低)要高。
  • template简单,可以直观看出内容想要表述的含义,但是不够灵活而;render渲染

     函数则是通过createElement的方式创建VNode,适合开发复发性强的组件。


扯完渲染函数,接下来介绍下动态表单的思路


3 动态表单的实现


这里使用的是iview组件库的基础上实现的动态表单,创建的组件都是基于iview来实现的,下面是具体的流程图


微信截图_20220512164739.png


3.1配置表单配置内容


我用第一节的例子来配置一个JSON格式的表单配置(因为配置文件过长,改用文字)


const formOption = {
  ref: 'formValidate',
  style: { //表单样式,非必须
    width: '300px',
    margin: 'auto',
  },  
  className: 'form',
  formProps: { //非必须
    'label-width': 80,
  },
  formData: {//所要监听的表单字段数据,必须
     name: '',
     city: '',
     sex: 'male',
  },
  formItem: [ //iview form表单的每个formItem,必须
    {
      type: 'input',
      label: '名称', //对应formItem的label
      key: 'name', //key对应formData中的字段
      props: {
        placeholder: '请输入名称',
      },
      rules: {  //表单检测规则,非必须
        required: true,
        message: '请填写名称',
        trigger: 'blur',
      },
    },
    {
      type: 'select',
      label: '城市', //对应formItem的label
      key: 'city', //key对应formData中的字段
      props: {
        placeholder: '请输入名称',
      },
     children: [{ label: 'xml', value: '1' },
        { label: 'json', value: '2' },
        { label: 'hl7', value: '3' }
      ],
      rules: {  //表单检测规则,非必须
        required: true,
        message: '请选择城市',
        trigger: 'blur',
      },
    }, 
    {
      type: 'radioGroup',
      key: 'type',
      label: 'sex',
      children: [
        {
          text: 'female',
          label: 'female',
        },
        {
          text: 'male',
          label: 'male',
        },
      ],
      events: {
        'on-change': (vm, value) => {
          vm.$emit('on-change', value);
        },
      },
    }
  ],
  events: events('formValidate'),//表单按钮组                                       
}


还有相应的事件按钮统一在events中处理(可复用)


微信截图_20220512164752.png


3.2 render函数渲染组件


第一节例子涉及到表单组件分别是Input、Select、radioGroup、formItem。分别是定义它们的render函数


  • 暴露不同组件渲染的api


微信截图_20220512164802.png


  • Input组件渲染函数


集合iview组件库Input的API,包括props属性、events事件、slot插槽、methods方法等来定义渲染函数,具体实现如下图所示


function generateInputComponent(h, formData = {}, obj, vm) {
    const key = obj.key? obj.key : ''
    let children = []
    if (obj.children) { //input有子集,走这里
        children = obj.children.map(item => {
            let component
            if (item.type == 'span') {  //复合型输入框情况                                       
                component = h('span', {
                    slot: item.slot
                }, [item.text])         
            } else {
                let func = componentObj[item.type]
                component = func? func.call(vm, h, formData, item, vm) : null
            }
            return component
        })
    }
    return h('Input', {
        props: {
            value: key? formData[key] : '',
            ...obj.props
        },
        style: obj.style,
        on: {
            ...translateEvents(obj.events, vm), //时间绑定
            input(val) {
                if (key) {
                    formData[key] = val
                }
            }
        },
        slot: obj.slot                                                                 
    }, children)
}
 //事件bind
function translateEvents(events = {}, vm, formData = {}) {
    const result = {}
    for (let event in events) {
        result[event] = events[event].bind(vm, vm, formData);
    }
    return result 
}                  


  • Select 组件渲染函数


function generateSelectComponent(h, formData = {}, obj, vm) {
    const key = obj.key? obj.key : ''
    let components = []
    if (obj.children) {
        components = obj.children.map(item => {
            if (item.type == 'optionGroup') {
                return h('OptionGroup', {
                    props: item.props? item.props : item
                }, item.children.map(child => {
                    return h('Option', {
                        props: child.props? child.props : child
                    })
                }))
            } else {
                return h('Option', {
                    props: item.props? item.props : item
                })
            }
        })
    }
    return h('Select', {
        props: {
            value: formData[key],
            ...obj.props
        },
        style: obj.style,
        on: {
            ...translateEvents(obj.events, vm),
            input(val) {
                if (key) {
                    formData[key] = val
                }
            }
        },
        slot: obj.slot
    }, components)
}


这里只是展示部分组件的实现方式,主要目的是梳理开发及应用的流程思路


  • events 按钮生成


function generateEventsComponent(h, formData = {}, obj, vm) {
    const components = [];
    if(obj.submit) {
        const submit = h('Button', {
            props: obj.submit.props,
            style: obj.submit.style,
            class: obj.submit.className,
            on: {
                click() {
                   //提交前校验
                    vm.$refs[obj.ref].validate((valid) => {
                        if (valid) {
                            obj.submit.success.call(vm, formData, vm)
                        } else {
                            obj.submit.fail.call(vm, formData, vm)
                        }
                    })
                }
            }
        }, [obj.submit.text])
        components.push(submit)
    }
    if (obj.reset) {
        const reset = h('Button', {
            props: obj.reset.props,
            style: {
                ...obj.reset.style,
            },
            class: obj.reset.className,
            on: {
                click() {
                    vm.$refs[obj.ref].resetFields() //重置表单
                    obj.reset.success.call(vm, formData, vm); 
                }
            }
        }, [obj.reset.text])
        components.push(reset)
    }
    return h('div',{
        class: 'vue-events',
        style: {
            ...obj.style
        }
    }, components)
}


  • formBuild动态表单组件的定义


实现好组件的动态生成逻辑,这个时候需要一个入口(formBuild.js),就是根据配置去映射相应的组件并生成合并,组合成为最终要的表单


// form-build.js
import componentObj from './utils'
export default {
    props: {
        options: {
            type: Object,
            required: true
        },
    },
    render(h) {
        const options = this.options
        const formData = options.formData
        if (!options.formItem) {
            return h('div')
        }
        const components = options.formItem.map(item => {
            let func = componentObj[item.type]
            let subComponent = func? func.call(this, h, formData, item, this) : null
            let component = componentObj.formItem(h, item, subComponent, formData)
            return componentObj.col(h, item, component)
        })
        const childComp = [];
        const fromComp = h('Form', {
                ref: options.ref,
                style: options.style ? options.style : '',
                props: {
                    model: formData,
                    ...options.formProps
                },
                class: 'vue-generate-form'
            }, [
                h('Row', {
                    props: options.rowProps
                }, components)
           ]);
        childComp.push(fromComp);
        if (options.events) {
            const eventComo = componentObj.events(h, formData, obj.events , vm)                         
          childComp.push(eventComp)
        }
        return h('div', [childComp]);
    }
}


还需要定义vue的插件安装


微信截图_20220512164814.png


2.3 如何使用


微信截图_20220512164822.png


  • 注意事项


  1. 某些组件(例如 button) iview 并没有提供类似于 on-click 这样的事件。可以使用 DOM 元素原生事件代替,例如 click
  2. 所有表单数据都要在formData里定义


4.总结


以上就可以通过render渲染函数来完成动态表单工具的实现,本文主要是通过一种思路去介绍整个开发,动态表单有多种实现方式,当然你可能也有疑惑


  • 如何支持多种UI组件库的动态表单配置?


你可以参考下开源的form-create(支持3种 UI 框架:Iview、ElementUI、Ant-design-vue)是如何实现的 form-create工具库


  • 如何开发在线编辑配置的动态表单工具?


可视化表单设计工具也很香,有兴趣的童鞋可以了解 vue-ele-form-generator


文章思路来源:vue-form-make


往期文章




相关文章
|
29天前
|
缓存 前端开发 JavaScript
cnblogs——从主题开发浅谈前端性能优化
cnblogs——从主题开发浅谈前端性能优化
27 0
|
13天前
|
JavaScript 前端开发 开发者
哇塞!Vue.js 与 Web Components 携手,掀起前端组件复用风暴,震撼你的开发世界!
【8月更文挑战第30天】这段内容介绍了Vue.js和Web Components在前端开发中的优势及二者结合的可能性。Vue.js提供高效简洁的组件化开发,单个组件包含模板、脚本和样式,方便构建复杂用户界面。Web Components作为新兴技术标准,利用自定义元素、Shadow DOM等技术创建封装性强的自定义HTML元素,实现跨框架复用。结合二者,不仅增强了Web Components的逻辑和交互功能,还实现了Vue.js组件在不同框架中的复用,提高了开发效率和可维护性。未来前端开发中,这种结合将大有可为。
50 0
|
20天前
|
设计模式 JavaScript 前端开发
Vue.js 组件设计模式:在前端热潮中找到归属感,打造可复用组件库,开启高效开发之旅!
【8月更文挑战第22天】Vue.js 以其高效构建单页应用著称,更可通过精良的组件设计打造可复用组件库。组件应职责单一、边界清晰,如一个显示文本并触发事件的按钮组件,通过 props 传递标签文本,利用插槽增强灵活性,允许父组件注入动态内容。结合 CSS 预处理器管理和封装独立模块,配以详尽文档,有效提升开发效率及代码可维护性。合理设计模式下,组件库既灵活又强大,持续实践可优化项目工作流。
31 1
|
23天前
|
缓存 前端开发 Linux
哇塞!NPM 缓存竟成开发拦路虎?快来掌握清空秘籍,开启前端开发逆袭之旅!
【8月更文挑战第20天】NPM是前端开发中管理依赖的关键工具。有时需清空其缓存以解决版本不一致或包损坏等问题,确保使用最新依赖。可通过命令`npm cache clean --force`强制清空全部缓存,或手动删除各系统下的缓存文件夹。注意清空缓存可能延长后续安装时间,建议事先备份依赖或确保可重新安装。正确管理缓存有助于提升开发效率。
31 1
|
24天前
|
缓存 前端开发 JavaScript
高效开发现代 Web 应用:从前端到后端的最佳实践
在开发现代 Web 应用时,前端和后端技术的选择对项目的性能、可维护性和用户体验至关重要。本文将探讨如何通过现代工具和框架来优化前端和后端开发流程。我们将分析前端技术(如 React 和 Vue.js)与后端技术(如 Node.js 和 Django)的集成,并提供实际案例来展示如何实现高效开发。无论是对新手还是经验丰富的开发者,本指南都提供了宝贵的洞见和实用的技巧,以帮助提高开发效率并构建出色的 Web 应用。
|
27天前
|
前端开发 Java C++
超简单使用Vite+Vue3构建共享开发和分模块打包的前端项目
使用Vite和Vue3构建支持共享组件和分模块独立打包的前端项目的方法。
80 0
超简单使用Vite+Vue3构建共享开发和分模块打包的前端项目
|
11天前
|
前端开发 Java UED
JSF遇上Material Design:一场视觉革命,如何让传统Java Web应用焕发新生?
【8月更文挑战第31天】在当前的Web开发领域,用户体验和界面美观性至关重要。Google推出的Material Design凭借其独特的动画、鲜艳的颜色和简洁的布局广受好评。将其应用于JavaServer Faces(JSF)项目,能显著提升应用的现代感和用户交互体验。本文介绍如何通过PrimeFaces等组件库在JSF应用中实现Material Design风格,包括添加依赖、使用组件及响应式布局等步骤,为用户提供美观且功能丰富的界面。
19 0
|
11天前
|
前端开发 大数据 数据库
🔥大数据洪流下的决战:JSF 表格组件如何做到毫秒级响应?揭秘背后的性能魔法!💪
【8月更文挑战第31天】在 Web 应用中,表格组件常用于展示和操作数据,但在大数据量下性能会成瓶颈。本文介绍在 JavaServer Faces(JSF)中优化表格组件的方法,包括数据处理、分页及懒加载等技术。通过后端分页或懒加载按需加载数据,减少不必要的数据加载和优化数据库查询,并利用缓存机制减少数据库访问次数,从而提高表格组件的响应速度和整体性能。掌握这些最佳实践对开发高性能 JSF 应用至关重要。
25 0
|
11天前
|
前端开发 API 开发者
JSF与RESTful服务的完美邂逅:如何打造符合现代Web潮流的数据交互新体验
【8月更文挑战第31天】随着互联网技术的发展,RESTful架构风格因其实现简便与无状态特性而在Web服务构建中日益流行。本文探讨如何结合JavaServer Faces (JSF) 和 JAX-RS 构建RESTful API,展示从前端到后端分离的完整解决方案。通过定义资源类、配置 `web.xml` 文件以及使用依赖注入等步骤,演示了在JSF项目中实现RESTful服务的具体过程,为Java开发者提供了实用指南。
23 0
|
11天前
|
开发者 容器 Docker
JSF与Docker,引领容器化浪潮!让你的Web应用如虎添翼,轻松应对高并发!
【8月更文挑战第31天】在现代Web应用开发中,JSF框架因其实用性和灵活性被广泛应用。随着云计算及微服务架构的兴起,容器化技术变得日益重要,Docker作为该领域的佼佼者,为JSF应用提供了便捷的部署和管理方案。本文通过基础概念讲解及示例代码展示了如何利用Docker容器化JSF应用,帮助开发者实现高效、便携的应用部署。同时也提醒开发者注意JSF与Docker结合使用时可能遇到的限制,并根据实际情况做出合理选择。
22 0