Description
简介
为 useTable 能力集成插件扩展能力,方便用户扩展 Filter、排序、多选等能力,同时也方便上层建设对应的解决方案,比如配置化、数据驱动。
基础案例
下面演示一个简单的例子,以伪代码的方式。
const usePlugin = () => {
const [state, setState] = useState();
return {
middlewares: () => {},
props: () => {},
}
}
const Component = () => {
const plugin = usePlugin();
const { formProps, tableProps } = useTable(service, { plugins: [plugin] })
return (
<>
<Form {...formProps} />
<Table {...tableProps} />
</>
)
}
useTable 具备插件的扩展能力,每一个插件其实也是一个 Hook,可以定义为高级 Hook,它可以
- 管理状态;
- 中间件方式管理请求链路;
- 注入 props;
动机
在中后台业务上表单查询场景很多,基本上占了 60% 左右,如何梳理一个通用解决方案来提效是我们现在面临的问题。另外一个问题就是虽然场景类似但是中后台业务场景变化不可预测,我们需要提供一个灵活的可扩展机制来让更多人沉淀能力。
并且每一个功能点都涉及到几个维度的能力,既要可以管理状态又要可以在请求链路做一些个性处理。插件核心的理念是“Write One Do Everything”也就是只写一个地方就可以处理一个功能。下面举两个例子来分析:
1. 异步默认值
我们要实现的功能是“发一个请求取下拉数据 --> 取第一个值设置默认值 --> 请求参数加上对应的默认值 --> 表格的请求才能发送”,并且在重置的时候要保留默认值。
2. 多选
如果你要实现一个多选的功能的话,你需要做几件事情
- 设置 Table props,比如 rowSelection;
- 监听事件,比如 onChange;
- query 请求之后要清除选中项;
也就是我们要去做很多事情才能实现这个功能,如果只做一次还好,但是如果你每次开发都涉及多个页面,你如何把这些能力沉淀起来,并且可以组合使用呢?
下面会具体介绍 useTable 整体方案的设计。
设计细节
主要分三个主题来讲解:
- 内置协议设计
- 请求 Response 规范
- 组件 Props 协议
- 插件扩展能力设计
- 外部 API 设计
内置协议设计
请求 Response 规范
每一次请求的 Response 规范,useTable 会获取对应的值填充对应组件的 Props。
interface IResponse {
success : boolean;
msg: string;
data: {
dataSource: any[]; // 数据
total: number; // 总数
current: number, // 当前页
pageSize: number // 页大小
}
}
Props 协议
useTable 会返回一些组件的 Props,方便用户直接设置到对应组件。
Table
interface ITableProps {
dataSource: any[]; // 数据展示
loading: boolean; // 是否显示数据加载中
};
Pagination
interface IPaginationProps {
total: number; // 总共
pageSize: number; // 每页条数
current: number; // 当前页
onChange: (current: number) => void; // 页跳转事件
onPageSizeChange: (pageSize: number) => void; // 页大小切换事件
}
form
底层使用的是 formily
interface IFormProps {
actions: IFormActions;
effects: IFormEffect<any, any>;
}
插件扩展能力设计
上面“动机”举了两个例子,我们可以推导为了实现一个功能需要具备的能力:
能力 | 备注 | 方案 |
---|---|---|
管理状态 | 比如多选需要管理 selectedKey | Hook |
管理请求链路 | 比如多选的时候请求之后需要取消 selectedKey,还有设置异步默认值的时候可以控制请求什么时候触发,还有设置参数。 | Koa Middleware |
注入 Props | 比如多选的时候需要监听事件还有其他属性注入到 table 组件上 | Object Pipe/Compose |
Interface
插件的 Interface
type Middleware = (ctx, next) => Promise<any>
type PluginReturnValue = {
middlewares?: Middleware[] | Middleware,
props?: (props: object) => any | object
}
interface IPlugin = () => PluginReturnValue;
伪代码
下面用伪代码演示下简单的插件写法
const usePlugin = () => {
// 里面可以维护状态
const [state, setState] = useState();
return {
middlewares: (ctx, next) => {},
props: {}
}
}
想要自定义插件,需要了解下 middlewares 和 props 两个属性的意义和具体用法。
Middlewares
这个是 Koa 的洋葱模型,可以方便你设置参数,也可以方便你在请求前做一些处理,请求后做一些处理。写法跟你在写 Koa Middleware 一样,只是 ctx 内容不一样而已。具体 ctx 内容后面会介绍:
// 请求之前
const willQueryMiddleware = (ctx, next) => {
// 可以获取参数
// 这里处理请求前的处理
return next();
}
// 请求之后
const didQueryMiddleware = (ctx, next) => {
return next().then(() => {
// 请求之后做的处理,比如处理一些状态设置或者返回数据处理
});
}
Props
props 有两个功能,一个是自动合并 table、 form、pagination 的 props,另外一个是为了暴露功能到外界,可以让外界使用,比如一些获取的数据。
props 可以两种表现形式,一个是对象的方式,另外一个是函数的方式。
const props = {
tableProps: {},
formProps: {},
paginationProps: {},
// 其他的,名字随意
getXy: {},
}
如果你要获取 ctx 的话,可以通过函数的方式
const props = (ctx) => {
return {
tableProps: {},
formProps: {},
paginationProps: {},
// 其他的,名字随意
getXy: {},
}
}
tableProps、formProps、paginationProps 这三个是特殊的属性,useTable 会检测并且合并到对应的 props 上。比如
const usePlugin = () => {
return {
props: {
tableProps: {
test: 1
}
}
}
}
const { tableProps } = useTable(service, { plugins: [usePlugin()] });
// 这个时候 tableProps 的 test 属性为 1
formProps、paginationProps 以此类推。
Ctx
这个是 middleware 还有 props 为函数的时候注入的 ctx 的 interface 定义
interface ICtx {
// 元信息
meta: {
// 请求的来源,有可能是点击查询,有可能是重置等等
queryFrom: string,
};
// 设置状态 & 重新渲染
actions: IFromActions;
// 每一次请求要缓存的数据
store: object;
helper: object;
// 可以手动触发请求
query: () => Promise<any>;
// 请求参数
params: object;
// 响应数据
response: object;
}
外部 API 设计
这个是外界用户最为感知的 API 设计
- service 是一个请求源,返回 Promise;
- options 是一些可选项,具体下面的 interface 有解释;
- deps 每次 deps 一更新,会重新发送请求;
type Obj = { [name: string]: any };
interface Options {
current?: number; // 默认从第几页请求
pageSize?: number; // 默认页码大小
autoFirstQuery?: boolean; // 是否第一次发送
plugins?: PluginReturnValue[]; // 插件集合
}
interface ReturnValue {
formProps: IFormProps; // 上面 form props 协议
tableProps: ITableProps; // 上面 table props 协议
paginationProps: IPaginationProps; // 上面 pagination props 协议
query: () => Promise<any>; // 调用 query 可以重新请求
getParams: () => any; // 获取请求成功的参数
}
function useTable(service: (params?: Obj) => Promise<any>, deps?: any[]): ReturnValue;
function useTable(
service: (params?: Obj) => Promise<any>,
options?: Options,
deps?: any[],
): ReturnValue;
缺点
Hooks 通病也会出现,比如经常遇到的问题
FAQ
- 配置化不香吗,为什么要弄插件方案呢?其实插件化是作为底座,可以上层方便其他建设,配置化也是可以基于插件方案。而且配置化很难推广,因为配置会越来越多,但是作为局部能力沉淀是一个不错选择。
- 插件会冲突吗?插件定义是特定功能的增强,遵循的是单一职责。