三、基本工具链和功能
实现 call 和 aplly
typescript
function add(x: number, y: number) {
return this.z + x + y
}
add._call({ z: 100 }, 1, 2) // 103
add._apply({ z: 100 }, [1, 2]) // 103
function add(x: number, y: number) {
return this.z + x + y
}
add._call({ z: 100 }, 1, 2) // 103
add._apply({ z: 100 }, [1, 2]) // 103
tsx
function foo() {
console.log('foo')
}
Function.prototype._call = function (thisArgs: any, args: Array<any>) {
const symbol = Symbol('call') // 不会覆盖原有属性
thisArgs[symbol] = foo
const ans = thisArgs[symbol](...args)
delete thisArgs[symbol]
return ans
}
foo._call({ x: 1 })
function foo() {
console.log('foo')
}
Function.prototype._call = function (thisArgs: any, args: Array<any>) {
const symbol = Symbol('call') // 不会覆盖原有属性
thisArgs[symbol] = foo
const ans = thisArgs[symbol](...args)
delete thisArgs[symbol]
return ans
}
foo._call({ x: 1 })
表单设计
表单的作用是收集用户的输入,在前端领域非常常见。特别是用 DSL 的方式设计表单,这是一个通用的技巧。
什么是 DSL
DSL(领域专有语言)。DSL 的表单设计,就是用专门设计表单的语言,来设计表单。
什么是专门设计表单的语言?
表单的通用设计
作为标准的表单设计,可以考虑下面的模型。

