📕
余烬的小册
数据结构与算法GitHub
  • 总述
  • 经验记录
    • 经验总结
      • web component
      • 前端性能优化总结与分析
      • 我的长列表优化方案
      • 双向通讯解决方案
      • 🔧基于istanbul实现代码测试覆盖率工具
      • 表单系统(低代码表单)
      • 跨端小程序
      • 设计一个即时聊天功能
      • 跨页面通讯 3658699fe4cb4d0bbe22b0881390bacd
    • 踩坑记录
      • HTML踩坑记录
      • Flutter踩坑记录
      • CSS踩坑记录
  • 源码解析
    • Vue源码解析
      • Vue2源码解析系列-响应式原理
      • Vue2源码解析系列-模板编译
      • Vue2源码解析系列-渲染系统(待更新)
        • Patch
      • Vue2源码解析系列-调度系统(todo)
      • Vue2组件更新流程(todo)
      • 如何学习Vue源码
      • Vue3源码解析系列-响应系统
      • Vue3源码解析系列-渲染系统
      • Vue3源码解析系列-组件化和渲染优化(todo)
      • Vue router源码解析(todo)
    • React源码解析(todo)
    • 微前端
      • qiankun源码解析(todo)
    • Vite源码解析
      • Vite Client源码
      • Vite Server源码(todo)
  • 前端技术
    • javaScript
      • ES6
        • 变量声明
        • 模块化
        • 箭头函数
        • 你不知道的for...of
        • 新的数据结构Set和Map
        • JavaScript异步编程终极解决方案
        • ES6 Class 3a0c0a225a534984aabe9a943c5df975
      • JavaScript Error
      • JavaScript浅拷贝和深拷贝
      • JavaScript闭包
      • JavaScript最佳实践
      • JavaScript设计模式
      • async函数的polyfill
    • 深入理解JavaScript系列
      • JavaScript中的继承
      • JavaScript原始类型和引用类型
      • JavaScript浅拷贝和深拷贝
      • JavaScript手写系列
      • JavaScript之this
      • 词法环境和环境记录
      • JavaScript内存泄漏
      • 执行上下文
      • 从ECMAScript规范中学习this
    • TypeScript
      • TypeScript基础教程
      • Typescript高级操作
      • TypeScript工具类型
      • Typescript手写实现工具类型
      • Typescript总结(思维导图)
    • 浏览器原理
      • 页面渲染原理
      • 浏览器存储
      • JavaScript事件循环
      • 事件循环
      • 跨域
      • DOM事件流
      • 从输入url到页面渲染
      • 判断节点之间的关系及根据节点关系查找节点
      • history API
    • 跨端技术
      • Flutter
        • Flutter布局组件
    • 前端工程化
      • Babel插件开发指南
      • 循环依赖
      • pm2
    • React
      • React 状态管理
      • React组件通讯
      • Redux入门
      • Flux
      • React Hook(todo)
      • Effect
  • 服务器端
    • 计算机网络
      • 应用层
      • 运输层
      • 物理层
      • 数据链路层
      • HTTP缓存
      • HTTPS
      • 网络层
    • NodeJs
      • Node.js
      • nodejs最佳实践
      • 《深入浅出Nodejs》小结
      • mongoose填充(populate)
      • node事件循环
      • Node子进程
      • nestjs从零开始
      • nodejs流
      • Nodejs调试
      • Koa源码解析
    • 服务器
      • 操作系统
      • Linux
      • nginx常用指令
      • nginx常用配置
    • 数据库
      • Mysql常见语法
      • MongoDB Indexes索引
  • 前端安全与性能优化
    • 前端安全
      • 跨站脚本攻击(XSS)
      • 跨站点请求伪造(CSRF)
      • 点击劫持
      • 中间人攻击
      • 越权攻击与JWT
    • 前端性能优化
      • 前端监控系统
      • 前端性能优化总结与分析 7348bba0918645b1899006dc842a64c1
      • 衡量性能的核心指标 0dc15ef127cf4f4a9f1137c377420292
      • 图片懒加载
  • 杂项
    • 其他
      • Git
      • web component框架
      • 实现滚动框的懒加载
      • Stencil指南
    • CSS
      • 定位和层叠上下文
      • BFC
      • 盒模型
      • css选择器
      • css变量
由 GitBook 提供支持
在本页
  • 低代码表单
  • 动态表单
  • 代码示例
在GitHub上编辑
  1. 经验记录
  2. 经验总结

表单系统(低代码表单)

低代码表单

实现三个目标

  • 根据一个特定的数据自动生成表单

  • 获取填写数据并支持校验功能

  • 能自定义属性作用到input等原始组件上

