@form-renderer/adapter-vue3
v1.0.0-alpha.12
Published
Vue3 adapter for FormEngine
Maintainers
Readme
FormAdapter
FormAdapter 是基于 FormEngine 的 Vue3 表单渲染适配器,提供了完整的表单渲染、交互和组件集成能力。它将 FormEngine 的声明式 Schema 转换为可视化的 Vue 组件,并提供响应式的数据绑定和事件处理。
项目结构
详见 项目结构文档。
核心特性
1. 响应式引擎集成
- Vue3 响应式系统深度集成
- 基于
shallowRef的性能优化 - 不可变数据流,确保追踪性
- 自动同步 FormEngine 和 Vue 状态
2. 组件注册系统
- 灵活的组件定义机制
- 支持单个注册、批量注册和预设注册
- 内置值转换器和事件映射
- 支持自定义渲染逻辑
3. 事件处理系统
- 统一的事件处理接口
- 可选的批量更新优化
- 完整的事件生命周期管理
- 错误处理和恢复机制
4. UI 框架集成
- 支持多种 UI 框架(Element Plus、Ant Design Vue 等)
- 自动转换校验规则(validators → rules)
- FormItem 自动包裹
- 主题和样式定制
5. 组合式 API
useFormAdapter- 编程式表单管理useFieldComponent- 字段组件开发- 完整的 TypeScript 类型支持
- 响应式状态和方法
6. 性能优化
- 智能批量更新调度
- 浅比较优化渲染
- 按需加载组件
- 结构共享机制
架构设计
FormAdapter 采用分层架构:
FormAdapter
├── FormAdapter.vue # 主组件(入口)
├── SchemaRenderer.vue # Schema 渲染器
├── Core 层 # 核心模块
│ ├── ReactiveEngine # 响应式引擎
│ ├── ComponentRegistry # 组件注册表
│ ├── EventHandler # 事件处理器
│ └── UpdateScheduler # 更新调度器
├── Composables 层 # 组合式函数
│ ├── useFormAdapter # 表单适配器
│ └── useFieldComponent # 字段组件
├── Components 层 # 容器组件
│ ├── FormContainer # 表单容器
│ ├── LayoutContainer # 布局容器
│ └── ListContainer # 列表容器
└── Presets 层 # 预设集成
├── ElementPlusPreset # Element Plus 预设
└── ExamplePreset # 示例预设设计原则
- 渐进增强 - 从基础功能到高级特性,按需使用
- 框架无关 - 核心逻辑与 UI 框架解耦
- 类型安全 - 完整的 TypeScript 类型定义
- 性能优先 - 批处理、浅比较、按需更新
- 开发友好 - 清晰的 API、完善的文档、丰富的示例
- 扩展性强 - 易于扩展新组件和预设
适用场景
- 复杂动态表单渲染
- 低代码/无代码平台
- 配置化表单系统
- 表单设计器/构建器
- 多 UI 框架适配
快速开始
安装
npm install @form-renderer/adapter-vue3 @form-renderer/engine vue基础使用
<template>
<FormAdapter
:schema="schema"
v-model:model="formData"
:components="ElementPlusPreset"
@submit="handleSubmit"
/>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { FormAdapter } from '@form-renderer/adapter-vue3'
import { ElementPlusPreset } from '@form-renderer/adapter-vue3/presets'
const schema = {
type: 'form',
properties: {
name: {
type: 'field',
component: 'Input',
required: true,
formItemProps: {
label: '姓名'
}
},
age: {
type: 'field',
component: 'InputNumber',
required: true,
formItemProps: {
label: '年龄'
}
}
}
}
const formData = ref({
name: '',
age: undefined
})
const handleSubmit = async (data) => {
console.log('提交数据:', data)
}
</script>使用组合式 API
import { useFormAdapter } from '@form-renderer/adapter-vue3'
import { ElementPlusPreset } from '@form-renderer/adapter-vue3/presets'
const {
renderSchema,
model,
updateValue,
validate,
submit,
reset
} = useFormAdapter({
schema: mySchema,
model: { name: '', age: undefined },
components: ElementPlusPreset,
onSubmit: async (data) => {
await api.submit(data)
}
})
// 更新值
updateValue('name', 'John')
// 校验表单
const result = await validate()
// 提交表单
await submit()
// 重置表单
reset()核心概念
1. ReactiveEngine - 响应式引擎
ReactiveEngine 是 FormAdapter 的核心,负责将 FormEngine 与 Vue3 响应式系统集成。
特性:
- 基于
shallowRef的浅响应式 - 自动同步 FormEngine 的状态变化
- 支持值变化和结构变化事件
- 可选的更新调度优化
使用:
import { createReactiveEngine } from '@form-renderer/adapter-vue3'
const engine = createReactiveEngine({
schema: mySchema,
model: myModel,
enableUpdateScheduler: true
})
// 获取响应式 renderSchema(只读)
const renderSchema = engine.getRenderSchema()
// 获取响应式 model(只读)
const model = engine.getModel()
// 更新值
engine.updateValue('name', 'John')
// 销毁引擎
engine.destroy()2. ComponentRegistry - 组件注册表
ComponentRegistry 管理所有可用的组件定义。
组件定义结构:
interface ComponentDefinition {
name: string // 组件名称
component: Component // Vue 组件
type: ComponentType // 组件类型
defaultProps?: Record<string, any>
valueTransformer?: ValueTransformer
eventMapping?: EventMapping
needFormItem?: boolean
customRender?: (props) => VNode
}使用:
import { createComponentRegistry } from '@form-renderer/adapter-vue3'
const registry = createComponentRegistry()
// 注册单个组件
registry.register({
name: 'Input',
component: ElInput,
type: 'field',
eventMapping: {
onChange: 'update:modelValue'
}
})
// 批量注册
registry.registerBatch([
{ name: 'Input', component: ElInput, type: 'field' },
{ name: 'Select', component: ElSelect, type: 'field' }
])
// 注册预设
registry.registerPreset(ElementPlusPreset)
// 获取组件
const inputDef = registry.get('Input')3. EventHandler - 事件处理器
EventHandler 负责处理所有用户交互事件。
事件类型:
- 字段值变化(change)
- 字段聚焦/失焦(focus/blur)
- 列表操作(add/remove/move)
使用:
import { createEventHandler } from '@form-renderer/adapter-vue3'
const handler = createEventHandler(engine, registry, {
enableBatch: true,
batchDelay: 16
})
// 处理字段变化
handler.handleFieldChange('name', 'John', 'Input')
// 处理列表添加
handler.handleListAdd('items', { name: '', price: 0 })
// 处理列表删除
handler.handleListRemove('items', 0)
// 立即刷新批量更新
handler.flush()事件处理层次:
FormAdapter 支持两种层次的事件处理:
核心事件(由 EventHandler 处理):
onChange: 字段值变化,会更新 modelonInput: 输入事件,只更新显示值onFocus: 字段聚焦onBlur: 字段失焦
字段级自定义事件(直接绑定到组件):
onKeydown、onKeyup: 键盘事件onClick、onMouseenter: 鼠标事件- 以及任何组件支持的原生事件
使用自定义事件:
const schema = {
fields: [
{
path: 'username',
component: 'input',
componentProps: {
// 在 componentProps 中定义事件处理器
onKeydown: (e: KeyboardEvent) => {
if (e.key === 'Enter') {
// 处理回车
}
},
onFocus: () => {
console.log('字段获得焦点')
}
}
}
]
}详见:自定义事件处理指南
4. 组件预设(ComponentPreset)
组件预设是一组预定义的组件配置,方便快速集成 UI 框架。
预设结构:
interface ComponentPreset {
name: string // 预设名称
components: ComponentDefinition[] // 组件列表
formItem?: Component // FormItem 组件
ruleConverter?: RuleConverter // 校验规则转换器
theme?: ThemeConfig // 主题配置
setup?: () => void // 初始化函数
}示例:
export const MyPreset: ComponentPreset = {
name: 'my-preset',
components: [
{
name: 'Input',
component: MyInput,
type: 'field',
eventMapping: {
onChange: 'update:modelValue'
}
}
],
formItem: MyFormItem,
ruleConverter: (node, computed, context) => {
const rules = []
if (computed.required) {
rules.push({ required: true, message: '必填' })
}
return rules
}
}组件开发
字段组件(Field Component)
字段组件是最基础的组件类型,用于渲染表单字段。
接口规范:
interface FieldComponentProps {
modelValue: any // v-model 绑定值
disabled?: boolean // 禁用状态
readonly?: boolean // 只读状态
placeholder?: string // 占位符
[key: string]: any // 其他属性
}
interface FieldComponentEmits {
'update:modelValue': (value: any) => void
focus?: (event: FocusEvent) => void
blur?: (event: FocusEvent) => void
change?: (value: any) => void
}示例:
<template>
<input
:value="modelValue"
:disabled="disabled"
:readonly="readonly"
:placeholder="placeholder"
@input="handleInput"
@focus="handleFocus"
@blur="handleBlur"
/>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
const props = defineProps<{
modelValue: any
disabled?: boolean
readonly?: boolean
placeholder?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: any]
focus: [event: FocusEvent]
blur: [event: FocusEvent]
}>()
const handleInput = (e: Event) => {
emit('update:modelValue', (e.target as HTMLInputElement).value)
}
const handleFocus = (e: FocusEvent) => {
emit('focus', e)
}
const handleBlur = (e: FocusEvent) => {
emit('blur', e)
}
</script>布局组件(Layout Component)
布局组件用于组织和排列表单字段。
<template>
<div class="layout-container">
<h3 v-if="title">{{ title }}</h3>
<slot />
</div>
</template>
<script setup lang="ts">
defineProps<{
title?: string
}>()
</script>列表组件(List Component)
列表组件用于渲染动态列表。
<template>
<div class="list-container">
<div v-for="(row, index) in rows" :key="index" class="list-item">
<slot :row="row" :index="index" />
<button @click="emit('remove', index)">删除</button>
</div>
<button @click="emit('add')">添加</button>
</div>
</template>
<script setup lang="ts">
defineProps<{
rows: any[]
}>()
const emit = defineEmits<{
add: []
remove: [index: number]
}>()
</script>值转换器(ValueTransformer)
值转换器用于在组件值和引擎值之间进行转换。
使用场景:
- 日期组件:
Date↔string - 数字组件:
number↔string - 多选组件:
string[]↔string(逗号分隔)
示例:
const dateTransformer: ValueTransformer = {
// 引擎值 → 组件值
toComponent: (engineValue: string) => {
return engineValue ? new Date(engineValue) : null
},
// 组件值 → 引擎值
fromComponent: (componentValue: Date | null) => {
return componentValue ? componentValue.toISOString() : ''
}
}
// 注册时使用
registry.register({
name: 'DatePicker',
component: ElDatePicker,
type: 'field',
valueTransformer: dateTransformer
})校验规则转换(RuleConverter)
RuleConverter 将 FormEngine 的 validators 转换为 UI 框架的 rules 格式。
示例(Element Plus):
const ruleConverter: RuleConverter = (node, computed, context) => {
const rules = []
// 必填校验
if (computed.required) {
rules.push({
required: true,
message: `${node.formItemProps?.label || '该字段'}为必填项`,
trigger: 'blur'
})
}
// 自定义校验器
if (node.validators && Array.isArray(node.validators)) {
node.validators.forEach((validator) => {
rules.push({
validator: async (rule, value, callback) => {
try {
const ctx = {
path: node.path,
getSchema: context.engine.getEngine().getSchema,
getValue: context.engine.getEngine().getValue,
getCurRowValue: () => ({}),
getCurRowIndex: () => -1
}
const result = await validator(value, ctx)
if (result === false || typeof result === 'string') {
callback(new Error(typeof result === 'string' ? result : '校验失败'))
} else {
callback()
}
} catch (error) {
callback(error)
}
},
trigger: 'blur'
})
})
}
return rules
}FormItem 集成
FormItem 组件用于包裹字段组件,提供标签、错误信息等。
要求:
- 接收
label、required、error等 props - 提供默认插槽用于渲染字段组件
- 支持校验规则(如果使用 ruleConverter)
示例(Element Plus):
<template>
<el-form-item
:label="label"
:required="required"
:prop="prop"
:rules="rules"
:error="error"
>
<slot />
</el-form-item>
</template>更新调度(UpdateScheduler)
UpdateScheduler 用于优化批量更新性能。
特性:
- 使用
requestAnimationFrame调度 - 合并多个连续更新
- 避免不必要的渲染
使用:
// 创建时启用
const engine = createReactiveEngine({
schema: mySchema,
model: myModel,
enableUpdateScheduler: true
})
// 多次更新会自动合并
engine.updateValue('name', 'John')
engine.updateValue('age', 25)
engine.updateValue('city', 'Beijing')
// ↑ 这三次更新会在下一帧合并为一次批量更新
// 立即刷新
engine.flush()性能优化
1. 使用 shallowRef
FormAdapter 使用 shallowRef 而非 ref,避免深度响应式的性能开销。
// ✅ 好
const renderSchema = shallowRef(engine.getRenderSchema())
// ❌ 差
const renderSchema = ref(engine.getRenderSchema())2. 启用更新调度
const engine = createReactiveEngine({
schema,
model,
enableUpdateScheduler: true // ✅ 启用批量更新
})3. 启用事件批处理
const handler = createEventHandler(engine, registry, {
enableBatch: true, // ✅ 启用批处理
batchDelay: 16
})4. 避免不必要的计算属性
// ✅ 好:浅比较
const renderSchema = computed(() => engine.getRenderSchema().value)
// ❌ 差:深比较
const renderSchema = computed(() => JSON.parse(JSON.stringify(engine.getRenderSchema().value)))5. 使用组件懒加载
const InputComponent = defineAsyncComponent(() => import('./MyInput.vue'))
registry.register({
name: 'Input',
component: InputComponent,
type: 'field'
})调试
启用日志
// 在 FormAdapter 组件中查看日志
console.log('RenderSchema:', renderSchema.value)
console.log('Model:', model.value)
console.log('Engine:', engine.value)使用 Vue Devtools
安装 Vue Devtools 浏览器扩展,可以查看:
- 组件树
- 响应式数据
- 事件触发
性能分析
// 使用 Vue 3 的性能 API
import { startMeasure, stopMeasure } from 'vue'
startMeasure('form-render')
// ... 渲染代码
stopMeasure('form-render')API 文档
详见 API 文档。
完整示例
动态表单
<template>
<FormAdapter
:schema="schema"
v-model:model="formData"
:components="ElementPlusPreset"
@change="handleChange"
@submit="handleSubmit"
ref="formRef"
/>
<div class="actions">
<button @click="handleValidate">校验</button>
<button @click="handleReset">重置</button>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import { FormAdapter } from '@form-renderer/adapter-vue3'
import { ElementPlusPreset } from '@form-renderer/adapter-vue3/presets'
const formRef = ref()
const schema = {
type: 'form',
properties: {
userType: {
type: 'field',
component: 'Select',
formItemProps: { label: '用户类型' },
componentProps: {
options: [
{ label: '个人', value: 'personal' },
{ label: '企业', value: 'company' }
]
}
},
personalInfo: {
type: 'layout',
ifShow: (ctx) => ctx.getValue('userType') === 'personal',
properties: {
name: {
type: 'field',
component: 'Input',
required: true,
formItemProps: { label: '姓名' }
},
idCard: {
type: 'field',
component: 'Input',
required: true,
formItemProps: { label: '身份证号' },
validators: [
(value) => {
if (value && value.length !== 18) {
return '身份证号必须是18位'
}
}
]
}
}
},
companyInfo: {
type: 'layout',
ifShow: (ctx) => ctx.getValue('userType') === 'company',
properties: {
companyName: {
type: 'field',
component: 'Input',
required: true,
formItemProps: { label: '企业名称' }
},
creditCode: {
type: 'field',
component: 'Input',
required: true,
formItemProps: { label: '统一社会信用代码' }
}
}
},
items: {
type: 'list',
items: {
name: {
type: 'field',
component: 'Input',
required: true,
formItemProps: { label: '商品名称' }
},
price: {
type: 'field',
component: 'InputNumber',
required: true,
formItemProps: { label: '价格' },
validators: [
(value) => {
if (value <= 0) return '价格必须大于0'
}
]
},
quantity: {
type: 'field',
component: 'InputNumber',
required: true,
formItemProps: { label: '数量' }
},
total: {
type: 'field',
component: 'InputNumber',
readonly: true,
formItemProps: { label: '小计' },
subscribes: {
'.price': (ctx) => {
const row = ctx.getCurRowValue()
ctx.updateSelf((row.price || 0) * (row.quantity || 0))
},
'.quantity': (ctx) => {
const row = ctx.getCurRowValue()
ctx.updateSelf((row.price || 0) * (row.quantity || 0))
}
}
}
}
}
}
}
const formData = ref({
userType: '',
items: []
})
const handleChange = (event) => {
console.log('值变化:', event)
}
const handleValidate = async () => {
const result = await formRef.value.validate()
console.log('校验结果:', result)
}
const handleSubmit = async (data) => {
console.log('提交数据:', data)
// 调用 API
await api.submitForm(data)
}
const handleReset = () => {
formRef.value.reset()
}
</script>常见问题
1. 为什么 v-model 不更新?
确保 FormAdapter 使用了 v-model:model 而不是 :model:
<!-- ✅ 正确 -->
<FormAdapter v-model:model="formData" :schema="schema" />
<!-- ❌ 错误 -->
<FormAdapter :model="formData" :schema="schema" />2. 组件找不到?
确保组件已注册:
// 检查组件是否已注册
const registry = formRef.value.getRegistry()
console.log(registry.has('Input')) // 应该返回 true3. 校验不生效?
检查以下几点:
- 是否正确配置了
required或validators - 是否使用了
ruleConverter(UI 框架校验) - 字段是否被隐藏(
ifShow: false)或禁用(disabled: true)
4. 性能问题?
尝试以下优化:
- 启用
enableUpdateScheduler - 启用
enableBatch - 使用组件懒加载
- 减少深度响应式
5. 如何调试事件?
在 FormAdapter 组件上监听所有事件:
<FormAdapter
@change="console.log('change', $event)"
@field-blur="console.log('blur', $event)"
@field-focus="console.log('focus', $event)"
@list-change="console.log('list', $event)"
@validate="console.log('validate', $event)"
/>最佳实践
1. 组件注册
推荐:使用预设
// ✅ 好
import { ElementPlusPreset } from '@form-renderer/adapter-vue3/presets'
<FormAdapter :components="ElementPlusPreset" />不推荐:手动注册每个组件
// ❌ 繁琐
const components = [
{ name: 'Input', component: ElInput, type: 'field' },
{ name: 'Select', component: ElSelect, type: 'field' },
// ... 很多组件
]2. 表单数据管理
推荐:使用 ref
// ✅ 好
const formData = ref({ name: '', age: 0 })
<FormAdapter v-model:model="formData" />不推荐:使用 reactive
// ⚠️ 可能导致类型问题
const formData = reactive({ name: '', age: 0 })3. 事件处理
推荐:使用具名回调
<template>
<FormAdapter
@change="handleChange"
@submit="handleSubmit"
/>
</template>
<script setup>
const handleChange = (event) => {
// 处理变化
}
const handleSubmit = async (data) => {
// 提交逻辑
}
</script>4. Schema 定义
推荐:提取为单独的文件
// schemas/user-form.ts
export const userFormSchema = {
type: 'form',
properties: {
// ...
}
}
// 使用
import { userFormSchema } from './schemas/user-form'迁移指南
从原生表单迁移
<!-- 原生表单 -->
<form @submit="handleSubmit">
<div>
<label>姓名</label>
<input v-model="formData.name" required />
</div>
<div>
<label>年龄</label>
<input v-model.number="formData.age" type="number" required />
</div>
<button type="submit">提交</button>
</form>
<!-- 迁移到 FormAdapter -->
<FormAdapter
:schema="{
type: 'form',
properties: {
name: {
type: 'field',
component: 'Input',
required: true,
formItemProps: { label: '姓名' }
},
age: {
type: 'field',
component: 'InputNumber',
required: true,
formItemProps: { label: '年龄' }
}
}
}"
v-model:model="formData"
@submit="handleSubmit"
/>扩展阅读
License
MIT
