Bootstrap

vue-form-craft,基于vue3的开箱即用表单方案

一、前言

首先祝各位朋友们新年快乐!龙年大吉!

在前端开发过程中,表单渲染是重要且繁琐的一环。为了提高开发效率并避免重复工作,我开发了一款基于vue3 的表单工具,并取名vue-form-craft(vue表单工艺)
是适用于 vue3项目 中后台表单的一种通用解决方案。

本文将介绍 vue-form-craft 的基本概念、使用方式及高级特性。

二、简介

vue-form-craft 主要由FormDesign(表单设计器)SchemaForm(表单渲染器) 组成。

FormDesign通过拖拽快速生成JsonSchema,SchemaForm使用 JsonSchema 协议渲染表单

在线预览

文档

github源码

优势

  • 轻量级: 可以通过npm依赖直接集成到你的vue3项目
  • 易于使用:容易上手,可以通过表单设计器可视化拖拽的方式快速生成表单。
  • 协议简单:遵循 JsonSchema 规范,因此相对容易理解和上手。
  • 较强的配置能力:具有较强的配置能力,可以对表单联动、校验、布局以及数据处理等方面进行配置。
  • 良好的性能体验:底层采用 element plus 的 Form 来实现表单的数据收集和管控,同时针对控件渲染层面进行优化处理,从而大幅提升性能,使得在使用过程中具有良好的性能体验。
  • 内置组件丰富:内置组件非常丰富,包括基础组件、嵌套卡片类组件和动态增减 List 组件等,可以满足大多数场景的表单实现需求。
  • 扩展性强:具有非常强的扩展性,支持自定义各种类型的表单控件,支持多种ui库,用户可以根据实际需要进行定制,非常灵活。

三、如何使用

最新版本v3.0.8 已经支持ts类型检查,推荐vue3+ts项目引入使用!

1、安装依赖

npm i vue-form-craft

2、全局注册

import { createApp } from 'vue'
import App from './App.vue'
import VueFormCraft from 'vue-form-craft'
const app = createApp(App)

app.use(VueFormCraft)
app.mount('#app')

3、使用

<template>
  <schema-form :schema="schema" footer @onFinish="onFinish" />
</template>

<script setup lang="ts">
import type { schemaType , formValuesType } from 'vue-form-craft'

const schema: schemaType = {
  labelWidth: 150,
  labelAlign: 'right',
  size: 'default',
  items: [
    {
      label: '用户名',
      component: 'Input',
      props: {
        placeholder: '请输入用户名'
      },
      name: 'username'
    },
    {
      label: '密码',
      component: 'Password',
      props: {
        placeholder: '请输入密码'
      },
      name: 'password'
    }
  ]
}

const onFinish = (values: formValuesType) => {
  alert(JSON.stringify(values))
}
</script>

SchemaForm.png

4、通过表单设计器拖拖拽拽 快速生成JsonSchema

<template>
  <form-design @onSave="(schema) => console.log(schema)" />
</template>

formDesign.png

四、一分钟读懂JsonShema

首先,我们要理解,JSON Schema就是 表单的抽象 。

JSON的最外层是表单整体的配置,items里面是每个字段的配置。

items里是每个字段的抽象,label、name、component等是每个字段的通用配置。

component代表使用什么组件,props是传给该组件的props。大部分组件都是基于el二次封装,所以也支持该组件在el文档的所有props

{
  "labelWidth": 150,   //表单label宽度
  "labelAlign": "right",   //表单label对齐方式
  "size": "default",   //表单字段大小
  "items": [  //表单所有字段的配置
    {
      "label": "用户名", //字段的label
      "component": "input", //字段使用的组件
      "props": {    //传给该组件的props,支持该组件在element plus的所有props
        "placeholder": "请输入用户名"
      },
      "name": "username" //唯一标识,也就是值key
    },
    {
      "label": "密码",
      "component": "password",
      "props": {
        "placeholder": "请输入密码"
      },
      "name": "password"
    }
  ]
}

五、表单联动

要评价一个表单工具能力强不强,表单联动能力至关重要。 vue-form-craft 通过 模板引擎 动态生成JsonSchema,让表单联动变得非常容易。

