设计表单引擎系统
问题
如何设计一个 Schema 驱动的表单引擎系统?从 Schema 协议设计、字段类型系统、校验引擎、联动引擎到可视化设计器,请详细说明核心模块的设计思路与关键技术实现。
答案
表单引擎是中后台应用中最核心的基础设施之一,其目标是通过 JSON Schema 配置驱动表单的渲染、校验和交互,实现"一次配置,多端运行"。一个完整的表单引擎需要解决 Schema 协议设计、字段注册与渲染、校验规则执行、联动逻辑编排、布局排列、数据收集与回填等多个关键问题。好的表单引擎能将表单开发效率提升数倍,同时保持足够的扩展性来应对复杂业务场景。
一、需求分析
功能需求
| 模块 | 功能点 |
|---|---|
| Schema 配置 | JSON Schema 定义表单结构、字段属性、校验规则、联动规则 |
| 字段渲染 | 支持 Input、Select、DatePicker、Upload 等内置字段,可注册自定义字段 |
| 表单校验 | 内置规则(required/min/max/pattern)、自定义校验、异步校验、跨字段校验 |
| 字段联动 | 显隐联动、值联动、选项联动、支持表达式引擎 |
| 布局系统 | 水平/垂直/栅格布局、分组(Fieldset)、分步表单(Steps) |
| 数据管理 | 值收集、初始值填充、重置、脏检查、提交 |
| 表单设计器 | 可视化拖拽配置、实时预览、导出 JSON Schema |
| 扩展能力 | 自定义字段组件、自定义校验规则、插件机制 |
非功能需求
| 指标 | 目标 |
|---|---|
| 性能 | 100+ 字段表单渲染 < 200ms,单字段变更不触发全量重渲染 |
| 体积 | 核心包 < 15KB gzipped,字段组件按需加载 |
| 扩展性 | 自定义组件接入 < 30 分钟,不侵入引擎核心 |
| 兼容性 | 支持 React / Vue 等主流框架,Schema 协议框架无关 |
| 可维护性 | Schema 可序列化、可版本化、可 Diff |
表单引擎的本质是一个 Schema 到 UI 的映射系统:输入是一份描述表单结构和行为的 JSON,输出是一个具有完整交互能力的表单 UI。Schema 是引擎的"灵魂",所有功能都围绕 Schema 展开。
二、整体架构
架构分层说明
| 层级 | 职责 | 关键模块 |
|---|---|---|
| 配置层 | 解析和标准化 JSON Schema | Schema Parser、Schema Validator |
| 核心层 | 引擎逻辑,框架无关 | 校验引擎、联动引擎、数据管理、字段注册 |
| 渲染层 | 将核心层的状态映射为具体 UI | FormRenderer、FieldRenderer、LayoutRenderer |
| 设计器 | 可视化配置 Schema | 拖拽面板、属性面板、Schema 导出 |
分层架构的核心优势是核心层框架无关。校验引擎、联动引擎、数据管理都是纯逻辑层,可以同时为 React 和 Vue 的渲染层提供服务。这也是 Formily 等开源方案采用的设计策略。
三、Schema 协议设计
Schema 协议是表单引擎的基石,它定义了表单的结构、字段属性、校验规则和联动规则。
3.1 Schema 类型定义
/** 字段类型枚举 */
type FieldType =
| 'input'
| 'textarea'
| 'number'
| 'select'
| 'radio'
| 'checkbox'
| 'switch'
| 'datePicker'
| 'upload'
| 'cascader'
| 'custom'; // 自定义字段
/** 校验规则 */
interface ValidationRule {
/** 规则类型 */
type: 'required' | 'min' | 'max' | 'minLength' | 'maxLength'
| 'pattern' | 'email' | 'url' | 'custom' | 'async';
/** 规则值 */
value?: unknown;
/** 错误提示 */
message: string;
/** 自定义校验函数名(需预注册) */
validator?: string;
/** 触发时机 */
trigger?: 'change' | 'blur' | 'submit';
}
/** 联动规则 */
interface LinkageRule {
/** 触发条件 —— 表达式字符串 */
condition: string; // 例如: "{{status}} === 'other'"
/** 联动效果 */
effect: {
/** 目标字段路径 */
target: string;
/** 动作类型 */
action: 'show' | 'hide' | 'setValue' | 'setOptions'
| 'enable' | 'disable' | 'setRequired';
/** 动作参数 */
payload?: unknown;
};
}
/** 核心:字段 Schema 定义 */
interface FieldSchema {
/** 字段唯一标识(对应数据路径) */
name: string;
/** 字段类型 */
type: FieldType;
/** 显示标签 */
label: string;
/** 默认值 */
defaultValue?: unknown;
/** 占位文本 */
placeholder?: string;
/** 是否隐藏 */
hidden?: boolean;
/** 是否禁用 */
disabled?: boolean;
/** 是否只读 */
readOnly?: boolean;
/** 字段描述/提示 */
description?: string;
/** 组件特有属性 */
componentProps?: Record<string, unknown>;
/** 校验规则 */
rules?: ValidationRule[];
/** 联动规则 */
linkages?: LinkageRule[];
/** 布局属性 */
layout?: {
span?: number; // 栅格占比 (1-24)
labelWidth?: number; // 标签宽度
labelAlign?: 'left' | 'right' | 'top';
};
/** 嵌套子字段(用于 Object/Array 类型) */
children?: FieldSchema[];
}
/** 表单 Schema 顶层定义 */
interface FormSchema {
/** Schema 版本号 */
version: string;
/** 表单标题 */
title?: string;
/** 全局布局模式 */
layout: 'horizontal' | 'vertical' | 'grid';
/** 栅格列数 */
columns?: number;
/** 全局标签宽度 */
labelWidth?: number;
/** 字段列表 */
fields: FieldSchema[];
/** 全局联动规则 */
linkages?: LinkageRule[];
}
3.2 Schema 实例
{
"version": "1.0.0",
"title": "用户注册",
"layout": "vertical",
"columns": 2,
"fields": [
{
"name": "username",
"type": "input",
"label": "用户名",
"placeholder": "请输入用户名",
"rules": [
{ "type": "required", "message": "用户名不能为空" },
{ "type": "minLength", "value": 3, "message": "至少 3 个字符" },
{ "type": "maxLength", "value": 20, "message": "最多 20 个字符" }
],
"layout": { "span": 12 }
},
{
"name": "email",
"type": "input",
"label": "邮箱",
"rules": [
{ "type": "required", "message": "邮箱不能为空" },
{ "type": "email", "message": "邮箱格式不正确" },
{
"type": "async",
"validator": "checkEmailUnique",
"message": "该邮箱已被注册",
"trigger": "blur"
}
],
"layout": { "span": 12 }
},
{
"name": "role",
"type": "select",
"label": "角色",
"componentProps": {
"options": [
{ "label": "普通用户", "value": "user" },
{ "label": "管理员", "value": "admin" },
{ "label": "其他", "value": "other" }
]
},
"rules": [{ "type": "required", "message": "请选择角色" }]
},
{
"name": "customRole",
"type": "input",
"label": "自定义角色",
"hidden": true,
"linkages": [
{
"condition": "{{role}} === 'other'",
"effect": {
"target": "customRole",
"action": "show"
}
},
{
"condition": "{{role}} === 'other'",
"effect": {
"target": "customRole",
"action": "setRequired"
}
}
]
}
]
}
- 字段
name支持路径语法:如address.city、contacts[0].name,用于嵌套数据结构 - 联动条件使用模板表达式:
{{fieldName}}引用字段值,避免直接写 JavaScript 代码(安全风险) - Schema 版本号必须携带:用于兼容性处理和数据迁移
四、核心模块设计
4.1 字段注册中心
字段注册中心管理所有可用的字段组件,支持内置字段和自定义字段的统一注册与获取。
import type { ComponentType } from 'react';
/** 字段组件的 Props 规范 */
interface FieldComponentProps<T = unknown> {
value: T;
onChange: (value: T) => void;
disabled?: boolean;
readOnly?: boolean;
placeholder?: string;
/** 字段级别的错误信息 */
error?: string;
/** 来自 Schema 的 componentProps */
[key: string]: unknown;
}
/** 字段注册项 */
interface FieldRegistryItem {
/** 字段组件 */
component: ComponentType<FieldComponentProps>;
/** 默认值工厂 */
getDefaultValue: () => unknown;
/** 值序列化(用于提交) */
serialize?: (value: unknown) => unknown;
/** 值反序列化(用于回填) */
deserialize?: (value: unknown) => unknown;
}
class FieldRegistry {
private registry = new Map<string, FieldRegistryItem>();
/** 注册字段类型 */
register(type: string, item: FieldRegistryItem): void {
if (this.registry.has(type)) {
console.warn(`Field type "${type}" is already registered, will be overwritten.`);
}
this.registry.set(type, item);
}
/** 批量注册 */
registerMany(items: Record<string, FieldRegistryItem>): void {
Object.entries(items).forEach(([type, item]) => {
this.register(type, item);
});
}
/** 获取字段组件 */
get(type: string): FieldRegistryItem | undefined {
return this.registry.get(type);
}
/** 获取所有已注册类型(用于设计器) */
getRegisteredTypes(): string[] {
return Array.from(this.registry.keys());
}
}
/** 全局单例 */
const fieldRegistry = new FieldRegistry();
// 注册内置字段
fieldRegistry.registerMany({
input: {
component: InputField,
getDefaultValue: () => '',
},
select: {
component: SelectField,
getDefaultValue: () => undefined,
},
datePicker: {
component: DatePickerField,
getDefaultValue: () => null,
serialize: (value: unknown) =>
value instanceof Date ? (value as Date).toISOString() : value,
deserialize: (value: unknown) =>
typeof value === 'string' ? new Date(value) : value,
},
upload: {
component: UploadField,
getDefaultValue: () => [],
},
});
export { fieldRegistry, FieldRegistry };
export type { FieldComponentProps, FieldRegistryItem };
开发者只需调用 fieldRegistry.register('myField', { component, getDefaultValue }) 即可接入自定义字段,无需修改引擎源码。这是开放-封闭原则的典型应用。
4.2 数据管理
数据管理模块负责表单值的存储、收集、初始值回填和脏检查。
type Subscriber = () => void;
type FieldSubscriber = (value: unknown) => void;
class DataManager {
/** 表单当前值 */
private values: Record<string, unknown> = {};
/** 初始值(用于重置和脏检查) */
private initialValues: Record<string, unknown> = {};
/** 全局订阅者 */
private subscribers = new Set<Subscriber>();
/** 字段级订阅者 */
private fieldSubscribers = new Map<string, Set<FieldSubscriber>>();
constructor(initialValues: Record<string, unknown> = {}) {
this.initialValues = this.deepClone(initialValues);
this.values = this.deepClone(initialValues);
}
/** 获取字段值(支持路径语法:address.city) */
getFieldValue(path: string): unknown {
return path.split('.').reduce<unknown>(
(obj, key) => (obj as Record<string, unknown>)?.[key],
this.values,
);
}
/** 设置字段值 */
setFieldValue(path: string, value: unknown): void {
this.setNestedValue(this.values, path, value);
// 通知字段级订阅者
this.fieldSubscribers.get(path)?.forEach((fn) => fn(value));
// 通知全局订阅者
this.subscribers.forEach((fn) => fn());
}
/** 批量设置值 */
setValues(values: Record<string, unknown>): void {
Object.entries(values).forEach(([path, value]) => {
this.setFieldValue(path, value);
});
}
/** 获取所有值 */
getValues(): Record<string, unknown> {
return this.deepClone(this.values);
}
/** 重置为初始值 */
reset(): void {
this.values = this.deepClone(this.initialValues);
this.subscribers.forEach((fn) => fn());
this.fieldSubscribers.forEach((subs, path) => {
const value = this.getFieldValue(path);
subs.forEach((fn) => fn(value));
});
}
/** 脏检查 —— 判断表单是否被修改过 */
isDirty(): boolean {
return JSON.stringify(this.values) !== JSON.stringify(this.initialValues);
}
/** 单字段脏检查 */
isFieldDirty(path: string): boolean {
const current = this.getFieldValue(path);
const initial = path.split('.').reduce<unknown>(
(obj, key) => (obj as Record<string, unknown>)?.[key],
this.initialValues,
);
return JSON.stringify(current) !== JSON.stringify(initial);
}
/** 订阅全局值变化 */
subscribe(fn: Subscriber): () => void {
this.subscribers.add(fn);
return () => this.subscribers.delete(fn);
}
/** 订阅单字段值变化(字段级渲染的基础) */
subscribeField(path: string, fn: FieldSubscriber): () => void {
if (!this.fieldSubscribers.has(path)) {
this.fieldSubscribers.set(path, new Set());
}
this.fieldSubscribers.get(path)!.add(fn);
return () => this.fieldSubscribers.get(path)?.delete(fn);
}
/** 设置嵌套值 */
private setNestedValue(
obj: Record<string, unknown>,
path: string,
value: unknown,
): void {
const keys = path.split('.');
let current: Record<string, unknown> = obj;
for (let i = 0; i < keys.length - 1; i++) {
if (!(keys[i] in current) || typeof current[keys[i]] !== 'object') {
current[keys[i]] = {};
}
current = current[keys[i]] as Record<string, unknown>;
}
current[keys[keys.length - 1]] = value;
}
private deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj)) as T;
}
}
export { DataManager };
4.3 校验引擎
校验引擎支持同步校验、异步校验和跨字段校验,是表单引擎的核心模块。
/** 校验结果 */
interface ValidationResult {
valid: boolean;
errors: FieldError[];
}
interface FieldError {
path: string;
message: string;
type: string;
}
/** 自定义校验函数签名 */
type CustomValidator = (
value: unknown,
formValues: Record<string, unknown>,
) => boolean | string | Promise<boolean | string>;
class Validator {
/** 自定义校验函数注册表 */
private customValidators = new Map<string, CustomValidator>();
/** 注册自定义校验函数 */
registerValidator(name: string, fn: CustomValidator): void {
this.customValidators.set(name, fn);
}
/** 校验单个字段 */
async validateField(
path: string,
value: unknown,
rules: ValidationRule[],
formValues: Record<string, unknown>,
): Promise<FieldError[]> {
const errors: FieldError[] = [];
for (const rule of rules) {
const error = await this.executeRule(path, value, rule, formValues);
if (error) {
errors.push(error);
// 遇到第一个错误即停止(可配置为收集全部)
break;
}
}
return errors;
}
/** 校验整个表单 */
async validateForm(
fields: FieldSchema[],
formValues: Record<string, unknown>,
): Promise<ValidationResult> {
const allErrors: FieldError[] = [];
for (const field of fields) {
if (field.hidden || !field.rules?.length) continue;
const value = this.getValueByPath(formValues, field.name);
const errors = await this.validateField(
field.name,
value,
field.rules,
formValues,
);
allErrors.push(...errors);
}
return {
valid: allErrors.length === 0,
errors: allErrors,
};
}
/** 执行单条校验规则 */
private async executeRule(
path: string,
value: unknown,
rule: ValidationRule,
formValues: Record<string, unknown>,
): Promise<FieldError | null> {
switch (rule.type) {
case 'required':
if (value === undefined || value === null || value === '') {
return { path, message: rule.message, type: 'required' };
}
break;
case 'minLength':
if (typeof value === 'string' && value.length < (rule.value as number)) {
return { path, message: rule.message, type: 'minLength' };
}
break;
case 'maxLength':
if (typeof value === 'string' && value.length > (rule.value as number)) {
return { path, message: rule.message, type: 'maxLength' };
}
break;
case 'min':
if (typeof value === 'number' && value < (rule.value as number)) {
return { path, message: rule.message, type: 'min' };
}
break;
case 'max':
if (typeof value === 'number' && value > (rule.value as number)) {
return { path, message: rule.message, type: 'max' };
}
break;
case 'pattern':
if (typeof value === 'string' && !new RegExp(rule.value as string).test(value)) {
return { path, message: rule.message, type: 'pattern' };
}
break;
case 'email': {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
if (typeof value === 'string' && !emailRegex.test(value)) {
return { path, message: rule.message, type: 'email' };
}
break;
}
case 'custom':
case 'async': {
// 自定义 / 异步校验
const validatorFn = rule.validator
? this.customValidators.get(rule.validator)
: undefined;
if (validatorFn) {
const result = await validatorFn(value, formValues);
if (result !== true) {
return {
path,
message: typeof result === 'string' ? result : rule.message,
type: rule.type,
};
}
}
break;
}
}
return null;
}
private getValueByPath(obj: Record<string, unknown>, path: string): unknown {
return path.split('.').reduce<unknown>(
(curr, key) => (curr as Record<string, unknown>)?.[key],
obj,
);
}
}
export { Validator };
export type { ValidationResult, FieldError, CustomValidator };
跨字段校验示例
跨字段校验是指一个字段的校验依赖于另一个字段的值,例如"确认密码必须与密码一致"。
const validator = new Validator();
// 注册跨字段校验:确认密码与密码一致
validator.registerValidator(
'confirmPassword',
(value: unknown, formValues: Record<string, unknown>) => {
if (value !== formValues['password']) {
return '两次输入的密码不一致';
}
return true;
},
);
// 注册异步校验:检查用户名是否已存在
validator.registerValidator(
'checkUsernameUnique',
async (value: unknown) => {
const response = await fetch(`/api/check-username?name=${value}`);
const data = (await response.json()) as { available: boolean };
return data.available ? true : '该用户名已被占用';
},
);
4.4 联动引擎
联动引擎是表单引擎中最复杂的模块,负责处理字段之间的依赖关系。
联动类型全览
| 联动类型 | 说明 | 示例 |
|---|---|---|
| 显隐联动 | 根据条件控制字段显示/隐藏 | 选择"其他"时显示自定义输入框 |
| 值联动 | 字段 A 变化时自动设置字段 B 的值 | 选择省份后自动设置城市 |
| 选项联动 | 字段 A 变化时更新字段 B 的可选项 | 选择国家后加载对应省份列表 |
| 禁用联动 | 根据条件控制字段的启用/禁用状态 | 未勾选"同意协议"时禁用提交 |
| 必填联动 | 根据条件动态改变字段的必填状态 | 选择"企业"时公司名称变必填 |
表达式引擎
/**
* 安全的表达式引擎
* 支持模板语法:{{fieldName}} 引用字段值
* 避免使用 eval / new Function,防止 XSS
*/
class ExpressionEngine {
/** 操作符集合 */
private static readonly OPERATORS: Record<
string,
(left: unknown, right: unknown) => boolean
> = {
'===': (l, r) => l === r,
'!==': (l, r) => l !== r,
'>': (l, r) => Number(l) > Number(r),
'<': (l, r) => Number(l) < Number(r),
'>=': (l, r) => Number(l) >= Number(r),
'<=': (l, r) => Number(l) <= Number(r),
'includes': (l, r) =>
Array.isArray(l) ? l.includes(r) : String(l).includes(String(r)),
'in': (l, r) => Array.isArray(r) && r.includes(l),
};
/** 解析并执行表达式 */
evaluate(expression: string, context: Record<string, unknown>): boolean {
// 1. 替换模板变量 {{fieldName}} -> 实际值
const resolved = this.resolveVariables(expression, context);
// 2. 解析比较表达式
return this.evaluateComparison(resolved, context);
}
/** 支持 AND / OR 组合 */
evaluateCompound(
expression: string,
context: Record<string, unknown>,
): boolean {
// 支持 && 和 || 组合
if (expression.includes('&&')) {
return expression
.split('&&')
.map((expr) => expr.trim())
.every((expr) => this.evaluate(expr, context));
}
if (expression.includes('||')) {
return expression
.split('||')
.map((expr) => expr.trim())
.some((expr) => this.evaluate(expr, context));
}
return this.evaluate(expression, context);
}
/** 解析模板变量 */
private resolveVariables(
expression: string,
context: Record<string, unknown>,
): string {
return expression.replace(/\{\{(\w+(?:\.\w+)*)\}\}/g, (_match, path: string) => {
const value = this.getByPath(context, path);
// 字符串值加引号,其他类型直接转字符串
return typeof value === 'string' ? `'${value}'` : String(value);
});
}
/** 解析比较表达式 */
private evaluateComparison(
expression: string,
_context: Record<string, unknown>,
): boolean {
// 匹配模式: leftOperand operator rightOperand
for (const [op, fn] of Object.entries(ExpressionEngine.OPERATORS)) {
if (expression.includes(` ${op} `)) {
const [left, right] = expression.split(` ${op} `).map((s) => s.trim());
return fn(this.parseValue(left), this.parseValue(right));
}
}
// 布尔判断(如 "{{isVip}}")
const trimmed = expression.trim();
return trimmed === 'true' || trimmed !== 'false' && trimmed !== '' && trimmed !== 'undefined' && trimmed !== 'null';
}
/** 解析字面量值 */
private parseValue(str: string): unknown {
// 字符串(带引号)
if ((str.startsWith("'") && str.endsWith("'")) ||
(str.startsWith('"') && str.endsWith('"'))) {
return str.slice(1, -1);
}
// 数字
if (!isNaN(Number(str))) return Number(str);
// 布尔
if (str === 'true') return true;
if (str === 'false') return false;
if (str === 'null') return null;
if (str === 'undefined') return undefined;
return str;
}
private getByPath(obj: Record<string, unknown>, path: string): unknown {
return path.split('.').reduce<unknown>(
(curr, key) => (curr as Record<string, unknown>)?.[key],
obj,
);
}
}
export { ExpressionEngine };
联动引擎实现
class LinkageEngine {
private expressionEngine = new ExpressionEngine();
private dataManager: DataManager;
private schemaManager: SchemaManager;
/** 字段 -> 依赖字段映射(用于精确触发) */
private dependencyGraph = new Map<string, Set<string>>();
constructor(dataManager: DataManager, schemaManager: SchemaManager) {
this.dataManager = dataManager;
this.schemaManager = schemaManager;
}
/** 构建依赖图 —— 分析哪些字段的变化会触发哪些联动 */
buildDependencyGraph(fields: FieldSchema[]): void {
this.dependencyGraph.clear();
for (const field of fields) {
if (!field.linkages) continue;
for (const linkage of field.linkages) {
// 提取表达式中引用的字段名
const deps = this.extractDependencies(linkage.condition);
for (const dep of deps) {
if (!this.dependencyGraph.has(dep)) {
this.dependencyGraph.set(dep, new Set());
}
this.dependencyGraph.get(dep)!.add(field.name);
}
}
}
}
/** 字段值变化时触发联动 */
onFieldChange(changedField: string): void {
const affectedFields = this.dependencyGraph.get(changedField);
if (!affectedFields) return;
const formValues = this.dataManager.getValues();
for (const fieldName of affectedFields) {
const schema = this.schemaManager.getField(fieldName);
if (!schema?.linkages) continue;
for (const linkage of schema.linkages) {
const conditionMet = this.expressionEngine.evaluateCompound(
linkage.condition,
formValues,
);
this.applyEffect(linkage.effect, conditionMet);
}
}
}
/** 应用联动效果 */
private applyEffect(
effect: LinkageRule['effect'],
conditionMet: boolean,
): void {
const { target, action, payload } = effect;
switch (action) {
case 'show':
this.schemaManager.setFieldProperty(target, 'hidden', !conditionMet);
break;
case 'hide':
this.schemaManager.setFieldProperty(target, 'hidden', conditionMet);
break;
case 'setValue':
if (conditionMet && payload !== undefined) {
this.dataManager.setFieldValue(target, payload);
}
break;
case 'setOptions':
if (conditionMet && payload) {
this.schemaManager.setFieldProperty(
target,
'componentProps.options',
payload,
);
}
break;
case 'enable':
this.schemaManager.setFieldProperty(target, 'disabled', !conditionMet);
break;
case 'disable':
this.schemaManager.setFieldProperty(target, 'disabled', conditionMet);
break;
case 'setRequired':
if (conditionMet) {
this.schemaManager.addRule(target, {
type: 'required',
message: `${this.schemaManager.getField(target)?.label ?? target}不能为空`,
});
} else {
this.schemaManager.removeRule(target, 'required');
}
break;
}
}
/** 从表达式中提取依赖的字段名 */
private extractDependencies(expression: string): string[] {
const deps: string[] = [];
const regex = /\{\{(\w+(?:\.\w+)*)\}\}/g;
let match = regex.exec(expression);
while (match !== null) {
deps.push(match[1]);
match = regex.exec(expression);
}
return deps;
}
}
export { LinkageEngine };
联动执行流程
4.5 布局引擎
布局引擎根据 Schema 中的布局配置,将字段组织为不同的排列形式。
/** 布局节点类型 */
type LayoutNodeType = 'form' | 'row' | 'col' | 'group' | 'steps' | 'step' | 'field';
/** 布局树节点 */
interface LayoutNode {
type: LayoutNodeType;
props: Record<string, unknown>;
children: LayoutNode[];
/** 关联的字段 Schema(type === 'field' 时存在) */
fieldSchema?: FieldSchema;
}
class LayoutEngine {
private columns: number;
constructor(columns: number = 1) {
this.columns = columns;
}
/** 将 Schema 转为布局树 */
buildLayoutTree(schema: FormSchema): LayoutNode {
const root: LayoutNode = {
type: 'form',
props: { layout: schema.layout, labelWidth: schema.labelWidth },
children: [],
};
if (schema.layout === 'grid') {
// 栅格布局:按 span 自动分行
root.children = this.buildGridLayout(schema.fields);
} else {
// 水平/垂直布局:一行一个字段
root.children = schema.fields.map((field) => ({
type: 'field' as const,
props: { span: 24 },
children: [],
fieldSchema: field,
}));
}
return root;
}
/** 栅格自动分行 */
private buildGridLayout(fields: FieldSchema[]): LayoutNode[] {
const rows: LayoutNode[] = [];
let currentRow: LayoutNode = {
type: 'row',
props: {},
children: [],
};
let currentSpan = 0;
const totalSpan = 24;
for (const field of fields) {
const span = field.layout?.span ?? Math.floor(totalSpan / this.columns);
if (currentSpan + span > totalSpan) {
// 当前行放不下,换行
rows.push(currentRow);
currentRow = { type: 'row', props: {}, children: [] };
currentSpan = 0;
}
currentRow.children.push({
type: 'col',
props: { span },
children: [
{
type: 'field',
props: {},
children: [],
fieldSchema: field,
},
],
});
currentSpan += span;
}
// 推入最后一行
if (currentRow.children.length > 0) {
rows.push(currentRow);
}
return rows;
}
}
export { LayoutEngine };
export type { LayoutNode };
五、渲染层实现(React)
5.1 FormRenderer
import React, { useEffect, useRef, useCallback } from 'react';
interface FormRendererProps {
schema: FormSchema;
initialValues?: Record<string, unknown>;
onSubmit?: (values: Record<string, unknown>) => void;
onChange?: (values: Record<string, unknown>) => void;
}
function FormRenderer({
schema,
initialValues = {},
onSubmit,
onChange,
}: FormRendererProps): React.ReactElement {
const engineRef = useRef<FormEngineCore | null>(null);
// 初始化引擎(仅一次)
useEffect(() => {
const engine = new FormEngineCore({
schema,
initialValues,
onChange: (values: Record<string, unknown>) => onChange?.(values),
});
engineRef.current = engine;
return () => engine.destroy();
}, []); // 空依赖,引擎仅初始化一次
const handleSubmit = useCallback(
async (e: React.FormEvent) => {
e.preventDefault();
const engine = engineRef.current;
if (!engine) return;
const result = await engine.validate();
if (result.valid) {
onSubmit?.(engine.getValues());
}
},
[onSubmit],
);
// 构建布局树
const layoutTree = engineRef.current?.getLayoutTree();
return (
<form onSubmit={handleSubmit}>
{layoutTree && <LayoutRenderer node={layoutTree} engine={engineRef.current!} />}
<button type="submit">提交</button>
</form>
);
}
export { FormRenderer };
5.2 FieldRenderer —— 字段级渲染
import React, { useState, useEffect, useCallback, memo } from 'react';
interface FieldRendererProps {
schema: FieldSchema;
engine: FormEngineCore;
}
/** 使用 memo + 字段级订阅实现精准渲染 */
const FieldRenderer = memo(function FieldRenderer({
schema,
engine,
}: FieldRendererProps): React.ReactElement | null {
const [value, setValue] = useState<unknown>(
engine.getFieldValue(schema.name),
);
const [error, setError] = useState<string>('');
const [fieldState, setFieldState] = useState({
hidden: schema.hidden ?? false,
disabled: schema.disabled ?? false,
});
useEffect(() => {
// 订阅字段值变化(字段级精准更新,不触发全局重渲染)
const unsubValue = engine.subscribeField(
schema.name,
(newValue: unknown) => {
setValue(newValue);
},
);
// 订阅字段状态变化(hidden / disabled 等)
const unsubState = engine.subscribeFieldState(
schema.name,
(state: { hidden?: boolean; disabled?: boolean }) => {
setFieldState((prev) => ({ ...prev, ...state }));
},
);
return () => { unsubValue(); unsubState(); };
}, [schema.name, engine]);
const handleChange = useCallback(
(newValue: unknown) => {
engine.setFieldValue(schema.name, newValue);
// 触发校验(根据 trigger 策略)
engine.validateField(schema.name, 'change').then((errors: FieldError[]) => {
setError(errors[0]?.message ?? '');
});
},
[schema.name, engine],
);
// 隐藏的字段不渲染
if (fieldState.hidden) return null;
// 从注册中心获取组件
const registryItem = fieldRegistry.get(schema.type);
if (!registryItem) {
return <div>Unknown field type: {schema.type}</div>;
}
const FieldComponent = registryItem.component;
return (
<div className="form-field">
<label>{schema.label}</label>
<FieldComponent
value={value}
onChange={handleChange}
disabled={fieldState.disabled}
readOnly={schema.readOnly}
placeholder={schema.placeholder}
error={error}
{...(schema.componentProps ?? {})}
/>
{error && <span className="field-error">{error}</span>}
</div>
);
});
export { FieldRenderer };
FieldRenderer 使用 memo 包裹,配合字段级订阅(subscribeField),实现了只有值变化的字段才会重渲染。如果使用全局 state(如 Redux store 的整个 form 对象),任何字段变化都会导致所有字段重渲染,100+ 字段时会有明显的性能问题。
六、表单设计器
表单设计器是一个可视化的 Schema 编辑工具,允许用户通过拖拽来配置表单。
6.1 设计器架构
6.2 拖拽核心实现
/** 拖拽项 */
interface DragItem {
type: 'component' | 'field';
/** 新组件时为组件类型,已有字段时为字段 ID */
id: string;
/** 新组件的默认 Schema */
defaultSchema?: Partial<FieldSchema>;
}
/** 放置目标 */
interface DropTarget {
/** 放置位置的索引 */
index: number;
/** 父容器 ID(用于嵌套) */
parentId?: string;
}
class DragManager {
private dragItem: DragItem | null = null;
private schemaFields: FieldSchema[] = [];
private listeners = new Set<() => void>();
/** 开始拖拽 */
startDrag(item: DragItem): void {
this.dragItem = item;
}
/** 放置到目标位置 */
drop(target: DropTarget): void {
if (!this.dragItem) return;
if (this.dragItem.type === 'component') {
// 新组件:生成默认 Schema 并插入
const newField: FieldSchema = {
name: this.generateFieldName(this.dragItem.id),
type: this.dragItem.id as FieldType,
label: `新字段_${Date.now()}`,
...this.dragItem.defaultSchema,
};
this.schemaFields.splice(target.index, 0, newField);
} else {
// 已有字段:移动位置
const currentIndex = this.schemaFields.findIndex(
(f) => f.name === this.dragItem!.id,
);
if (currentIndex !== -1) {
const [removed] = this.schemaFields.splice(currentIndex, 1);
const insertIndex = target.index > currentIndex
? target.index - 1
: target.index;
this.schemaFields.splice(insertIndex, 0, removed);
}
}
this.dragItem = null;
this.notify();
}
/** 删除字段 */
removeField(fieldName: string): void {
this.schemaFields = this.schemaFields.filter((f) => f.name !== fieldName);
this.notify();
}
/** 更新字段属性(属性面板编辑后调用) */
updateField(fieldName: string, updates: Partial<FieldSchema>): void {
const field = this.schemaFields.find((f) => f.name === fieldName);
if (field) {
Object.assign(field, updates);
this.notify();
}
}
/** 导出 Schema */
exportSchema(): FormSchema {
return {
version: '1.0.0',
layout: 'vertical',
fields: this.deepClone(this.schemaFields),
};
}
private generateFieldName(type: string): string {
const count = this.schemaFields.filter((f) => f.type === type).length;
return `${type}_${count + 1}`;
}
private notify(): void {
this.listeners.forEach((fn) => fn());
}
subscribe(fn: () => void): () => void {
this.listeners.add(fn);
return () => this.listeners.delete(fn);
}
private deepClone<T>(obj: T): T {
return JSON.parse(JSON.stringify(obj)) as T;
}
}
export { DragManager };
export type { DragItem, DropTarget };
七、性能优化
7.1 优化策略总览
| 优化方向 | 具体措施 | 效果 |
|---|---|---|
| 字段级渲染 | 每个字段独立订阅,memo 阻断无关更新 | 单字段变更不触发其他字段重渲染 |
| 懒加载字段组件 | 使用 React.lazy 按需加载字段组件 | 首屏只加载可见字段的组件代码 |
| 校验防抖 | 输入时校验加 debounce,blur 时立即校验 | 减少不必要的校验计算 |
| 联动依赖图 | 构建静态依赖图,精准触发受影响字段 | 避免全量遍历联动规则 |
| 虚拟滚动 | 超长表单使用虚拟列表渲染可见区域 | 200+ 字段仍流畅 |
| Schema 缓存 | Schema 解析结果缓存,避免重复解析 | 减少 JSON 解析开销 |
7.2 字段懒加载
import { lazy, type ComponentType } from 'react';
/** 字段组件懒加载映射 */
const lazyFieldMap: Record<string, () => Promise<{ default: ComponentType<FieldComponentProps> }>> = {
input: () => import('../fields/InputField'),
select: () => import('../fields/SelectField'),
datePicker: () => import('../fields/DatePickerField'),
upload: () => import('../fields/UploadField'),
cascader: () => import('../fields/CascaderField'),
richText: () => import('../fields/RichTextField'),
};
/** 获取懒加载字段组件 */
function getLazyFieldComponent(type: string): ComponentType<FieldComponentProps> | null {
const loader = lazyFieldMap[type];
if (!loader) return null;
return lazy(loader);
}
export { getLazyFieldComponent };
7.3 校验防抖
class DebouncedValidator {
private validator: Validator;
private timers = new Map<string, ReturnType<typeof setTimeout>>();
private debounceMs: number;
constructor(validator: Validator, debounceMs: number = 300) {
this.validator = validator;
this.debounceMs = debounceMs;
}
/** 带防抖的字段校验 */
validateField(
path: string,
value: unknown,
rules: ValidationRule[],
formValues: Record<string, unknown>,
trigger: 'change' | 'blur' | 'submit',
): Promise<FieldError[]> {
// blur 和 submit 立即执行,change 使用防抖
if (trigger !== 'change') {
return this.validator.validateField(path, value, rules, formValues);
}
return new Promise((resolve) => {
// 清除上一次的定时器
const existingTimer = this.timers.get(path);
if (existingTimer) clearTimeout(existingTimer);
this.timers.set(
path,
setTimeout(async () => {
const errors = await this.validator.validateField(
path,
value,
rules,
formValues,
);
resolve(errors);
this.timers.delete(path);
}, this.debounceMs),
);
});
}
/** 销毁时清理所有定时器 */
destroy(): void {
this.timers.forEach((timer) => clearTimeout(timer));
this.timers.clear();
}
}
export { DebouncedValidator };
八、扩展设计
8.1 插件机制
/** 插件钩子 */
interface FormPlugin {
name: string;
/** 引擎初始化后调用 */
onInit?: (engine: FormEngineCore) => void;
/** 字段值变化前拦截 */
onBeforeChange?: (
path: string,
value: unknown,
oldValue: unknown,
) => unknown | false; // 返回 false 阻止变更
/** 字段值变化后 */
onAfterChange?: (path: string, value: unknown) => void;
/** 校验前 */
onBeforeValidate?: (path: string, rules: ValidationRule[]) => ValidationRule[];
/** 表单提交前 */
onBeforeSubmit?: (values: Record<string, unknown>) => Record<string, unknown> | false;
/** 引擎销毁时 */
onDestroy?: () => void;
}
class PluginManager {
private plugins: FormPlugin[] = [];
use(plugin: FormPlugin): void {
this.plugins.push(plugin);
}
/** 执行钩子(链式执行,支持拦截) */
async executeHook<K extends keyof FormPlugin>(
hookName: K,
...args: Parameters<NonNullable<FormPlugin[K]> & ((...a: unknown[]) => unknown)>
): Promise<unknown> {
let result: unknown = args[0];
for (const plugin of this.plugins) {
const hook = plugin[hookName];
if (typeof hook === 'function') {
const hookResult = await (hook as (...a: unknown[]) => unknown)(...args);
if (hookResult === false) return false; // 拦截
if (hookResult !== undefined) result = hookResult;
}
}
return result;
}
}
// 使用示例:值格式化插件
const trimPlugin: FormPlugin = {
name: 'trim-plugin',
onBeforeChange: (_path, value) => {
// 自动去除字符串字段的首尾空格
return typeof value === 'string' ? value.trim() : value;
},
};
// 使用示例:日志插件
const logPlugin: FormPlugin = {
name: 'log-plugin',
onAfterChange: (path, value) => {
console.log(`[FormEngine] Field "${path}" changed to:`, value);
},
onBeforeSubmit: (values) => {
console.log('[FormEngine] Submitting:', values);
return values;
},
};
export { PluginManager };
export type { FormPlugin };
8.2 多框架适配
- React
- Vue 3
import { useRef, useEffect, useState, useCallback } from 'react';
interface UseFormOptions {
schema: FormSchema;
initialValues?: Record<string, unknown>;
}
interface UseFormReturn {
values: Record<string, unknown>;
errors: Record<string, string>;
setFieldValue: (path: string, value: unknown) => void;
validate: () => Promise<ValidationResult>;
submit: () => Promise<void>;
reset: () => void;
isDirty: boolean;
}
function useForm({ schema, initialValues = {} }: UseFormOptions): UseFormReturn {
const engineRef = useRef(
new FormEngineCore({ schema, initialValues }),
);
const [values, setValues] = useState(initialValues);
const [errors, setErrors] = useState<Record<string, string>>({});
const [isDirty, setIsDirty] = useState(false);
useEffect(() => {
const engine = engineRef.current;
const unsubscribe = engine.subscribe(() => {
setValues(engine.getValues());
setIsDirty(engine.isDirty());
});
return () => { unsubscribe(); engine.destroy(); };
}, []);
const setFieldValue = useCallback((path: string, value: unknown) => {
engineRef.current.setFieldValue(path, value);
}, []);
const validate = useCallback(() => {
return engineRef.current.validate();
}, []);
const submit = useCallback(async () => {
const result = await engineRef.current.validate();
if (!result.valid) {
const errorMap: Record<string, string> = {};
result.errors.forEach((err: FieldError) => {
errorMap[err.path] = err.message;
});
setErrors(errorMap);
}
}, []);
const reset = useCallback(() => {
engineRef.current.reset();
setErrors({});
}, []);
return { values, errors, setFieldValue, validate, submit, reset, isDirty };
}
export { useForm };
import { ref, onMounted, onUnmounted } from 'vue';
function useForm({ schema, initialValues = {} }: {
schema: FormSchema;
initialValues?: Record<string, unknown>;
}) {
const engine = new FormEngineCore({ schema, initialValues });
const values = ref(initialValues);
const errors = ref<Record<string, string>>({});
const isDirty = ref(false);
let unsubscribe: (() => void) | null = null;
onMounted(() => {
unsubscribe = engine.subscribe(() => {
values.value = engine.getValues();
isDirty.value = engine.isDirty();
});
});
onUnmounted(() => {
unsubscribe?.();
engine.destroy();
});
const setFieldValue = (path: string, value: unknown) => {
engine.setFieldValue(path, value);
};
const validate = () => engine.validate();
const reset = () => {
engine.reset();
errors.value = {};
};
return { values, errors, setFieldValue, validate, reset, isDirty };
}
export { useForm };
React 和 Vue 的适配层都依赖同一个 FormEngineCore,差异仅在于状态绑定方式(React 用 useState,Vue 用 ref)。这正是分层架构的价值所在。
九、主流方案对比
| 特性 | Formily | FormRender | Ant Design ProForm | react-hook-form |
|---|---|---|---|---|
| Schema 驱动 | JSON Schema + JSX Schema | JSON Schema | JSX 配置 | 非 Schema 驱动 |
| 框架支持 | React + Vue | React | React | React |
| 联动能力 | 表达式 + 主动/被动联动 | 简单表达式 | 手动编码 | 手动 watch |
| 性能优化 | 精准渲染(响应式) | 全量渲染 | React 渲染机制 | 非受控组件 |
| 设计器 | Formily Designer | FormRender Generator | 无 | 无 |
| 学习成本 | 高(概念多) | 低 | 低 | 低 |
| 扩展性 | 高(自定义组件) | 中(Widget) | 高(自定义组件) | 高 |
| 包体积 | ~50KB gzipped | ~15KB gzipped | Ant Design 依赖 | ~8KB gzipped |
| 适用场景 | 复杂中后台表单 | 简单配置化表单 | Ant Design 生态 | 通用表单 |
Formily 使用 @formily/reactive 实现了类似 MobX 的精准渲染机制,每个字段都是一个响应式对象,值变更时只有依赖该字段的组件会重渲染。这是其性能优势的核心来源。
常见面试问题
Q1: 表单引擎的 Schema 应该如何设计?
答案:
Schema 设计是表单引擎的核心,需要在表达能力和简洁性之间取得平衡。一个好的 Schema 协议应该满足以下原则:
/**
* 好的 Schema 设计原则:
*
* 1. 声明式 —— 描述"是什么",而非"怎么做"
* 2. 可序列化 —— 纯 JSON,无函数引用,便于持久化和传输
* 3. 可扩展 —— 预留 componentProps 存放组件专属配置
* 4. 可嵌套 —— children 支持 Object/Array 等复合结构
* 5. 版本化 —— 携带 version 字段,便于兼容性处理
*/
// 好的设计:声明式,字段职责清晰
const goodSchema: FieldSchema = {
name: 'email',
type: 'input',
label: '邮箱',
rules: [
{ type: 'required', message: '必填' },
{ type: 'email', message: '格式不正确' },
],
linkages: [
{
condition: "{{userType}} === 'enterprise'",
effect: { target: 'email', action: 'setRequired' },
},
],
};
// 不好的设计:混入了运行时逻辑
const badSchema = {
name: 'email',
type: 'input',
// 函数无法序列化,无法存储到数据库
validator: (value: unknown) => /^.+@.+$/.test(value as string),
// 直接写 JS 代码,有安全风险
visibleCondition: "formValues.userType === 'enterprise'",
};
Schema 分层策略(适合复杂场景):
| 层级 | 内容 | 示例 |
|---|---|---|
| 基础层 | 字段类型、标签、默认值 | type, label, defaultValue |
| 校验层 | 校验规则 | rules: [{ type: 'required' }] |
| 联动层 | 字段间关系 | linkages: [{ condition, effect }] |
| 布局层 | 排列方式 | layout: { span: 12 } |
| UI 层 | 组件专属属性 | componentProps: { mode: 'multiple' } |
Q2: 联动逻辑怎么实现?如何避免循环依赖?
答案:
联动实现的核心是依赖图 + 表达式引擎 + 变更传播。
/**
* 联动实现三步走:
*
* 1. 构建依赖图:分析 Schema 中所有联动规则,建立字段间的依赖关系
* 2. 监听变更:字段值变化时,通过依赖图找到受影响的字段
* 3. 执行效果:使用表达式引擎计算条件,应用联动效果
*/
// 循环依赖检测
class DependencyGraph {
private edges = new Map<string, Set<string>>();
addEdge(from: string, to: string): void {
if (!this.edges.has(from)) {
this.edges.set(from, new Set());
}
this.edges.get(from)!.add(to);
}
/** 检测是否存在循环依赖 */
hasCycle(): boolean {
const visited = new Set<string>();
const recursionStack = new Set<string>();
const dfs = (node: string): boolean => {
visited.add(node);
recursionStack.add(node);
const neighbors = this.edges.get(node) ?? new Set();
for (const neighbor of neighbors) {
if (!visited.has(neighbor)) {
if (dfs(neighbor)) return true;
} else if (recursionStack.has(neighbor)) {
// 在递归栈中发现已访问节点 → 存在环
return true;
}
}
recursionStack.delete(node);
return false;
};
for (const node of this.edges.keys()) {
if (!visited.has(node) && dfs(node)) return true;
}
return false;
}
}
// 防止联动无限递归的保护机制
class SafeLinkageEngine {
private static MAX_CASCADE_DEPTH = 10;
private currentDepth = 0;
onFieldChange(fieldName: string): void {
this.currentDepth++;
if (this.currentDepth > SafeLinkageEngine.MAX_CASCADE_DEPTH) {
console.error(
`[FormEngine] Linkage cascade depth exceeded ${SafeLinkageEngine.MAX_CASCADE_DEPTH}, ` +
`possible circular dependency involving field "${fieldName}".`,
);
this.currentDepth = 0;
return;
}
// ... 执行联动逻辑 ...
this.currentDepth--;
}
}
避免循环依赖的策略:
| 策略 | 说明 |
|---|---|
| 构建时检测 | Schema 解析阶段检测依赖图中的环,提前报错 |
| 运行时限深 | 联动执行设最大递归深度(如 10 层),超出则终止并警告 |
| 单向数据流 | 推荐联动规则只从"源字段"到"目标字段",避免双向依赖 |
| 批量更新 | 一次变更产生的多个联动效果合并执行,避免中间状态触发额外联动 |
Q3: 大表单(100+ 字段)如何做性能优化?
答案:
大表单性能优化需要从渲染、校验、数据三个维度入手。
import { memo, lazy, Suspense, useCallback } from 'react';
/** 1. 字段级精准渲染 —— 使用独立 store 而非全局 state */
const FieldRenderer = memo(function FieldRenderer({
name,
engine,
}: {
name: string;
engine: FormEngineCore;
}): React.ReactElement {
// 每个字段独立订阅自己的值,其他字段变化不会触发本组件更新
const value = useFieldValue(name, engine);
const handleChange = useCallback(
(v: unknown) => engine.setFieldValue(name, v),
[name, engine],
);
// ...
return <div>{/* 字段 UI */}</div>;
});
/** 2. 虚拟滚动 —— 仅渲染可视区域的字段 */
function VirtualFormRenderer({
fields,
engine,
}: {
fields: FieldSchema[];
engine: FormEngineCore;
}): React.ReactElement {
return (
<VirtualList
itemCount={fields.length}
itemHeight={72}
overscan={5}
renderItem={(index: number) => (
<FieldRenderer
key={fields[index].name}
name={fields[index].name}
engine={engine}
/>
)}
/>
);
}
/** 3. 字段组件懒加载 —— 按需加载重型组件 */
const RichTextField = lazy(() => import('./fields/RichTextField'));
const UploadField = lazy(() => import('./fields/UploadField'));
function LazyField({ type, ...props }: { type: string } & Record<string, unknown>): React.ReactElement {
const Component = getLazyFieldComponent(type);
return (
<Suspense fallback={<div className="field-skeleton" />}>
{Component && <Component {...props} />}
</Suspense>
);
}
优化效果对比:
| 优化措施 | 优化前 | 优化后 | 提升 |
|---|---|---|---|
| 100 字段首次渲染 | 450ms | 120ms | 73% |
| 单字段输入延迟 | 80ms | 5ms | 94% |
| 全量校验(100 字段) | 200ms | 50ms | 75% |
| 内存占用(200 字段) | 15MB | 6MB | 60% |
Q4: 如何支持自定义组件?
答案:
自定义组件的接入需要遵循表单引擎的字段组件规范——即实现统一的 Props 接口。引擎通过字段注册中心管理所有组件,自定义组件和内置组件完全平等。
import React, { useState, useRef } from 'react';
/**
* 自定义组件接入三步曲:
* 1. 实现 FieldComponentProps 接口
* 2. 注册到 FieldRegistry
* 3. 在 Schema 中使用
*/
// 步骤 1: 实现 FieldComponentProps 接口
interface ColorPickerProps extends FieldComponentProps<string> {
/** 预设颜色列表 */
presetColors?: string[];
}
function ColorPickerField({
value,
onChange,
disabled,
presetColors = ['#FF0000', '#00FF00', '#0000FF', '#FFD700', '#FF69B4'],
}: ColorPickerProps): React.ReactElement {
const inputRef = useRef<HTMLInputElement>(null);
return (
<div className="color-picker">
<div className="color-presets">
{presetColors.map((color) => (
<button
key={color}
className={`color-swatch ${value === color ? 'active' : ''}`}
style={{ backgroundColor: color }}
disabled={disabled}
onClick={() => onChange(color)}
type="button"
/>
))}
</div>
<input
ref={inputRef}
type="color"
value={value ?? '#000000'}
onChange={(e) => onChange(e.target.value)}
disabled={disabled}
/>
</div>
);
}
// 步骤 2: 注册到 FieldRegistry
fieldRegistry.register('colorPicker', {
component: ColorPickerField,
getDefaultValue: () => '#000000',
// 可选:序列化/反序列化
serialize: (value: unknown) => value,
deserialize: (value: unknown) => value,
});
// 步骤 3: 在 Schema 中使用
const schemaWithCustomField: FormSchema = {
version: '1.0.0',
layout: 'vertical',
fields: [
{
name: 'themeColor',
type: 'colorPicker' as FieldType, // 使用注册的类型名
label: '主题颜色',
defaultValue: '#1890ff',
componentProps: {
presetColors: ['#1890ff', '#52c41a', '#faad14', '#f5222d', '#722ed1'],
},
rules: [{ type: 'required', message: '请选择主题颜色' }],
},
],
};
自定义组件设计要点:
| 要点 | 说明 |
|---|---|
| 受控组件 | 必须通过 value + onChange 受控,不维护内部状态 |
| 空值处理 | 正确处理 undefined / null,提供合理的默认值 |
| 禁用/只读 | 响应 disabled 和 readOnly 属性 |
| 错误展示 | 可选接收 error 用于展示校验错误 |
| TypeScript | 泛型 FieldComponentProps<T> 约束 value 类型 |
自定义组件机制体现了开放-封闭原则(OCP)和依赖倒置原则(DIP)。引擎核心依赖的是 FieldComponentProps 抽象接口而非具体组件实现,新增字段类型无需修改引擎源码。
相关链接
- Formily 官方文档 - 阿里巴巴统一前端表单解决方案
- FormRender - 阿里飞猪团队的 Schema 表单渲染器
- react-hook-form - 高性能 React 表单库
- JSON Schema 规范 - JSON Schema 标准定义
- Ajv JSON Schema Validator - 高性能 JSON Schema 校验器
- MDN: FormData API - 浏览器表单数据 API
- 设计权限管理系统 - 权限系统中的表单权限控制
- 前端 SDK 通用架构设计 - SDK 架构设计思路参考