使用范式及设计思路
代码地址: https://github.com/kizuner-bonely/oh-my-mini-react
首先我们需要一个表单,它的使用范式如下所示:
import { createRef, Component, useEffect } from 'react'
import Form, { Field } from '@form'
import { Input } from '@form/components'
const nameRules = { required: true, message: '请输入姓名!' }
const passwordRules = { required: true, message: '请输入密码!' }
export default function MyRCFieldForm() {
const [form] = Form.useForm()
const onFinish = (val: any) => {
console.log('onFinish', val)
}
// 表单校验失败执行
const onFinishFailed = (val: any) => {
console.log('onFinishFailed', val)
}
useEffect(() => {
console.log('form', form)
form.setFieldValue({ username: 'default' })
}, [])
return (
<div>
{/* 函数组件 */}
<h3>MyRCFieldForm</h3>
<Form form={form} onFinish={onFinish} onFinishFailed={onFinishFailed}>
<Field name="username" rules={[nameRules]}>
<Input placeholder="input UR Username" />
</Field>
<Field name="password" rules={[passwordRules]}>
<Input placeholder="input UR Password" />
</Field>
<button>Submit</button>
</Form>
{/* 类组件 */}
<MyClassFieldForm />
</div>
)
}
class MyClassFieldForm extends Component {
formRef = createRef()
onFinish = (val: any) => {
console.log('onFinish', val)
}
// 表单校验失败执行
onFinishFailed = (val: any) => {
console.log('onFinishFailed', val)
}
render() {
return (
<div>
<h3>MyClassFieldForm</h3>
<Form
ref={this.formRef}
onFinish={this.onFinish}
onFinishFailed={this.onFinishFailed}
>
<Field name="username" rules={[nameRules]}>
<Input placeholder="input UR Username" />
</Field>
<Field name="password" rules={[passwordRules]}>
<Input placeholder="input UR Password" />
</Field>
<button>Submit</button>
</Form>
</div>
)
}
}
通过使用范式我们可以得到以下结论:
- 在组件层面,我们需要
<Form>
、<Field>
,它们最终渲染的是<Field>
包裹的表单元素 - 对于函数组件,我们需要使用
useForm()
来生成 Form 实例,并将其通过参数传给<Form>
- 对于类组件,我们需要创建一个 ref 来获取 Form 实例
- 示例中的
button
没有绑定事件也能正常提交
根据以上结论,我们可以推导出以下设计思路:
- 根据结论1,无论是
<Form>
还是<Field>
,它们都会装饰并返回children
。 - 根据结论2、3:
- 类组件和函数组件对于 Form 的控制方式不同:
- 函数组件先通过
useForm()
生成 Form 实例,并通过参数传给<Form>
,这样函数组件就能通过该 Form 实例对 Form 进行控制。 - 类组件是先生成一个 ref,通过参数传给
<Form>
,这样类组件就能通过 ref 对 Form 进行控制。
- 函数组件先通过
- 对于
<Form>
,无论使用它的是类组件还是函数组件都需要有一个 Form 实例,对于函数组件来说 Form 实例是函数组件自己生成并当作参数传给<Form>
。
而对于类组件来说并没有自己生成 Form 实例,因此在<Form>
内部要对这两种情况作兼容。
兼容方式很明显,如果使用者有通过参数传 Form 实例就用外界传来的,如果没有则自己生成并将该 Form 实例返回给外界传入的 ref。 - 整个 Form 是通过 Form 实例进行控制,比如说规则校验、提交等等。
- 类组件和函数组件对于 Form 的控制方式不同:
- 根据结论4,
<Form>
的子组件肯定包括<form>
。
图1:Form 整体架构
1. 最简实现
首先把整体架构搭好,后面再补充细节,为了实现最简版本,一开始只考虑调用组件为函数组件。
从图一可以看出,Form Instance
需要和 <Form>
交互,也要和 <Field>
交互,需要注意的是 <Form>
和 <Field>
不一定是父子组件,中间可能隔着 <div>
这样的组件。因此通过参数传递有点不合适,使用 context 能保证它们都能拿到同一 Form Instance
。
- Form.tsx
- Field.tsx
- useForm.ts
- FieldContext.ts
- form.d.ts
form.d.ts
import {
FormStore } from './useForm'
export type FormProps = {
form?: ReturnType<FormStore['getForm']>
children: JSX.Element | JSX.Element[]
} & FormCallbacks
export type FieldProps = {
name: string
children: ReactElement<any, string | JSXElementConstructor<any>>
rules?: Array<{
required: boolean, message: string }>
}
export type FieldContextType = ReturnType<FormStore['getForm']>
FieldContext.ts
import type {
FieldContextType } from './form.d'
import {
createContext } from 'react'
import {
FormStore } from './useForm'
// 这里的参数只是为了不让 ts 报错,真正的 Form 实例还是要在 <Form> 中获得
export const FieldContext = createContext<FieldContextType>(new FormStore().getForm())
useForm.ts
import {
useRef } from 'react'
export default function useForm() {
const store = useRef(new FormStore()).current
return [store]
}
// Form 实例仓库,要实现两个功能点
// 1.存储状态
// 2.更新状态 -> 更新仓库值 + 通知组件更新
export class FormStore {
// store 存的是一个个键值对,键名是 <Field> 的 name,键值是 <Field> 包裹表单的值
private store: Record<string, any> = {
}
// 取
getFieldsValue = () => {
return {
...this.store }
}
getFieldValue = (name: string) => {
return this.store[name]
}
// 存
setFieldValue = (val: Record<string, any>) => {
// 1.更新仓库值
this.store = {
...this.store, ...val }
// todo 2.通知组件更新
}
// 统一接口
getForm = (