1、模板表达式

模板表达式为字符串格式,以双花括号 {{ … }}为语法特征,对于简单的联动提供一种简洁的配置方式。

在JsonSchema中,被双花括号包裹的字符串一律会被解析为 js表达式并返回结果,且只能使用联动变量。这种联动方式能应对大部分联动场景😎

例如:控制字段禁用、隐藏、文案提示等交互。

JsonSchema 所有协议字段都支持模板表达式。

{
  "labelWidth": 150,
  "labelAlign": "right",
  "size": "default",
  "items": [
    {
      "label": "姓名",
      "component": "Input",
      "name": "name",
      "props": {
        "placeholder": "请输入姓名"
      }
    },
    {
      "label": "自我介绍",
      "component": "TextArea",
      "name": "desc",
      "props": {
        "placeholder": "{{ $values.name + '的自我介绍' }}",
        "disabled":"{{ !$values.name }}"
      }
    }
  ]
}

Schema插值表达式 可以使用的联动变量:

变量名类型描述
$valany当前字段值
$valuesObject整个表单的值
$selectObject当前字段如果是【选择类字段】,这个就是选中项对应的数据源
$selectDataObject【选择类字段】选中项数据源合集
$itemObject【自增组件】专用,单行的数据值
any由schemaContext传入的自定义变量

联动案例1

linkage1.gif

{
  labelWidth: 150,
  labelAlign: 'right',
  size: 'default',
  items: [
    {
      label: '评分',
      component: 'Rate',
      name: 'rate',
      props: {
        max: 5,
        'allow-half': true
      },
      required: true
    },
    {
      label: '差评原因',
      component: 'Textarea',
      name: 'reason',
      props: {
        placeholder: '请输入...',
        autosize: {
          minRows: 4,
          maxRows: 999
        }
      },
      hidden: '{{ !$values.rate || $values.rate>3 }}' //评分大于3分时隐藏,未评分时也要隐藏
    }
  ]
}


联动案例2

linkage2.gif

{
  labelWidth: 150,
  labelAlign: 'right',
  size: 'default',
  items: [
    {
      label: '分类',
      component: 'Radio',
      props: {
        mode: 'static',
        options: [
          {
            name: '前端',
            id: 1
          },
          {
            name: '后端',
            id: 2
          },
          {
            name: '运维',
            id: 3
          },
          {
            name: '其他',
            id: 4
          }
        ],
        labelKey: 'name',
        valueKey: 'name',
        optionType: 'button',
        space: 0
      },
      name: 'category',
      required: true
    },
    {
      label: '文章',
      component: 'Radio',
      props: {
        mode: 'remote',
        placeholder: '请选择文章',
        labelKey: 'title',
        valueKey: 'id',
        api: {
          url: '/current/query/article',
          method: 'GET',
          params: {
            filters: {
              category: '{{$values.category}}'
            }
          },
          dataPath: 'data'
        },
        optionType: 'circle',
        autoSelectedFirst: true,
        direction: 'vertical',
        space: 0
      },
      name: 'article',
      required: true,
      hidden: '{{!$values.category}}'
    }
  ]
}

2、字段监听

上面的 模板表达式 虽然足够灵活,但是不能做到表单值联动,所以给每个字段提供了一个change配置,可以监听字段变化去修改其他字段的值。

change是一个数组,可以同时联动多个字段。target为目标字段,value是修改的值,也支持插值表达式。

联动案例3

ffl55-trc6z.gif

{
  "labelWidth": 150,
  "labelAlign": "right",
  "size": "default",
  "items": [
    {
      "label": "字段1",
      "component": "Input",
      "props": {
        "placeholder": "请输入..."
      },
      "name": "item1",
      "change": [
        {
          "target": "item2",
          "value": "{{$val * 2}}"
        },
        {
          "target": "item3",
          "value": "{{$val + '元'}}"
        }
      ]
    },
    {
      "label": "字段2",
      "component": "Input",
      "props": {
        "placeholder": "请输入..."
      },
      "name": "item2"
    },
    {
      "label": "字段3",
      "component": "Input",
      "props": {
        "placeholder": "请输入..."
      },
      "name": "item3"
    }
  ]
}

