【手撸低代码工具】二次封装UI库(六)反思与重构(表单)
对UI库进行封装,其实很早就开始了,先是使用JS的方式,后来学会了一点TS,于是一点一点把 anyScript 换成 typescript,最近 Vue3.3 的更新,支持了从外部引入 Interface,于是又开始重构组件 props 的创建方式。
当然,如果是一般的项目,基本是没有这个时间去改的,能跑就行嘛,为啥要改?所以,这不是一般的项目,只是一个尝试。
因为有时间,所以看以前写的代码不顺眼,那么就会去重构。比如这次就发现,组件内部传递信息,也必须使用 props 吗?使用状态行不行?
使用状态,可以可以汇拢散乱的数据,轻松支持复杂的子组件的结构,不用去设计各种 props,都放入状态即可,那么要不要改?
使用状态代替 props 的传递。
首先声明一点,封装的组件内部与外部的使用者之间,最好还是通过 props,因为各种UI库都是这么做的。如果这也使用状态,那么UI库就和状态发生一种强行绑定,这个不是很友好,不建议使用。
那么 表单控件内部为啥要用 状态呢?因为 el-form-item 又这封装了一个小组件,而且还使用的比较频繁,而它的props 又比较复杂,所以适合使用状态。
设计一种状态
- 状态的范围:那必须是局部状态,不可能是全局状态,否则就乱套了。
- 包含需要的各种数据
- 内部处理好联动
其实,这个状态从某种角度来看,代替了部分 controller 的职责。
我们整理一下表单控件需要的各种数据。
来至于配置信息的数据
因为json文件的结构问题,以及维护 json 的便捷性问题,所以结构有些散乱:
- formMeta:表单控件的 meta
- columnsNumber: number 表单列数
- colOrder: Array<number> 字段ID集合,排序依据
- linkageMeta: 联动筛选的配置信息
- ruleMeta: 验证规则的信息
- subMeta: 分栏信息
- childPropsList input 的 props 和 meta
- meta: colName、controlType 等
- props: UI库的组件需要的属性
运行时创建的数据
根据配置信息,创建或者调整的数据:
- formColSpan: 多列用的
- showCol: 联动筛选用的
- childMetaList: 字段纯 meta 的集合
- childPropsList: 字段纯 props 的集合
来至于 props 的数据
- model:表单的 model,通过 props 传入
- partModel 通过props 传入,同时也是依据 联动筛选 创建的
使用 nf-state 做一个状态
为啥不用 pinia?因为他是全局状态,虽然可以使用 id 来区分,但是万一重名了咋办?
截止到 Pinia V2.1.3 ,Pinia 的 id 不支持 Symbol,因为内部会判断 ID 是不是 __hot:
开头:
function devtoolsPlugin({ app, store, options }) {
// HMR module
if (store.$id.startsWith('__hot:')) {
return;
}
略
}
使用 Symbol 可以方便的避免重名,但是可惜不支持。
所以,自己做一套状态好了,又不难。当然我们也可以直接使用 reactive + provide/inject 来实现。
先定义一个 Interface
发现了,还得先定义 Interface:
/**
* 表单控件内部需要的状态
*/
export interface IFormState<T> {
// props 传入的表单 model
model: T,
// 筛选后的 model
partModel?: IAnyObject,
// 来自配置 formMeta,确定表单的列数
columnsNumber: () => number
// 来自配置的 formMeta
formMeta: IFromMeta,
// 来自 配置 的 验证规则
ruleMeta: IRuleMeta,
// 来自配置的 排序字段ID
colOrder: Array<string | number>,
// 来自配置的 联动筛选的设置
linkageMeta: ILinkageMeta,
// input 的 props 的集合,不含 meta
childPropsList: IFormChildPropsList,
// input 的 meta 集合
childMetaList: IFormChildMetaList,
// 根据 columnsNumber 和 input 的 meta 计算出来一个字段占多少 span
formColSpan: FormColSpan,
// 联动筛选时,字段是否可见
showCol: ShowCol
}
这样就可以把表单控件需要的各种数据都集合在一起,需要使用的时候,拿出来即可。
定义状态。
// 创建一个局部表单内部状态
export function regFormState<T extends IAnyObject>(props: IFromProps<T>) {
const state = defineStore<IFormState<T>>(key, () => {
const {
formMeta,
model,
partModel
} = props
// 多列用的
const formColSpan = reactive<FormColSpan>({})
// 设置字段的是否可见
const showCol = reactive<ShowCol>({})
// 字段的纯 meta
const childMetaList = {} as IAnyObject
// 字段的纯 props
const childPropsList = {} as IAnyObject
// 转换一下
Object.keys(props.childPropsList).forEach((key) => {
childMetaList[key] = props.childPropsList[key].meta
childPropsList[key] = props.childPropsList[key].props
})
// 获取表单的列数
const columnsNumber = () => {
return formMeta.columnsNumber
}
return {
// 表单的 model
model,
partModel,
// 配置
columnsNumber,
formMeta: formMeta, // 表单的meta
ruleMeta: formMeta.ruleMeta, // 验证规则
colOrder: formMeta.colOrder, // 字段排序
linkageMeta: formMeta.linkageMeta, // 联动筛选
// 创建
childPropsList, // input 的 props
childMetaList, // 字段的纯 meta
formColSpan, // 多列用的
showCol // 设置字段的是否可见
}
})
return state
}
/**
* 获取局部状态,表单的内部状态
* @returns
*/
export function getFormState<T>() {
const re = useStoreLocal<IFormState<T>>(key)
return re
}
看起来有点像 Pinia,不过有两个区别:
- 一个是局部状态,
- 一个是可以使用 Symbol 做状态的标识,这样可以不用担心重名的问题了。
这里只是把数据集中在一起,并没有把操作方法也集中起来,还是放在了 controller 里面。 感觉放在哪里都差不多。
columnsNumber 使用了函数的形式,是想在配置信息发生变化时,可以获取最新的数据,当然也可以使用 computed ,或者直接使用 formMeta.columnsNumber
。
根据需求,这里做一个函数即可。
controller 的变化
/**
* 表单控件的管理类,创建内部局部状态
* @param props 表单控件的 props
* @returns
*/
export default function formController<T extends IAnyObject> (
props: IFromProps<T>
) {
// 注册一个状态,统一管理各种数据
const state = regFormState(props)
// IFormItemList<T>, 表单子组件的 meta
const { childMetaList, formMeta } = state
// 调用 useColSpan 设置一个字段占用多少 span, 返回刷新函数
const { setFormColSpan } = useColSpan(state)
// 调用 useLinkage 监听联动关系,返回刷新函数
const { setFormColShow } = useLinkage(state)
// 设置各种 watch 略
return {
state
}
}
其实变化并不太大,因为没有把 watch 放在状态内部。还没想出来更适合的方式。
先创建一个状态,然后调用函数设置 formColSpan 和 showCol,然后各种监听,当发生变化的时候,重新设置各种需要的数据。
使用状态
首先使用状态的是 base-item,之前通过 props传递数据,父组件要组织数据,子组件要一个一个获取,过程中又被 props 包装了一层。
子组件获取状态
现在方便多了,父组件不用想到底需要设置哪些 props,子组件自己从状态里获取即可。
<script setup lang="ts" generic="T extends object">
import type { IFormItemMeta } from '../map'
// 获取状态
import { getFormState } from '../map'
// 引入表单子控件
import { formItemKey } from '../form-item/_map-form-item'
// 定义 props
const props = defineProps<IFormItemMeta>()
// 获取表单内部的状态
const state = getFormState<T>()
const {
model,
ruleMeta,
formColSpan,
showCol,
childPropsList, // input 的 props
childMetaList, // 字段的纯 meta
} = state
</script>
这样方便父组件使用。
父组件的使用
<base-item :colOrder="formMeta.colOrder"></base-item>
父组件只需要设置 colOrder 即可。这是关于显示哪些字段的数组,因为表单控件需要分栏显示,那么一个栏目里有哪些字段,这个是动态数据,由分栏组件确定,不方便写入状态。
小结
折腾了好几天,效果似乎并不太理想,不过没关系,后续想出来更好的方式可以继续重构。
表单控件,基本就是这样了,后续是列表控件、查询控件等。
转载自:https://juejin.cn/post/7244351125448982584