Skip to content
New issue

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

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

Already on GitHub? Sign in 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/

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

No branches or pull requests

3 participants