首先讲讲总体思想,所谓的低代码表单就是后端传递一个特殊结构的对象数组,然后确定根据这个对象数组来自动生成表单,这个对象结构可以是这样的:

interface FormItem {
	value: "" // 预填
	config: {  // 配置
		field: 0 // 字段类型
		validator:  '[0-9]{2}' // 正则校验
		SelectOptions: ['男','女'] // 选项
		placeholder: "xxx"  // 需要透传到原生元素节点的html属性
	}
}

然后我们定义好每个原子组件,比如Input组件、Select组件等,然后利用vue动态组件来动态地选择渲染哪个组件。在这个过程中,我们定义Form组件,作为一个整体的表单组件,它会接收前面说的对象数组并负责迭代渲染每一个具体的组件,而在Form组件到原生元素节点之间建立一个Field组件,来做为中间层进行一些处理,例如数据处理。

按这种设计思路,组件数据流就是这样的:

  Form
		|
 	Filed
	  |
	原生节点

如何传递html属性到原生表单元素上?

通过$attrs透传config对象

如何获取每个表单子项的数据到Form组件中并实现数据校验?

在Field组件监听onChange事件可以获取数据,并通过emit(’update:modelValue’) 将数据传递到Form表单组件中。这样的优点是能够实时地获取数据,在某些时候会很有用,但是缺点也有很多:

  1. 对一些表单元素无效

  2. 如果用户不输入任何值,则传递到Form组件的值为空,而不是预填值

因此我们还需要做另外一个方案来弥补这些缺点,并且还能实现校验。

这个方案的思路是通过defineExpose暴露一个校验函数,函数校验成功则返回输入数据,否则返回提示信息。

在用户点击表单提交时可以利用此校验函数进行校验,如果想用户每输入一个表单项就校验的话可以监听onBlur事件。

简易版本完整代码:

// App.vue
<template>
  <div>
    <div>
      <h3>操作区</h3>
      <div  v-for="(item, index) in operators" :key="index">
        <span>{{FieldMap[item.config.field]}}</span>
        <button @click="addField(item)">添加</button>
      </div>
    </div>
    <Form :formData="formData" v-model="formValue" />
  </div>
</template>
<script setup>
import { reactive, watch, ref } from 'vue';
import { FILES_MAP } from './form/field/fields';
import Form from './form/Form.vue';

const FieldMap = reactive(FILES_MAP)

const operators = [
  {
    value: '10',  // 预填数值
    config: {
      field: 0, // 类型
      type: 'number',   // 透传
      placeholder: "手机号码",  // 透传
      validator:  '[0-9]{2}'   // 校验
      
    }
  },
  {
    value: '女',
    config: {
      field: 1,
      placeholder: "性别",
      SelectOptions: ["男", "女"],
      
    }
  }
]

const formData = reactive([])
const formValue = ref([])  // 表单各项输入值

watch(formValue, (val)=>{
  console.log(val);  // 观察数值是否异常
})

function addField(item){
  formData.push(item)
}

</script>
// Form.vue
<template>
    <div>
				// 遍历渲染
        <Field v-for="(field, index) in formData" :key="index" :field="field" v-model="formValue[index]" ref="fieldRefs" />
        <button @click="submit">提交</button>
    </div>
</template>
<script setup>
import { reactive, ref } from 'vue';
import Field from './field/Field.vue';

const emit = defineEmits(['update:modelValue'])

const props = defineProps({
    formData: {
        type: Array,
        default: ()=>[]
    }
})

// 数据的值,初始时使用预填值
const formValue = reactive(props.formData.map(item => item.value))
// 在v-for中获取所有的ref
const fieldRefs = ref([])

// 提交时进行校验
function submit(){
    const validators = fieldRefs.value.map((fieldRef)=> fieldRef.validate)
    Promise.all(validators.map(validate=> validate())).then(res=>{
        formValue.splice(0, formValue.length)
        formValue.push(...res)
        console.log('res',res);
        emit('update:modelValue', formValue)
    }).catch(err=>{
        alert(err)
    })
}
</script>

Field组件,包裹元素节点组件,在这里通过动态组件渲染特定的表单元素组件,另外也负责将数据回传会Form组件中。

// Field.vue
<template>
    <div>
        <component :is="filedCom" v-bind="field.config" :value="field.value" @change="onChange" ></component>
    </div>
</template>
<script setup>
import { computed, toRef,ref  } from '@vue/reactivity';
import { FIELDS_COMPONENTS, FILES_MAP } from './fields';

