Skip to content
New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

【RFC】useTable Hook 插件 #465

Closed
monkindey opened this issue Jun 29, 2020 · 10 comments
Closed

【RFC】useTable Hook 插件 #465

monkindey opened this issue Jun 29, 2020 · 10 comments
Assignees
Labels
feature New feature or request

Comments

@monkindey
Copy link
Collaborator

monkindey commented Jun 29, 2020

简介


为 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

  • 配置化不香吗,为什么要弄插件方案呢?其实插件化是作为底座,可以上层方便其他建设,配置化也是可以基于插件方案。而且配置化很难推广,因为配置会越来越多,但是作为局部能力沉淀是一个不错选择。
  • 插件会冲突吗?插件定义是特定功能的增强,遵循的是单一职责。
@brickspert
Copy link
Collaborator

  1. 名字叫 useTable,但是文章内还是叫 useTableQuery
  2. 如上次讨论所说,formProps 不建议作为底层能力,可以作为一个官方插件。
  3. paginationProps 的字段,建议和 fusion 或 antd 保持一致,没必要重新起个名字。比如 pageIndex => current

deps 每次 deps 一更新,会重新发送请求;

  1. API 设计中建议不要 deps,而使用 options.refreshDeps 代替,和 useRequset 一样。否则用户会有歧义,以为 service 中用到的变量都必须放到 deps 中。 useRequest 最初就是这么设计的,后来改掉了。

@brickspert
Copy link
Collaborator

十!分!期!待!~~ ✿✿ヽ(°▽°)ノ✿

@monkindey
Copy link
Collaborator Author

monkindey commented Jun 29, 2020

@brickspert

  • 第一点 已改
  • 第二点 我想按照你的思路想,看看能不能实现;
  • 第三点 paginationProps 就是 current,而不是 pageIndex
interface IPaginationProps {
  total: number; // 总共
  pageSize: number; // 每页条数
  current: number; // 当前页
  onChange: (pageIndex: number) => void; // 页跳转事件
  onPageSizeChange: (pageSize: number) => void; // 页大小切换事件
}
  • 第四点之前改成 refreshDeps 的考虑是?

@brickspert
Copy link
Collaborator

brickspert commented Jun 30, 2020

  • 文中很多地方是 pageIndex,为什么不是 current 呢?这里不太理解。
  • useEffect 的第二个参数有两个含义:1. 参数变化,重新执行。 2. 函数中用到的所有参数,必须放到 deps 中。
    而我们的 deps,只有第一层含义,没有第二层含义。
    可能和我们的 Hooks 开发方式有关,我们所有的函数都用 usePersistFn 包了一次。

@monkindey
Copy link
Collaborator Author

@brickspert

  • 你们后端返回都是 current 不是 pageIndex 吗?我们这边都是 pageIndex 😅,我现在全部改成 current 了
  • useEffect 居然还有两层意思?第二层意思应该是“建议”吧

@brickspert
Copy link
Collaborator

brickspert commented Jun 30, 2020

useEffect 居然还有两层意思?第二层意思应该是“建议”吧

第二层不是建议吧,如果不放到 deps 里面,会造成闭包问题。并且如果使用了 react 提供的 vscode 插件,不写还会报警告。

我举个例子来对比下 useEffect 的 deps 和 useRequest 的 refreshDeps

  1. useEffect deps
const [state, setState] = useState('hello');
const [id, setState] = useState(1);

useEffect(()=>{
    const timer = setTimeout(()=>{
        console.log(id, state);
    }, 1000);
   return ()=> clearTimeout(timer);
}, [id]);

上面的例子是不对的,每次打印的 state 很可能不是最新的。
所以 useEffect 中用到的所有外部依赖,都要放在第二个参数中。防止出现闭包问题吧,这也是官方的建议。

  1. useRequest 的实现原理大概如下
const [state, setState] = useState('hello');
const [id, setState] = useState(1);

const persistFn = usePersistFn(()=>{
     setTimeout(()=>{
        console.log(id, state);
    }, 1000);
});

useEffect(()=>{
    persistFn();
}, []);

通过 usePersistFn 包装,避免了闭包问题。也就不需要把依赖放在 deps 中了。
然后我们加了一个 refreshDeps,来提供变化重新执行的能力。应该和 useTable 要实现的能力一致。

不知道你能看懂我说的么~好像有点绕。。哈哈

@brickspert
Copy link
Collaborator

你们后端返回都是 current 不是 pageIndex 吗?我们这边都是 pageIndex 😅,我现在全部改成 current 了

这个只要统一就没问题。但是不能有些地方是 current,有些地方是 pageIndex

@monkindey
Copy link
Collaborator Author

monkindey commented Jul 1, 2020

@brickspert 那就最后的 API 应该是

interface Options {
  current?: number; // 默认从第几页请求
  pageSize?: number; // 默认页码大小
  autoFirstQuery?: boolean; // 是否第一次发送
  plugins?: PluginReturnValue[]; // 插件集合
  refreshDeps?: any[]; // 里面的值一变就会重新发请求
}

function useTable(
  service: (params?: Obj) => Promise<any>,
  options?: Options,
): ReturnValue;

@brickspert
Copy link
Collaborator

+1

@monkindey monkindey self-assigned this Jul 4, 2020
@awmleer awmleer added the feature New feature or request label Jul 13, 2020
@monkindey
Copy link
Collaborator Author

fixed by https://usetable-ahooks.js.org/

# for free to join this conversation on GitHub. Already have an account? # to comment
Labels
feature New feature or request
Projects
None yet
Development

No branches or pull requests

3 participants