- DSL(Domain Specific Language),领域专有语言负责
- 描述表单
- 初始化表单数据的存储
- Render 负责根据 DSL 渲染表单
- Store 负责存储表单数据
- 用户在表单视图发生输入时触发 reducer,触发表单数据的变化
单向更新,不需要 store 再更新视图,由用户控制表单项数据,只是做存储功能。
简单案例如下:
tsx
// meta.config.ts DSL
export default {
form: {
items: [{ type: 'input', path: ['user', 'name'] }]
}
}
// meta.config.ts DSL
export default {
form: {
items: [{ type: 'input', path: ['user', 'name'] }]
}
}
tsx
// FormDSL.ts
import { Map as ImmutableMap } from 'immutable'
import { Store, Meta } from './dsl.types'
class FormDSL {
private store: Store = ImmutableMap()
private meta: Meta
constructor(meta: Meta) {
this.meta = meta
}
initStore() {}
}
// FormDSL.ts
import { Map as ImmutableMap } from 'immutable'
import { Store, Meta } from './dsl.types'
class FormDSL {
private store: Store = ImmutableMap()
private meta: Meta
constructor(meta: Meta) {
this.meta = meta
}
initStore() {}
}
tsx
// Render.tsx
import { Meta } from './dsl.types'
export default (meta: Meta) => {}
// Render.tsx
import { Meta } from './dsl.types'
export default (meta: Meta) => {}
typescript
// dsl.types.d.ts
import { Map as ImmutableMap } from 'immutable'
export type Store = ImmutableMap<string, Store>
export type FormItem = {
type: string
path: Array<string | number>
}
export type Meta = {
form: {
items: Array<FormItem>
}
}
// dsl.types.d.ts
import { Map as ImmutableMap } from 'immutable'
export type Store = ImmutableMap<string, Store>
export type FormItem = {
type: string
path: Array<string | number>
}
export type Meta = {
form: {
items: Array<FormItem>
}
}
表单组件
表单组件的分类设计
表单组件按照功能可以分成几类:
- 基础组件:负责具体的输入工作,比如图片上传、地区选择、姓名输入等;
- 布局组件:负责布局,比如 Row/Column/Group/Tabs 等等;
- 组合组件:支持在基础组件上的随意组合,例如 Group、List 等。
在每个表单组件的 DLS 中都需要有两个最基本的属性:
- 类型
- 数据路径(对应 store 中哪个数据进行操作)
还可以设计一些自定义属性,比如说:
- 样式
- 选项(仅 Select)
注意区分实例数据和元数据:
类型、数据路径、样式是所有表单组件公共的属性,是元数据(描述表单元素的数据)。
在设计数据结构的时候,不能将元数据和实例数据混合,举个反例:
typescript
{
type: 'input',
path: ['user', 'name'],
value: null,
dirty: false
}
{
type: 'input',
path: ['user', 'name'],
value: null,
dirty: false
}
推荐的实现:
typescript
{
meta: Metas.InputMeta,
value: null,
dirty: false
}
{
meta: Metas.InputMeta,
value: null,
dirty: false
}
元数据变更意味着所有 Input 组件变更,value
、dirty
是 Input 元素单独的内容。
受控组件和非受控组件设计
在性能影响范围不大的前提下,不要编写影响阅读效率的代码,保证代码易读。
设计具体表单组件的时候,需要考虑组件中数据是否受控。
设计思想如下:
尽量优先考虑非受控组件
- 最小知识原则(组件状态 !== 表单数据)
- 最小交互原则(减少数据流)
- 单一职责原则(让组件自己承担自己应该承担的部分)
- 性能考虑
tsx
// dsl.type.ts
import { Map as ImmutableMap } from 'immutable'
import { FormItem } from './Form'
export type Store = ImmutableMap<string, Store>
export type FormItemMeta = {
type: string
path?: Array<string | number>
cond?: (ctx: any) => any
default?: any
items?: Array<FormItemMeta>
}
export type Meta = {
form: FormItemMeta
}
export type FormItemProps = {
onChange?: (value: any) => any
item: FormItem
defaultValue?: any
}
// dsl.type.ts
import { Map as ImmutableMap } from 'immutable'
import { FormItem } from './Form'
export type Store = ImmutableMap<string, Store>
export type FormItemMeta = {
type: string
path?: Array<string | number>
cond?: (ctx: any) => any
default?: any
items?: Array<FormItemMeta>
}
export type Meta = {
form: FormItemMeta
}
export type FormItemProps = {
onChange?: (value: any) => any
item: FormItem
defaultValue?: any
}
tsx
// Form.ts 领域层
import { useMemo, useRef, useEffect } from 'react'
import { FormItemProps, Meta } from './dsl.types'
import { FormItem, FormComponent } from './Form'
function useForm(meta: Meta) {
const form = useMemo(() => new FormComponent(meta), [])
return form
}
export default ({ meta }: { meta: Meta }) => {
const form = useForm(meta)
return <Form item={form.getRoot()} />
}
const Form = (props: FormItemProps) => {
const item = props.item
return <div>{item.getChildren().map(child => render(child))}</div>
}
function useListenUpdata(item: FormItem) {
useEffect(() => {
// custom update
// item.on('update', () => {})
})
}
const Condition = (props: FormItemProps) => {
const cond = props.item.getCond()
const condIndex = cond()
return render(props.item.getChildren()[condIndex])
}
const Input = (props: FormItemProps) => {
const ref = useRef<HTMLInputElement>(null)
useListenUpdata(props.item)
useEffect(() => {
ref.current!.value = props.defaultValue
}, [])
return (
<input
ref={ref}
onChange={e => {
props.onChange && props.onChange(e.target.value)
}}
/>
)
}
function render(formItem: FormItem) {
const passProps = {
onChange: (value: any) => {
formItem.setValue(value)
},
defaultValue: formItem.getValue(),
item: formItem
}
switch (formItem.getType()) {
case 'form':
return <Form {...passProps} />
case 'input':
return <Input {...passProps} />
case 'condition':
return <Condition {...passProps} />
default:
throw new Error(`component ${formItem.getType()} not Found`)
}
}
// Form.ts 领域层
import { useMemo, useRef, useEffect } from 'react'
import { FormItemProps, Meta } from './dsl.types'
import { FormItem, FormComponent } from './Form'
function useForm(meta: Meta) {
const form = useMemo(() => new FormComponent(meta), [])
return form
}
export default ({ meta }: { meta: Meta }) => {
const form = useForm(meta)
return <Form item={form.getRoot()} />
}
const Form = (props: FormItemProps) => {
const item = props.item
return <div>{item.getChildren().map(child => render(child))}</div>
}
function useListenUpdata(item: FormItem) {
useEffect(() => {
// custom update
// item.on('update', () => {})
})
}
const Condition = (props: FormItemProps) => {
const cond = props.item.getCond()
const condIndex = cond()
return render(props.item.getChildren()[condIndex])
}
const Input = (props: FormItemProps) => {
const ref = useRef<HTMLInputElement>(null)
useListenUpdata(props.item)
useEffect(() => {
ref.current!.value = props.defaultValue
}, [])
return (
<input
ref={ref}
onChange={e => {
props.onChange && props.onChange(e.target.value)
}}
/>
)
}
function render(formItem: FormItem) {
const passProps = {
onChange: (value: any) => {
formItem.setValue(value)
},
defaultValue: formItem.getValue(),
item: formItem
}
switch (formItem.getType()) {
case 'form':
return <Form {...passProps} />
case 'input':
return <Input {...passProps} />
case 'condition':
return <Condition {...passProps} />
default:
throw new Error(`component ${formItem.getType()} not Found`)
}
}
tsx
// FormRender.tsx
import { useMemo, useRef, useEffect } from 'react'
import { FormItemProps, Meta } from './dsl.types'
import { FormItem, FormComponent } from './Form'
function useForm(meta: Meta) {
const form = useMemo(() => new FormComponent(meta), [])
return form
}
export default ({ meta }: { meta: Meta }) => {
const form = useForm(meta)
return <Form item={form.getRoot()} />
}
const Form = (props: FormItemProps) => {
const item = props.item
return <div>{item.getChildren().map(child => render(child))}</div>
}
function useListenUpdata(item: FormItem) {
useEffect(() => {
// custom update
// item.on('update', () => {})
})
}
const Condition = (props: FormItemProps) => {
const cond = props.item.getCond()
const condIndex = cond()
return render(props.item.getChildren()[condIndex])
}
const Input = (props: FormItemProps) => {
const ref = useRef<HTMLInputElement>(null)
useListenUpdata(props.item)
useEffect(() => {
ref.current!.value = props.defaultValue
}, [])
return (
<input
ref={ref}
onChange={e => {
props.onChange && props.onChange(e.target.value)
}}
/>
)
}
function render(formItem: FormItem) {
const passProps = {
onChange: (value: any) => {
formItem.setValue(value)
},
defaultValue: formItem.getValue(),
item: formItem
}
switch (formItem.getType()) {
case 'form':
return <Form {...passProps} />
case 'input':
return <Input {...passProps} />
case 'condition':
return <Condition {...passProps} />
default:
throw new Error(`component ${formItem.getType()} not Found`)
}
}
// FormRender.tsx
import { useMemo, useRef, useEffect } from 'react'
import { FormItemProps, Meta } from './dsl.types'
import { FormItem, FormComponent } from './Form'
function useForm(meta: Meta) {
const form = useMemo(() => new FormComponent(meta), [])
return form
}
export default ({ meta }: { meta: Meta }) => {
const form = useForm(meta)
return <Form item={form.getRoot()} />
}
const Form = (props: FormItemProps) => {
const item = props.item
return <div>{item.getChildren().map(child => render(child))}</div>
}
function useListenUpdata(item: FormItem) {
useEffect(() => {
// custom update
// item.on('update', () => {})
})
}
const Condition = (props: FormItemProps) => {
const cond = props.item.getCond()
const condIndex = cond()
return render(props.item.getChildren()[condIndex])
}
const Input = (props: FormItemProps) => {
const ref = useRef<HTMLInputElement>(null)
useListenUpdata(props.item)
useEffect(() => {
ref.current!.value = props.defaultValue
}, [])
return (
<input
ref={ref}
onChange={e => {
props.onChange && props.onChange(e.target.value)
}}
/>
)
}
function render(formItem: FormItem) {
const passProps = {
onChange: (value: any) => {
formItem.setValue(value)
},
defaultValue: formItem.getValue(),
item: formItem
}
switch (formItem.getType()) {
case 'form':
return <Form {...passProps} />
case 'input':
return <Input {...passProps} />
case 'condition':
return <Condition {...passProps} />
default:
throw new Error(`component ${formItem.getType()} not Found`)
}
}
tsx
// meta.config.ts
type FormContext = {
user: {
state: string
}
}
export default {
form: {
type: 'form',
items: [
{ type: 'input', path: ['user', 'name'], default: 'hello' }
// {
// type: 'condition',
// cond: (ctx: FormContext) => {
// return ctx.user.state === 'loggedIn' ? 0 : 1
// },
// items: [
// {
// type: 'input',
// path: ['lang', 'ts']
// },
// {
// type: 'input',
// path: ['land', 'node']
// }
// ]
// }
]
}
}
// meta.config.ts
type FormContext = {
user: {
state: string
}
}
export default {
form: {
type: 'form',
items: [
{ type: 'input', path: ['user', 'name'], default: 'hello' }
// {
// type: 'condition',
// cond: (ctx: FormContext) => {
// return ctx.user.state === 'loggedIn' ? 0 : 1
// },
// items: [
// {
// type: 'input',
// path: ['lang', 'ts']
// },
// {
// type: 'input',
// path: ['land', 'node']
// }
// ]
// }
]
}
}
tsx
// App.tsx
import FormRender from './FormRender'
import metaConfig from './meta.config'
import './App.css'
function App() {
return (
<div className="App">
<FormRender meta={metaConfig} />
</div>
)
}
export default App
// App.tsx
import FormRender from './FormRender'
import metaConfig from './meta.config'
import './App.css'
function App() {
return (
<div className="App">
<FormRender meta={metaConfig} />
</div>
)
}
export default App