// v-model 绑定
const emit = defineEmits({
    "update:modelValue": String
})

const props = defineProps(['field'])

const filed = toRef(props, 'field')
// 用户输入的数值,初始化时使用预填值
const value = ref(props.field.value)

// 根据field字段选择组件
const filedCom = computed(()=> FIELDS_COMPONENTS[FILES_MAP[filed.value.config.field]])

function onChange(e){
    value.value = e.target?.value || ''
    emit("update:modelValue", value.value)
}

// 定义校验函数,校验函数不仅仅起到校验作用,还要传递数据
async function validate(){
    const validator = filed.value.config?.validator
    if(validator){
        if(new RegExp(validator).test(value.value)){
            return value.value
        }else{
            return Promise.reject("错误提示消息")
        }
    }

    return value.value
}
defineExpose({validate})
</script>

field文件,保存组件映射。

// field.js
import InputCom from "../components/input.vue"
import SelectCom from "../components/select.vue"
// 表单组件映射
export const FIELDS_COMPONENTS = {
    'INPUT': InputCom,
    "SELECT": SelectCom
}
// field字段映射,和后端约定好有哪些类型
export const FILES_MAP = {
    0: 'INPUT',
    1: 'SELECT'
}

原生表单组件,可以通过css美化样式,可以根据不同的元素类型做一些特定的处理。

// input.vue
<template>
    <input />
</template>

// select.vue
<template>
    <select>
        <option v-for="item in $attrs.SelectOptions" :key="item">{{
                item
        }}</option>
    </select>
</template>

实现无限嵌套

实现无限嵌套是指表单子项里嵌套着另一个表单,例如新加一个表格类型,表格类型可以渲染输入框、多选框,甚至也可以再渲染表格类型。

实现的思路也不难,首先约定好表格类型的field值,假设是3,然后我们在config字段里加上一个新字段fields,它只在表格类型时有效,fields实际上也是一个FormItem对象数组,然后fields数组中又可以嵌套表格类型,如此形成无限嵌套。

在组件上,可以利用递归处理这种嵌套情况,可以递归Form表单组件,也可以递归Field字段组件。具体这里不展开讲

小结

上面的实现比较简单,只实现了文本输入框和多选框,实际中要处理的情况更复杂,不过有了思路后面的组件扩展也比较简单。

需要注意的是,defineExpose暴露函数时,函数的返回值如果是reactive或者ref,那么就会将响应式传递到父组件中,这不符合单向数据流的思想,并且会导致项目变得难以维护,最好是先消除响应式再传递数据,简单的解决思路是:

return data.value //ref
return JSON.parse(JSON.stringify(toRaw(data))) // reactive

toRaw函数会返回reactive的原始对象,但是不建议直接操作原始对象,最好使用深拷贝拷贝出一个新的对象进行操作,最简单的深拷贝就是使用JSON。更多细节见Vue组件通讯

动态表单

所谓的动态表单,是指在多表单系统(一个页面多个表单)中,用户输入某个字段达到一定条件时可以影响另一个表单,举个例子,例如第一个表单的select选项框选择男时,另一个表单的输入框placeholder显示先生你好,如果是女则显示女生你好。

实现起来并不难,因为我们前面已经实现了Form组件的v-model ,这里需要注意的是设计思路应该是自上而下的,因为单向数据流更容易维护。

解决思路是在两表单上增加一个逻辑层,然后监听表单的数据,当达到要求条件时操作FormItem.config配置对象来实现改变表单结构的目的。

// App.js
<Form :formData="formData" v-model="formValue" />
<Form :formData="formData" v-model="formValue" />
// Field中监听change事件,当change时也向上传数据
<Field v-for="(field, index) in formData" :key="index" :field="field" @change="submit"
            v-model="formValue[index]" ref="fieldRefs" />

// App.js
const formData = reactive([{
  value: '',
  config: {
    field: 1,
    placeholder: "性别",
    SelectOptions: ["男", "女"],

  }
}])
const formValue = ref([])

const formData2 = reactive([{
  value: '',  // 预填数值
  config: {
    field: 0, // 类型
    type: 'number',
    placeholder: "手机号码",  // 透传
    validator: '[0-9]{2}'

  }
},])
const formValue2 = ref([])

watch(formValue, ([val]) => {
  if(val === '男'){
     formData2[0].config.placeholder = "先生你好"
  }else {
    formData2[0].config.placeholder = "女士你好"
  }
})

代码示例

上一页🔧基于istanbul实现代码测试覆盖率工具下一页跨端小程序

最后更新于1年前

PlayCode - Javascript Playground