skyroc-form
v0.1.0
Published
[English](./README.md) | 简体中文
Readme
Skyroc Form
English | 简体中文
一个功能强大、类型安全的 React 表单管理库,提供完整的表单状态管理、验证和动态字段支持。
✨ 特性
- 🎯 完整的类型支持 - 基于 TypeScript,提供完整的类型推导和类型安全
- 📦 轻量级设计 - 核心功能精简,无额外依赖负担
- 🔄 响应式状态管理 - 高效的表单状态订阅和更新机制
- ✅ 强大的验证系统 - 支持同步/异步验证、内置规则和自定义验证器
- 🎨 灵活的组件设计 - 支持多态渲染和无头(Headless)模式
- 📝 动态数组字段 - 完整的数组字段管理(增删改查、排序等)
- 🔌 Schema 验证支持 - 兼容 Zod、Yup 等主流验证库
- ⚡ 性能优化 - 精确的字段订阅,避免不必要的重渲染
- 🔧 中间件系统 - 支持扩展和自定义表单行为
- ↩️ Undo/Redo - 内置撤销/重做功能
📦 安装
npm install skyroc-form
# 或
pnpm add skyroc-form
# 或
yarn add skyroc-form🚀 快速开始
基础表单
import { Form, Field } from 'skyroc-form';
function BasicForm() {
return (
<Form
initialValues={{ username: '', email: '' }}
onFinish={(values) => {
console.log('表单提交:', values);
}}
>
<Field name="username">
<input placeholder="用户名" />
</Field>
<Field name="email">
<input type="email" placeholder="邮箱" />
</Field>
<button type="submit">提交</button>
</Form>
);
}带验证的表单
import { Form, Field, useForm } from 'skyroc-form';
function ValidatedForm() {
const [form] = useForm();
return (
<Form
form={form}
initialValues={{ email: '', password: '' }}
onFinish={(values) => console.log('提交成功:', values)}
onFinishFailed={(error) => console.log('验证失败:', error)}
>
<Field
name="email"
rules={[
{ required: true, message: '请输入邮箱' },
{ type: 'email', message: '邮箱格式不正确' }
]}
>
<input placeholder="邮箱" />
</Field>
<Field
name="password"
rules={[
{ required: true, message: '请输入密码' },
{ minLength: 6, message: '密码至少6位' }
]}
>
<input type="password" placeholder="密码" />
</Field>
<button type="submit">提交</button>
<button type="button" onClick={() => form.resetFields()}>重置</button>
</Form>
);
}📚 核心 API
组件
<Form>
表单容器组件,提供表单上下文和状态管理。
Props:
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| form | FormInstance | - | 外部表单实例 |
| initialValues | object | {} | 初始表单值 |
| onFinish | (values) => void | - | 提交成功回调 |
| onFinishFailed | (error) => void | - | 提交失败回调 |
| onValuesChange | (changed, all) => void | - | 值变化回调 |
| onFieldsChange | (changed, all) => void | - | 字段元数据变化回调 |
| schema | FormSchema | - | Schema 验证器(支持 Zod 等) |
| validateTrigger | string \| string[] | 'onChange' | 验证触发事件 |
| validateMessages | ValidateMessages | - | 自定义验证消息 |
| preserve | boolean | true | 卸载时是否保留字段值 |
| component | ElementType \| false | 'form' | 渲染的组件类型 |
示例:
// 基础用法
<Form initialValues={{ name: 'John' }}>
{/* 字段 */}
</Form>
// 无头模式(不渲染包裹元素)
<Form component={false}>
<div className="custom-layout">
{/* 字段 */}
</div>
</Form>
// 使用 Schema 验证
const schema = z.object({
username: z.string().min(3),
age: z.number().min(18)
});
<Form schema={schema} onFinish={handleSubmit}>
{/* 字段 */}
</Form><Field>
字段组件,用于包裹输入控件并提供状态管理和验证。
Props:
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| name | string | 必填 | 字段名(支持嵌套路径如 user.email) |
| rules | Rule[] | - | 验证规则数组 |
| initialValue | any | - | 字段初始值 |
| trigger | string | 'onChange' | 触发值更新的事件 |
| validateTrigger | string \| string[] | - | 触发验证的事件 |
| valuePropName | string | 'value' | 值属性名 |
| getValueFromEvent | (...args) => any | - | 自定义值提取函数 |
| normalize | (value, prevValue, allValues) => any | - | 值规范化/转换函数 |
| preserve | boolean | true | 卸载时是否保留值 |
示例:
// 基础字段
<Field name="username">
<input />
</Field>
// 带验证规则
<Field
name="phone"
rules={[
{ required: true, message: '请输入手机号' },
{ pattern: /^1\d{10}$/, message: '手机号格式不正确' }
]}
>
<input />
</Field>
// 自定义值提取和规范化
<Field
name="phone"
getValueFromEvent={(e) => e.target.value.replace(/\D/g, '')}
normalize={(value) => {
const cleaned = value.replace(/\D/g, '');
const match = cleaned.match(/^(\d{3})(\d{4})(\d{4})$/);
return match ? `${match[1]}-${match[2]}-${match[3]}` : value;
}}
>
<input placeholder="138-0013-8000" />
</Field>
// 嵌套字段
<Field name="user.profile.email">
<input type="email" />
</Field><List>
动态数组字段管理组件,提供完整的数组操作能力。
Props:
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| name | string | 必填 | 数组字段名 |
| initialValue | any[] | - | 数组初始值 |
| children | (fields, operations) => ReactNode | 必填 | 渲染函数 |
Operations:
add(value?)- 添加新项remove(index)- 移除指定项move(from, to)- 移动项位置swap(i, j)- 交换两项位置insert(index, value)- 在指定位置插入replace(index, value)- 替换指定项
示例:
// 基础列表
<List name="users" initialValue={[{ name: '', email: '' }]}>
{(fields, { add, remove }) => (
<>
{fields.map((field) => (
<div key={field.key}>
<Field name={`${field.name}.name`}>
<input placeholder="姓名" />
</Field>
<Field name={`${field.name}.email`}>
<input placeholder="邮箱" />
</Field>
<button onClick={() => remove(field.name)}>删除</button>
</div>
))}
<button onClick={() => add()}>添加用户</button>
</>
)}
</List>
// 高级列表操作
<List name="tasks">
{(fields, { add, remove, move, swap }) => (
<>
{fields.map((field, index) => (
<div key={field.key}>
<Field name={`${field.name}.title`}>
<input />
</Field>
<button onClick={() => remove(index)}>删除</button>
<button onClick={() => move(index, index - 1)} disabled={index === 0}>
上移
</button>
<button onClick={() => move(index, index + 1)} disabled={index === fields.length - 1}>
下移
</button>
</div>
))}
<button onClick={() => add({ title: '' })}>添加任务</button>
</>
)}
</List><ComputedField>
计算字段组件,根据其他字段值自动计算和更新。
Props:
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| name | string | 必填 | 计算字段名 |
| deps | string[] | 必填 | 依赖的字段名数组 |
| compute | (get, all) => any | 必填 | 计算函数 |
| rules | Rule[] | - | 验证规则数组 |
| valuePropName | string | 'value' | 值属性名 |
| preserve | boolean | true | 卸载时是否保留值 |
示例:
// 计算总价
<Form initialValues={{ quantity: 1, unitPrice: 100 }}>
<Field name="quantity">
<input type="number" placeholder="数量" />
</Field>
<Field name="unitPrice">
<input type="number" placeholder="单价" />
</Field>
<ComputedField
name="totalPrice"
deps={['quantity', 'unitPrice']}
compute={(get) => {
const quantity = get('quantity') || 0;
const unitPrice = get('unitPrice') || 0;
return quantity * unitPrice;
}}
>
<input placeholder="总价(自动计算)" />
</ComputedField>
</Form>
// 格式化全名
<Form>
<Field name="firstName">
<input placeholder="名" />
</Field>
<Field name="lastName">
<input placeholder="姓" />
</Field>
<ComputedField
name="fullName"
deps={['firstName', 'lastName']}
compute={(get) => {
const first = get('firstName') || '';
const last = get('lastName') || '';
return `${last}${first}`.trim();
}}
>
<input placeholder="全名(自动生成)" />
</ComputedField>
</Form>Hooks
useForm()
创建表单实例,用于程序化控制表单。
const [form] = useForm<FormValues>();
// 表单实例方法
form.getFieldValue('username'); // 获取字段值
form.getFieldsValue(); // 获取所有字段值
form.setFieldValue('username', 'John'); // 设置字段值
form.setFieldsValue({ username: 'John', email: '[email protected]' });
form.resetFields(); // 重置所有字段
form.resetFields(['username']); // 重置指定字段
form.submit(); // 触发提交
form.validateFields(); // 验证所有字段
form.validateFields(['email']); // 验证指定字段
form.getFieldError('email'); // 获取字段错误
form.getFieldsError(); // 获取所有字段错误useWatch()
监听表单字段值变化。
// 监听单个字段
const username = useWatch('username', { form });
// 监听多个字段
const { email, phone } = useWatch(['email', 'phone'], { form });
// 监听所有字段
const allValues = useWatch(form);
// 在表单内部使用(自动获取表单实例)
function FormContent() {
const values = useWatch(); // 无需传入 form
return <div>{JSON.stringify(values)}</div>;
}useFieldState()
获取字段的完整状态(值、错误、验证状态等)。
const fieldState = useFieldState('email', { form });
// {
// value: '[email protected]',
// errors: [],
// warnings: [],
// validating: false,
// validated: true,
// touched: true
// }useFieldError()
获取字段错误信息。
const error = useFieldError('email', { form });useArrayField()
用于数组字段的操作 Hook,提供完整的数组操作能力。
const { fields, add, remove, move, swap, insert, replace } = useArrayField('users', form);
// 返回值说明
// fields: ListRenderItem[] - 数组字段项列表(带稳定 key)
// add: (value?) => void - 添加新项
// remove: (index) => void - 移除指定项
// move: (from, to) => void - 移动项位置
// swap: (i, j) => void - 交换两项
// insert: (index, value) => void - 在指定位置插入
// replace: (index, value) => void - 替换指定项示例:
function TodoList() {
const [form] = useForm();
const { fields, add, remove, move } = useArrayField('todos', form);
return (
<Form form={form} initialValues={{ todos: [] }}>
{fields.map((field, index) => (
<div key={field.key}>
<Field name={`${field.name}.title`}>
<input placeholder="任务标题" />
</Field>
<button onClick={() => remove(index)}>删除</button>
<button onClick={() => move(index, index - 1)}>上移</button>
</div>
))}
<button onClick={() => add({ title: '' })}>添加任务</button>
</Form>
);
}useFieldEffect()
创建响应式副作用,当指定字段变化时执行自定义逻辑。
useFieldEffect(
deps: string[], // 依赖的字段名数组
effect: (get, all) => void, // 副作用函数
form?: FormInstance // 可选的表单实例
);示例:
function UserForm() {
const [form] = useForm();
// 监听用户名变化并记录日志
useFieldEffect(
['firstName', 'lastName'],
(get) => {
const firstName = get('firstName');
const lastName = get('lastName');
console.log(`姓名变更为: ${firstName} ${lastName}`);
},
form
);
// 监听国家变化并清空城市
useFieldEffect(
['country'],
(get, all) => {
const country = get('country');
if (country !== all.previousCountry) {
form.setFieldValue('city', undefined);
}
},
form
);
return (
<Form form={form}>
<Field name="firstName">
<input placeholder="名" />
</Field>
<Field name="lastName">
<input placeholder="姓" />
</Field>
<Field name="country">
<select>{/* 国家选项 */}</select>
</Field>
<Field name="city">
<select>{/* 城市选项 */}</select>
</Field>
</Form>
);
}useSelector()
选择和订阅派生的表单状态,只在选中的值变化时触发重渲染。
const value = useSelector<Values, Result>(
selector: (get, all) => Result, // 选择器函数
options?: {
deps?: string[]; // 依赖字段(空数组表示所有字段)
form?: FormInstance; // 表单实例
eq?: (a, b) => boolean; // 自定义相等比较函数
mask?: ChangeMask; // 订阅的变化类型掩码
includeChildren?: boolean; // 是否包含子字段变化
}
);示例:
// 计算购物车总价
function ShoppingCart() {
const total = useSelector(
(get) => {
const items = get('items') || [];
return items.reduce((sum, item) => sum + item.price * item.quantity, 0);
},
{ deps: ['items'] }
);
return (
<Form>
<List name="items">
{(fields, { add, remove }) => (
<>
{fields.map((field) => (
<div key={field.key}>
<Field name={`${field.name}.name`}>
<input placeholder="商品名称" />
</Field>
<Field name={`${field.name}.price`}>
<input type="number" placeholder="单价" />
</Field>
<Field name={`${field.name}.quantity`}>
<input type="number" placeholder="数量" />
</Field>
<button onClick={() => remove(field.name)}>删除</button>
</div>
))}
<button onClick={() => add({ name: '', price: 0, quantity: 1 })}>
添加商品
</button>
</>
)}
</List>
<div>总价: ¥{total.toFixed(2)}</div>
</Form>
);
}
// 使用自定义相等比较(避免对象引用变化导致的重渲染)
function UserProfile() {
const userInfo = useSelector(
(get) => ({
name: get('name'),
age: get('age'),
email: get('email')
}),
{
deps: ['name', 'age', 'email'],
eq: (a, b) => {
// 深度相等比较
return a.name === b.name && a.age === b.age && a.email === b.email;
}
}
);
return <div>{JSON.stringify(userInfo)}</div>;
}useUndoRedo()
撤销/重做功能。
const { undo, redo, canUndo, canRedo } = useUndoRedo({ form });🎯 验证规则
内置验证类型
<Field
name="field"
rules={[
// 必填
{ required: true, message: '此字段必填' },
// 类型验证
{ type: 'email', message: '邮箱格式不正确' },
{ type: 'url', message: 'URL 格式不正确' },
{ type: 'number', message: '必须是数字' },
{ type: 'integer', message: '必须是整数' },
{ type: 'float', message: '必须是浮点数' },
{ type: 'boolean', message: '必须是布尔值' },
{ type: 'date', message: '必须是日期' },
{ type: 'hex', message: '必须是十六进制颜色' },
{ type: 'regexp', message: '必须是正则表达式' },
// 字符串长度
{ minLength: 3, message: '至少 3 个字符' },
{ maxLength: 20, message: '最多 20 个字符' },
{ len: 11, message: '必须是 11 位' },
// 数值范围
{ min: 0, max: 100, message: '必须在 0-100 之间' },
// 正则匹配
{ pattern: /^[a-zA-Z]+$/, message: '只能包含字母' },
// 枚举值
{ type: 'enum', enum: ['admin', 'user', 'guest'], message: '角色不正确' },
// 仅警告(不阻止提交)
{ minLength: 8, message: '密码较弱,建议至少 8 位', warningOnly: true },
// 空格验证
{ whitespace: true, message: '不能只包含空格' },
// 自定义验证器
{
validator: async (rule, value, allValues) => {
if (value !== allValues.password) {
return '两次密码不一致';
}
}
},
// 值转换
{
type: 'string',
transform: (value) => value?.trim(),
message: '无效的字符串'
}
]}
>
<input />
</Field>验证模式
// 串行验证(遇到第一个错误即停止)
form.validateFields({ mode: 'serial' });
// 并行验证(收集所有错误)
form.validateFields({ mode: 'parallelAll' });
// 并行验证(返回第一个错误)
form.validateFields({ mode: 'parallelFirst' });Schema 验证
import { z } from 'zod';
const schema = z.object({
username: z.string().min(3, '用户名至少 3 个字符'),
email: z.string().email('邮箱格式不正确'),
age: z.number().min(18, '必须年满 18 岁'),
password: z.string().min(8, '密码至少 8 位'),
confirmPassword: z.string()
}).refine((data) => data.password === data.confirmPassword, {
message: '两次密码不一致',
path: ['confirmPassword']
});
<Form schema={schema} onFinish={handleSubmit}>
{/* 字段 */}
</Form>🔧 高级用法
条件字段显示/禁用
function ConditionalFields() {
const [form] = useForm();
const userType = useWatch('userType', { form });
return (
<Form form={form}>
<Field name="userType">
<select>
<option value="individual">个人</option>
<option value="company">企业</option>
</select>
</Field>
{userType === 'company' && (
<Field name="companyName">
<input placeholder="公司名称" />
</Field>
)}
</Form>
);
}表单联动
function LinkedFields() {
const [form] = useForm();
return (
<Form form={form}>
<Field
name="country"
onChange={(value) => {
// 国家变化时清空城市
form.setFieldValue('city', undefined);
}}
>
<select>{/* 国家选项 */}</select>
</Field>
<Field name="city">
<select>{/* 根据国家动态加载城市 */}</select>
</Field>
</Form>
);
}中间件系统
// 创建日志中间件
const loggerMiddleware = (action, next) => {
console.log('Action:', action);
const result = next(action);
console.log('Result:', result);
return result;
};
const [form] = useForm();
form.use(loggerMiddleware);嵌套表单
<Form>
<Field name="user.profile.firstName">
<input />
</Field>
<Field name="user.profile.lastName">
<input />
</Field>
<Field name="user.contact.email">
<input />
</Field>
</Form>🎨 TypeScript 支持
完整的类型推导和类型安全:
interface FormValues {
username: string;
email: string;
profile: {
age: number;
bio: string;
};
hobbies: string[];
}
function TypedForm() {
const [form] = useForm<FormValues>();
// ✅ 类型安全
const username = form.getFieldValue('username'); // string
const age = form.getFieldValue('profile.age'); // number
// ✅ 自动补全
form.setFieldsValue({
username: 'John',
profile: { age: 25, bio: 'Developer' }
});
return (
<Form<FormValues>
form={form}
onFinish={(values) => {
// values 已完全类型化
console.log(values.username); // ✅
console.log(values.profile.age); // ✅
}}
>
<Field<FormValues> name="username">
<input />
</Field>
<Field<FormValues> name="profile.age">
<input type="number" />
</Field>
</Form>
);
}📖 最佳实践
1. 使用外部表单实例进行程序化控制
function MyForm() {
const [form] = useForm();
const handleReset = () => {
form.resetFields();
};
const handleFill = () => {
form.setFieldsValue({
username: 'admin',
email: '[email protected]'
});
};
return (
<Form form={form}>
{/* 字段 */}
<button type="button" onClick={handleReset}>重置</button>
<button type="button" onClick={handleFill}>填充</button>
</Form>
);
}2. 合理使用 useWatch 避免不必要的渲染
// ❌ 不好:整个组件都会在任何字段变化时重渲染
function BadExample() {
const values = useWatch();
return <div>{values.someField}</div>;
}
// ✅ 好:只在 someField 变化时重渲染
function GoodExample() {
const someField = useWatch('someField');
return <div>{someField}</div>;
}3. 复杂验证使用自定义验证器
<Field
name="password"
rules={[
{
validator: async (rule, value) => {
// 异步验证:检查密码强度
const strength = await checkPasswordStrength(value);
if (strength < 3) {
return '密码强度不够';
}
}
}
]}
>
<input type="password" />
</Field>🤝 与 UI 库集成
Skyroc Form 可以与任何 UI 库无缝集成:
// 与 Ant Design 集成
import { Input, Button } from 'antd';
<Form>
<Field name="username">
<Input placeholder="用户名" />
</Field>
<Button type="primary" htmlType="submit">提交</Button>
</Form>
// 与 Material-UI 集成
import { TextField, Button } from '@mui/material';
<Form>
<Field name="email">
<TextField label="邮箱" />
</Field>
<Button type="submit">提交</Button>
</Form>📄 License
MIT License
