Skip to content

XSchemaForm 组件详细设计文档

引言

因为 @formily/core 作为一个独立的包而存在,它的核心意义是将领域模型从 UI 框架中抽离出来,同时给开发者带来了以下两个直观收益:

  1. 可以方便 formily 开发者从 UI 与逻辑的耦合关系中释放出来,提升代码可维护性;

  2. 可以让 formily 拥有跨终端,跨框架的能力,不管你是 React 用户,Vue 用户还是 Angular 用户,都能享受到 formily 的领域模型带来的提效。

超高性能

借助 @formily/reactive,@formily/core 天然获得了依赖追踪,高效更新,按需渲染的能力,不管是在 React 下,还是 Vue/Angular 下,不管是字段频繁输入,还是字段联动,都能给用户带来 O(1)的性能体验,开发者无需关心性能优化的事情,只需要专注于业务逻辑实现即可。

领域模型

如果把表单问题做分解,其实我们可以分解出:

  1. 数据管理问题

  2. 字段管理问题

  3. 校验管理问题

  4. 联动管理问题

这几个方向的问题其实都可以作为领域级问题去解决,每一个领域问题,其实都是非常复杂的问题,在 Formily 中,全部一一给您突破解决了,所以您只需要专注于业务逻辑即可。

Formily 内核架构非常复杂,因为要解决一个领域级的问题,而不是单点具体的问题,架构图示意:

image.png

组件概述

XSchemaForm 是一个基于 Vue 3 和 Formily 框架开发的表单组件,它封装了 @formily/element-plus 中的表单相关组件,为开发者提供了一个可复用、可定制的表单解决方案。该组件支持自定义表单组件、自动聚焦输入框、通过插槽自定义表单内容等功能,并且在按下回车键时可以触发提交事件。

功能需求

  1. 表单属性配置:支持通过 formProps 和 form 属性自定义表单的布局、标签显示方式等。

  2. 自动聚焦:通过 needFocus 属性控制表单在挂载后是否自动聚焦到第一个输入框。

  3. 提交事件:按下回车键或点击提交按钮时,触发 enter 事件。

  4. 组件扩展:支持注册自定义组件,以满足不同的表单需求。

组件设计

组件结构

文件路径功能描述
schema-form/src/schema-form.vue组件的模板和逻辑实现,包含表单的渲染和事件处理。
schema-form/src/schema-form.ts定义组件的 props 类型和默认值。
schema-form/index.ts导出组件并进行安装处理,方便在项目中使用。
schema-form/src/useSchema.ts提供组件状态管理和自定义组件注册功能。

组件关系

  • XSchemaForm 组件依赖于 @formily/element-plus 提供的表单组件,如 FormFormButtonGroup 和 Submit

  • useSchema 函数用于管理组件状态和注册自定义组件,为 XSchemaForm 提供了扩展能力。

组件属性

属性名类型默认值描述
formPropsFormLayout{ wrapperWrap: false, labelWrap: true, layout: 'vertical' }表单的属性配置,用于控制表单的布局和显示方式。
formObject{}表单对象,可用于传递表单数据和验证规则。
needFocusBooleanfalse控制表单在挂载后是否自动聚焦到第一个输入框。
schemaISchema{}表单配置项
iptWidthString'240px'表单组件在 inline模式下的宽度

FormLayout 属性

属性名类型默认值描述
styleCSSProperties-样式
classNamestring-类名
colonbooleantrue是否有冒号
labelAlign'right' | 'left' | ('right' | 'left')[]-标签内容对齐
wrapperAlign'right' | 'left' | ('right' | 'left')[]-组件容器内容对齐
labelWrapbooleanfalse标签内容换行
labelWidthnumber-标签宽度(px)
wrapperWidthnumber-组件容器宽度(px)
wrapperWrapbooleanfalse组件容器换行
labelColnumber | number[]-标签宽度(24 column)
wrapperColnumber | number[]-组件容器宽度(24 column)
fullnessbooleanfalse组件容器宽度 100%
size'small' | 'default' | 'large'default组件尺寸
layout'vertical' | 'horizontal' | 'inline' | ('vertical' | 'horizontal' | 'inline')[]horizontal布局模式
direction'rtl' | 'ltr'ltr方向(暂不支持)
insetbooleanfalse内联布局
shallowbooleantrue上下文浅层传递
feedbackLayout'loose' | 'terse' | 'popover' | 'none'true反馈布局
tooltipLayout'icon' | 'text''icon'问提示布局
borderedbooleantrue是否有边框
breakpointsnumber[]-容器尺寸断点
gridColumnGapnumber8网格布局列间距
gridRowGapnumber4网格布局行间距
spaceGapnumber8弹性间距