联动案例4

一些场景需要根据已选值的数据源中取某个字段,再给其他字段作为值,这就可以用上 $select

4or83-4wx9e.gif

{
  "labelWidth": 150,
  "labelAlign": "right",
  "size": "default",
  "items": [
    {
      "label": "选择商品",
      "component": "Select",
      "props": {
        "mode": "static",
        "options": [
          {
            "name": "商品1",
            "id": "1",
            "price": 25
          },
          {
            "name": "商品2",
            "id": "2",
            "price": 65
          },
          {
            "name": "商品3",
            "id": "3",
            "price": 100
          }
        ],
        "placeholder": "请选择...",
        "labelKey": "name",
        "valueKey": "id"
      },
      "name": "commodity",
      "change": [
        {
          "target": "price",
          "value": "{{$select.price}}"
        }
      ]
    },
    {
      "label": "价格",
      "component": "InputNumber",
      "name": "price",
      "props": {
        "min": 1,
        "max": 9999,
        "step": 1,
        "unit": "元",
        "disabled": true,
        "controlsPosition": "right"
      }
    }
  ]
}

六、高级特性

1、表单校验

所有表单项都可以配置required:true, 来给字段设置必填校验。

如果是input类字段,则可以配置 rules 设置更复杂的校验规则,参考el文档

type做了扩展,可以直接写正则表达式,就会根据其校验了

{
  "labelWidth": 150,
  "labelAlign": "right",
  "size": "default",
  "items": [
    {
      "label": "邮箱",
      "component": "Input",
      "props": {
        "placeholder": "请输入邮箱"
      },
      "name": "email",
      "required": true,
      "rules": [
        {
          "type": "email",
          "message": "邮箱格式不合法",
          "trigger": [
            "blur"
          ]
        },
        {
          "type": "^\\S*$",
          "message": "不能包含空格",
          "trigger": [
            "blur",
            "change"
          ]
        }
      ]
    }
  ]
}

2、远程数据

下拉选择框、单选框等选择类字段,vue-form-craft都进行了二次封装,可以直接配置接口参数,来自动获取远程数据。

选项.png

{
      label: '文章',
      component: 'Radio',
      props: {
        mode: 'remote',
        placeholder: '请选择文章',
        labelKey: 'title',
        valueKey: 'id',
        api: {
          url: '/current/query/article',
          method: 'GET',
          params: {},
          dataPath: 'data'
        },
        optionType: 'circle',
        autoSelectedFirst: true,
        direction: 'vertical',
        space: 0
      },
      name: 'article',
    }

默认使用axios来请求,你也可以在main.js里给组件传入你项目里封装好的axios,然后表单所有组件都会用它来发ajax请求

import { createApp } from 'vue'
import App from './App.vue'
import VueFormCraft from 'vue-form-craft'
import { request } from '@/utils'

const app = createApp(App)

app.use(VueFormCraft, { request }) //传入你项目里的公共请求方法
app.mount('#app')

3、自增组件

收集一组格式一样的重复数据是表单经常遇到的场景,在 vue-form-craft 中可以轻松实现,
且支持多种展示格式

自增组件1.png

自增2.png

自增3.png

{
  "labelWidth": 150,
  "labelAlign": "right",
  "size": "default",
  "items": [
    {
      "label": "增添用户",
      "component": "FormList",
      "children": [
        {
          "label": "用户名",
          "component": "Input",
          "props": {
            "placeholder": "请输入文本"
          },
          "name": "username",
        },
        {
          "label": "密码",
          "component": "Password",
          "props": {
            "placeholder": "请输入密码"
          },
          "name": "password"
        },
        {
          "label": "设为管理员",
          "component": "Switch",
          "name": "vip",
          "props": {
            "inline-prompt": 0
          }
        }
      ],
      "props": {
        "mode": "table"
      },
      "designKey": "design-pMUa",
      "name": "users",
    }
  ]
}

4、深层数据绑定

在开发过程中,经常会遇到需要将前端数据转换为符合服务端数据结构的情况。

比如一张表单你收集到的可能是这样的数据:

转换前.png