ISchema 属性

属性类型字段模型映射描述
typeSchemaTypesGeneralField(opens new window)类型
titleReact.ReactNodetitle标题
descriptionReact.ReactNodedescription描述
defaultAnyinitialValue默认值
readOnlyBooleanreadOnly是否只读
writeOnlyBooleaneditable是否只写
enumSchemaEnumdataSource枚举
constAnyvalidator校验字段值是否与 const 的值相等
multipleOfNumbervalidator校验字段值是否可被 multipleOf 的值整除
maximumNumbervalidator校验最大值(大于)
exclusiveMaximumNumbervalidator校验最大值(大于等于
minimumNumbervalidator校验最小值(小于)
exclusiveMinimumNumbervalidator最小值(小于等于)
maxLengthNumbervalidator校验最大长度
minLengthNumbervalidator校验最小长度
patternRegExpStringvalidator正则校验规则
maxItemsNumbervalidator最大条目数
minItemsNumbervalidator最小条目数
uniqueItemsBooleanvalidator是否校验重复
maxPropertiesNumbervalidator最大属性数量
minPropertiesNumbervalidator最小属性数量
requiredBooleanvalidator必填
formatValidatorFormats(opens new window)validator正则校验格式
propertiesSchemaProperties-属性描述
itemsSchemaItems-数组描述
additionalItemsSchema-额外数组元素描述
patternPropertiesSchemaProperties-动态匹配对象的某个属性的 Schema
additionalPropertiesSchema-匹配对象额外属性的 Schema
x-indexNumber-UI 展示顺序
x-patternFieldPatternTypes(opens new window)patternUI 交互模式
x-displayFieldDisplayTypes(opens new window)displayUI 展示
x-validatorFieldValidator(opens new window)validator字段校验器
x-decoratorString | React.FCdecorator字段 UI 包装器组件
x-decorator-propsAnydecorator字段 UI 包装器组件属性
x-componentString | React.FCcomponent字段 UI 组件
x-component-propsAnycomponent字段 UI 组件属性
x-reactionsSchemaReactionsreactions字段联动协议
x-contentReact.ReactNodeReactChildren字段内容,用来传入某个组件的子节点
x-visibleBooleanvisible字段显示隐藏
x-hiddenBooleanhidden字段 UI 隐藏(保留数据)
x-disabledBooleandisabled字段禁用
x-editableBooleaneditable字段可编辑
x-read-onlyBooleanreadOnly字段只读
x-read-prettyBooleanreadPretty字段阅读态
definitionsSchemaProperties-Schema 预定义
$refString-从 Schema 预定义中读取 Schema 并合并至当前 Schema
x-dataObjectdata扩展属性

组件事件

事件名参数描述
enterparams(可选)按下回车键或点击提交按钮时触发的事件。

组件插槽

插槽名描述
默认插槽用于自定义表单内容,开发者可以在插槽中添加自定义的表单字段。

详细设计

**schema-form** 组件

  1. 模板部分

使用 Form 组件渲染表单,包含一个隐藏的提交按钮,用于支持回车键提交表单。

vue
<template>
  <div ref="schemaFormRef" :class="[n()]">
    <Form v-bind="formProps" :form="form" @auto-submit="onSubmit">
      <slot />
      <!-- enter 键 提交必须的条件, css隐藏 -->
      <FormButtonGroup align-form-item class="schema-form-box-btn">
        <Submit>  </Submit>
      </FormButtonGroup>
    </Form>
  </div>
</template>
  1. 逻辑部分

在 onMounted 钩子中,如果 needFocus 为 true,则自动聚焦到第一个输入框。

typescript
<script lang="ts" setup>
import { onMounted, ref,nextTick } from 'vue'
import { createNamespace } from '@eco-library/utils'
import { Form, FormButtonGroup, Submit } from '@formily/element-plus'
import { SchemaFormProps } from './schema-form'

interface EmitEvent {
  (e: 'enter', params?: any): void
}
const emit = defineEmits<EmitEvent>()

defineOptions({
  name: 'XSchemaForm'
})
const schemaFormRef = ref()

const props = defineProps(SchemaFormProps)

function onSubmit() {
  emit('enter')
}

const { n, classes } = createNamespace('schema-form')

onMounted(() => {
  if (props.needFocus) {
    // 页面渲染完,寻找第一个输入框将其置为 focus
    nextTick(() => {
      schemaFormRef.value?.querySelector('input').focus()
    })
  }
})
</script>
  1. 样式部分

定义了表单的样式,包括隐藏提交按钮、表单布局和输入框样式等。

less
.x-schema-form {
  .schema-form-box-btn {
    height: 0;
    width: 0;
    position: absolute;
    z-index: -9999;
    opacity: 0;
    left: -9999px;
    bottom: -9999px;
  }
  &.filter-form {
    box-sizing: border-box;
    border-radius: 8px 8px 0 0;
    padding: 24px 24px 12px;
    width: 100%;
    background-color: #ffffff;
  }

  .formily-element-plus-form-item-label {
    font-weight: 500;
    label {
      color: #222222;
    }
  }

  .formily-element-plus-form-inline {
    .formily-element-plus-form-item {
      margin-right: 12px;
      .el-select-v2,
      .el-select,
      .el-cascader,
      .sc-input-v2 {
        width: v-bind('props.iptWidth'); // 绑定输入值

        .el-select__tags-text {
          max-width: 80px !important; // select 多选情况下 tag的最大长度,保证select样式正常显示
        }
      }

      .el-date-editor--daterange {
        width: 280px; // 日期区间选择框,最小宽度
      }

      .el-date-editor--datetimerange {
        width: 360px;  // 日期带时间区间选择框,最小宽度
      }
    }
  }

  .formily-element-plus-form-item-error-help {
    font-size: 12px;
    color: #f14846;
  }
}
  1. Props定义
typescript
export const SchemaFormProps = {
  /* 表单布局:表单的配置属性,包括包装器是否换行、标签是否换行和布局方式等 */
  formProps: {
    type: Object,
    default: () => ({
      wrapperWrap: false,
      labelWrap: true,
      layout: "vertical"
    })
  },
  /* 表单实例,能够通过它对表单进行一系列操作 */
  form: {
    type: Object,
    default: () => ({})
  },
  /* 是否需要进行首次 focus */
  needFocus: {
    type: Boolean,
    default: false
  },
  /* inline 状态下的表单长度*/
  iptWidth: {
    type: String,
    default: "240px"
  }
};

**useSchema** 函数

useSchema 函数用于管理表单组件库和自定义组件,提供组件状态管理和自定义组件注册的功能。

  1. 组件状态管理

在 useSchema.ts 文件中,使用 reactive 创建了两个响应式对象:

publicComponentStore:用于存储公共组件,全局可用。

customComponentStore:用于存储应用内部组件,相同命名的内部组件会覆盖公共组件。

typescript
// 组件状态管理
const publicComponentStore = reactive<SchemaVueComponents>({
  // 布局组件
  FormItem,
  FormLayout,
  FormGrid,
  Space,
  // 表单组件,样式不易变
  Radio,
  Checkbox,
  Switch,
  // 预览组件
  PreviewText
})

export enum FormilyComponentType {
  PUBLIC, // 公共库
  CUSTOM, // 私有库
}

// formily UI桥阶层与 UI组件库绑定,并抛出 Schmea 实例
export function useSchema() {
  // 应用内部组件,不污染全局,相同命名的,内部组件会覆盖公共组件
  const customComponentStore = reactive<SchemaVueComponents>({})

  // 桥阶层与UI组件库融合形成新组件供应用使用
  const FormilySchema = computed(() => {
    return createSchemaField({
      components: { ...publicComponentStore, ...customComponentStore }
    })
  })

  /**
   * 注册自定义组件到组件库
   * @param name
   * @param component
   * @param type
   */
  function setFormilyComponent(
    name: string,
    component: VueComponent,
    type: FormilyComponentType = FormilyComponentType.PUBLIC
  ) {
    switch (type) {
      case FormilyComponentType.PUBLIC:
        publicComponentStore[name] = component
        break
      case FormilyComponentType.CUSTOM:
      default:
        customComponentStore[name] = component
    }
  }

  return {
    FormilySchema,
    setFormilyComponent
  }
}
  1. 自定义组件注册

setFormilyComponent 函数用于注册自定义组件,支持将组件注册为公共组件或应用内部组件。

  1. 组件融合

FormilySchema 是一个计算属性,将 publicComponentStore 和 customComponentStore 中的组件合并,生成新的 SchemaField 实例供应用使用。

入口文件

typescript
export * from './src/schema-form'

export * from './src/useSchema'
export * from '@formily/vue'
export { onFieldValueChange } from '@formily/core'
export * from '@formily/reactive-vue'
export * from '@formily/shared'

import { withInstall } from '@eco-library/utils'
import SchemaForm from './src/schema-form.vue'

export const XSchemaForm = withInstall(SchemaForm)

export default XSchemaForm

该文件作为组件的入口,导出了 schema-form 和 useSchema 相关的内容,并将 SchemaForm 组件进行了安装处理,方便在项目中使用。

自定义组件

1. 实现业务自定义组件主要是使用@formily/react 或@formily/vue中的 Hooks API 与 observer API

2. 接入现成组件库的话,我们主要使用 connect/mapProps/mapReadPretty API

3. 如果想要实现一些更复杂的自定义组件,我们强烈推荐直接看@formily/antd或 @formily/next的源码

**核心概念与原理:**Formily 通过 连接器(Connect) 和 属性映射(mapProps) 机制,将普通组件转换为表单组件:

  • 连接器(Connect):将组件与 Formily 表单系统连接

  • 属性映射(mapProps):将 Formily 标准属性(如 value)映射到组件实际属性(如 modelValue

  • 预览态(mapReadPretty):定义组件在只读模式下的显示方式

自定义表单组件

  1. 创建基础组件

首先需要创建一个普通的 Vue组件,例如 ScInputView.vue

vue
<template>
  <el-input v-model="value" v-bind="$attr" />
</template>
<script lang="ts" setup>
import { defineModel } from 'vue'
// 通过 defineModel 实现双向绑定  
const value = defineModel<string | undefined>()
// 提供 更新函数
defineEmits(['update:modelValue'])
</script>
  1. 使用 **connect** 函数封装组件

创建 view.tsx 文件,将基础组件转换为 Formily 表单组件:

tsx
import ScInputView from './ScInputView.vue'
import { connect, mapProps, mapReadPretty } from '@eco-library/core'
import { PreviewText } from '../model/preview'

export const FormilyScInput = connect(
  ScInputView, // 基础组件
  mapProps(
    { value: 'modelValue' }, // 将 Formily 的 value 映射到组件的 modelValue
    (props: any) => {
      // 额外属性转换:withCount 转换为 showWordLimit
      const { withCount } = props
      return {
        ...props,
        showWordLimit: withCount,
      }
    }
  ),
  mapReadPretty(PreviewText.Input) // 预览态组件
)
  1. 导出组件

创建 index.ts 文件,统一导出组件:

typescript
export { FormilyScInput } from './view'
  1. 注册组件

在应用中注册自定义组件:

typescript
import { useSchema } from '@eco-library/core'
import { FormilyScInput } from './components/ScInput'

const { setFormilyComponent } = useSchema()

// 默认存储到公共组件库(FormilyComponentType.PUBLIC)中, 可在函数第三个参数中传入 FormilyComponentType.CUSTOM 存储到私有库中
setFormilyComponent('FormilyScInput', FormilyScInput)
  1. 在 Schema 中使用
typescript
{
  "type": "object",
  "properties": {
    "username": {
      "type": "string",
      "title": "用户名",
      "x-decorator": "FormItem", // 容器组件
      "x-component": "FormilyScInput", // 表单组件
      "x-component-props": {
        "withCount": true, // 会被映射为 showWordLimit
        // ...
        // 等同于 el-input 的属性
        "maxlength": 20
      }
    }
  }
}

自定义容器组件

容器组件是表单系统中的高级组件,用于组织和布局多个表单字段组件。与普通表单组件的主要区别在于:

  • 递归渲染子组件:通过 RecursionField 组件渲染所有子 Schema

  • 子组件管理:使用 useFieldSchema 获取和处理子组件

  • 布局控制:定义内部表单组件的排版规则和样式

  1. 创建基础容器组件
vue
<template>
  <section class="form-container">
    <div class="container-header">
      <h3>{{ title }}</h3>
      <p v-if="description">{{ description }}</p>
    </div>
    <div class="container-body">
      <!-- 子组件将在这里渲染 -->
    </div>
  </section>
</template>

<script lang="ts" setup>
defineProps({
  title: String,
  description: String,
})
</script>
  1. 集成 Formily 容器逻辑
vue
<template>
  <section class="form-container">
    <div class="container-header">
      <h3>{{ title }}</h3>
      <p v-if="description">{{ description }}</p>
    </div>
    <div class="container-body">
      <!-- 递归渲染所有子组件 -->
      <RecursionField 
        v-for="item in childrenSchema" 
        :key="item.id" 
        :schema="item.schema" 
        :name="item.id" 
      />
    </div>
  </section>
</template>

<script lang="ts" setup>
import { useFieldSchema, RecursionField } from '@eco-library/core'
import { reactive } from 'vue'

const props = defineProps({
  title: String,
  description: String,
})

// 获取子组件 Schema
const schema = useFieldSchema()
const childrenSchema = reactive<any[]>([])

// 处理子组件 Schema
schema.value.mapProperties((schema, name) => {
  childrenSchema.push({
    id: name,
    schema,
  })
})
</script>
  1. 实现子组件分类和布局

如果需要区分不同类型的子组件(如操作按钮和普通字段),可以添加分类逻辑:

typescript
const useChildrenSchema = () => {
  const schema = useFieldSchema()
  const childrenSchema = []
  const actionSchema = []
  
  schema.value.mapProperties((schema, name) => {
    if (props.isActionRender?.includes(name)) {
      // 分类为操作按钮
      actionSchema.push({ id: name, schema })
    } else {
      // 分类为普通字段
      childrenSchema.push({ id: name, schema })
    }
  })
  
  return {
    childrenSchema,
    actionSchema,
  }
}

const { childrenSchema, actionSchema } = useChildrenSchema()
  1. 处理表单状态与交互

添加对表单状态的响应逻辑,如根据表单编辑状态显示 / 隐藏容器:

typescript
import { useForm } from '@eco-library/core'

const form = useForm()
const containerVisible = ref(true)

// 监听表单状态变化
form.value.subscribe(({ type }) => {
  if (type === 'onFieldValueChange' && props.controlKey) {
    const fieldValue = form.value.getFieldValue(props.controlKey)
    containerVisible.value = !!fieldValue
  }
})
  1. 样式设计与布局

根据需求设计容器的样式和内部组件的排版规则:

  • css样式

  • 布局组件

使用示例

  1. 安装插件
shell
# 首先查看 npm 源是不是在 http://192.168.0.248:4873/ (内网环境)
# 不是的话需要切换源

pnpm install @eco-library/core --registry=http://192.168.0.248:4873/

#或者在 .npmrc 中添加
# @eco-library/core="http://192.168.0.248:4873/"
  1. 页面使用
vue
<template>
  <x-schema-form :form="editForm" @enter="handleConfirm">
    <component :is="FormilySchema.SchemaField" :schema="formSchema" />
  </x-schema-form>
</template>

<script lang="ts" steup>
  import { reactive } from 'vue'
  import { XSchemaForm, createForm, ISchema, useSchema } from '@eco-library/core';
  // 创建表单实例
  const editForm = createForm() 
  // 引入UI桥接后的 formily 实例组件 (SchemaField 是使用 jsonSchema 模式构建表单)
  const { FormilySchema } = useSchema
  const formSchema = reactive<ISchema>({
    type: 'object',
    properties: {
      // ... 配置表单项或布局容器
    }
  })

  function handleConfirm() {
    // 表单 enter 提交
  }
</script>

项目实践

设备调测(乐充运维平台)

  1. UI

新建模版.png

编辑模版.png

编辑模版 (1).png

  1. 测试报告

附件一:移动运维专项测试报告-批准.pdf

设备静态属性配置(乐充运维平台)

  1. UI

image.png

image.png

image.png

  1. 测试报告

附件二:新增录入设备基础静态属性测试报告-批准.pdf

注意事项

  1. 注册自定义组件时,确保组件名称的唯一性,避免与现有组件名称冲突。

  2. 相同命名的自定义组件会覆盖公共组件,因此在使用时需要注意组件的优先级。

  3. FormilySchema 是一个计算属性,会根据 publicComponentStore 和 customComponentStore 的变化自动更新。

总结

XSchemaForm 组件通过结合 Formily 库,提供了强大的表单开发能力。schema-form 组件实现了基本的表单功能,useSchema 函数则提供了组件状态管理和自定义组件注册的功能,使得表单开发更加灵活和可扩展。

仅供内部学习使用