而后端希望收到的是这样的数据

转换后.png

为了解决这个问题,name 字段扩展为魔法字段,既是唯一标识,也是数据路径,可以让你自由指定数据存储的层级。

比如name是【hostname】,数据就会保存为 { hostname: 'xxx' }

比如name是【flavor.cpu】,数据就会保存为 { flavor: { cpu:'xxx' } }

比如name是【flavor.memory】,数据就会保存为 { flavor: { memory:'xxx' } }

无论数据层级保存的多深,都能准确追踪,且能精准校验

5、组件自定义

vue-form-craft 提供了一些基础组件,例如 Input、Select 和 Radio 等,但有时候这些组件并不能完全符合我们的业务需求,此时可以考虑使用自定义组件(Custom)。

需要将你的组件注册为全局组件,并且能够接收v-model

{
      "label": "自定义组件",
      "component": "Custom",
      "props": {
        "componentName": "GridTable"
      },
      "designKey": "design-3J39",
      "name": "form-iOOm"
}

6、支持多种组件库

可能你并不喜欢element ui的组件风格,或者你项目里用的是其他ui库。那么你也可以选择vue-form-craft ,因为它提供了ui库定制功能

全局配置customElements可以用来定制所有内置组件,比如你想将内置组件替换成ant-design-vue风格,示例如下:

import { createApp } from 'vue'
import App from './App.vue'
import VueFormCraft from 'vue-form-craft'
import { request } from '@/utils'
import { Switch, Input, Textarea, InputNumber } from 'ant-design-vue'

const app = createApp(App)

app.use(VueFormCraft, { 
  request,
  customElements: {
      Input: {
        component: Input,
        modelName: 'value'
      },
      Switch: {
        component: Switch,
        modelName: 'checked'
      },
      Textarea: {
        component: Textarea,
        modelName: 'value'
      },
      InputNumber: {
        component: InputNumber,
        modelName: 'value'
      }
    }
})

app.mount('#app')

2024-02-27_103328.png

可能不同组件库的参数会不一样,比如el都是直接使用v-model:modelValue,而ant大部分都是v-model:value,所以提供了modelName来指定v-model的名字

而其他参数不一样的问题,可以选择二次封装将组件的props都封装符合el参数格式的组件,再传给customElements

也可以通过attrs来自行配置每个字段的字段配置,和JsonSchema的items配置一样,配置成符合对应组件库参数的attr表单

 Switch: {
        component: Switch,
        modelName: 'checked',
        attrs: [
          { label: '标签', component: 'Input', name: 'label' },
          {
            label: '唯一标识',
            component: 'Input',
            name: 'name',
            help: "既是唯一标识,也是数据路径。比如输入【props.name】,数据就会保存为 { props: { name:'xxx' } }"
          },
          { label: '字段说明', component: 'Textarea', name: 'help' },
          {
            label: '占位提示',
            component: 'Input',
            name: 'props.placeholder',
            designKey: 'form-ekRL'
          },
          { label: '初始值', component: 'Input', name: 'initialValue' },
          { label: '是否必填', component: 'Switch', name: 'required' },
          { label: '是否只读', component: 'Switch', name: 'props.readonly' },
          { label: '是否禁用', component: 'Switch', name: 'props.disabled' },
          { label: '隐藏字段', component: 'Switch', name: 'hidden' },
          { label: '隐藏标签', component: 'Switch', name: 'hideLabel' }
        ]
      }

七、写在最后

作为一款开箱即用的表单方案,vue-form-craft目标是大幅提高中后台系统中的表单开发效率,让你可以快速创建各种类型的表单,并省略从头编写表单组件的繁琐步骤。我将一直坚持这个初衷,并不断推进协议配置方面的创新和提升,努力提供更加完善的表单开发体验。

后续开发的目标期望:

  • 结合ts实现类型化支持
  • 国际化翻译
  • 结合 vue-form-craft 开发一套vue低代码平台

如果觉得 vue-form-craft 做的不错,或者本文对你有所帮助和启迪,可不可以顺手点个赞😁

如果项目对你有帮助,求个github star! 谢谢各位帅哥美女(❁´◡`❁)

github源码

;