Skip to the content.

@sinoui/use-rest-page-api

npm version downloads

@sinoui/use-rest-page-api 旨在简化分页列表和 RESTful CRUD API 交互的状态管理。

它可以帮助我们:

目录:

安装

yarn add @sinoui/use-rest-page-api

或者

npm i --save @sinoui/use-rest-page-api

快速使用

import React from 'react';
import useRestPageAPi from '@sinoui/use-rest-page-api';

interface User {
  userId: string;
  userName: string;
}

function ListDemo() {
  const dataSource = useRestPageApi<User>('/apis/users');

  return (
    <div>
      {dataSource.isLoading && <div>正在加载人员列表数据...</div>}
      <h1>人员列表</h1>
      {dataSource.items.map((user) => (
        <div key={user.userId}>{user.userName}</div>
      ))}

      <button type="button" onClick={() => dataSource.fetch(1)}>
        加载第二页数据
      </button>
    </div>
  );
}

RESTful CRUD API

以 Spring MVC 的分页格式为例说明 RESTful CRUD API。@sinoui/use-rest-page-api 默认集成了 Spring MVC 分页格式的解析器,同时支持扩展,自定义解析器,以支持其他类型的 API。

假定我们要维护一组人员数据,获取人员列表的 url 是/users

获取分页数据

请求

GET /users?sex=male&size=10&page=0&sort=firstName&sort=lastName,desc

请求参数说明:

注意:这是@sinoui/use-rest-page-api 默认发送分页查询请求的格式,你的 RESTful API 如果不是这样的,那么你需要定制分页查询请求

响应

后端返回 json 格式数据,数据如下:

{
  "content": [
    {
      "id": "1",
      "firstName": "",
      "lastName": "",
      "sex": "male"
    },
    {
      "id": "2",
      "firstName": "",
      "lastName": "",
      "sex": "male"
    }
    // 此处省去8条数据
  ],
  "totalElements": 540, // 符合条件的所有用户数量
  "size": 15, // 页大小,可以没有,默认与请求中的size一致
  "number": 0, // 当前第几页的意思,可以没有,默认与请求的page一致
  "totalPages": 54 // 一共多少页,可以没有,默认为`Math.ceil(totalElements / size)`
}

解释一下各属性的含义:

注意:如果你的 API 响应的数据格式不是这样的,那么你可以定制分页查询响应转换器,将 API 响应数据转换成上面说的数据格式即可。

获取单个数据

有时为了展现详情数据,而列表返回的数据不是很全,这时你就需要通过 API 获取单个数据。

请求

按照 RESTful 风格设计的 API,请求如下:

GET /users/1

响应

返回 JSON 格式数据。

{
  "id": "1",
  "firstName": "张",
  "lastName": "三",
  "sex": "male",
  "birthday": "1999-01-12"
}

注意:如果你的 API 响应数据格式不一致,你可以通过定制请求单个数据响应转换器,来转换成这样的数据格式。

新增数据

请求

POST /users

请求体是 JSON 格式的数据:

{
  "firstName": "王",
  "lastName": "五",
  "sex": "female",
  "birthday": "2000-08-12"
}

注意:如果你的 API 请求数据格式不一致,你可以通过定制新增请求的数据转换器,将上面的数据格式转换成满足你的 API 的数据格式。

响应

返回 JSON 格式的数据:

{
  "id": "3",
  "firstName": "王",
  "lastName": "五",
  "sex": "female",
  "birthday": "2000-08-12"
}

注意:如果你的 API 响应数据格式不一致,你可以通过定制新增响应的数据转换器,将上面的数据格式转换成满足你的 API 的数据格式。

更新数据

请求

PUT /users/3

请求体是 JSON 格式数据:

{
  "id": "3",
  "firstName": "王",
  "lastName": "五",
  "sex": "male",
  "birthday": "2000-08-12"
}

注意:如果你的 API 请求数据格式不一致,你可以通过定制更新请求的数据转换器,将上面的数据格式转换成满足你的 API 的数据格式。

响应

返回 JSON 格式的数据:

{
  "id": "3",
  "firstName": "王",
  "lastName": "五",
  "sex": "male",
  "birthday": "2000-08-12"
}

注意:如果你的 API 响应数据格式不一致,你可以通过定制更新响应的数据转换器,将上面的数据格式转换成满足你的 API 的数据格式。

删除数据

请求

删除单个数据:

DELETE / users / 1

删除多条数据:

DELETE /users/1,2,3

注意:如果你的 API 不支持删除多条数据,那么请设置options.useMultiDeleteApifalse

响应

返回 200、201 等 2xx 状态码表示删除成功即可。

数据结构

分页与排序信息

排序:

interface SortInfo {
  direction: 'desc' | 'asc';
  property: string;
}

分页:

interface PageInfo {
  /**
   * 一共有多少条数据
   */
  totalElements: number;
  /**
   * 页大小,表示一页有多少条数据
   */
  pageSize: number;
  /**
   * 第几页,从0开始
   */
  pageNo: number;
  /**
   * 排序信息
   */
  sorts: SortInfo[];
}

分页查询响应

useRestPageApi 默认认为分页列表查询的数据结构如下:

interface PageResponse<T> {
  /**
   * 数据列表
   */
  content: T[];
  /**
   * 总大小
   */
  totalElements: number;
  /**
   * 一页显示多少条结果
   */
  size: number;
  /**
   * 当前是第几页
   */
  number: number;

  /**
   * 总页数,可以没有。如果没有,则等于`Math.ceil(totalElements/size)`。
   */
  totalPages?: number;
}

useRestPageApi 参数说明

const dataSource = useRestPageApi<T, PageData>(
    url: string,
    defaultValue?: PageData<T>,
    options?: Options
);

url

指定加载列表数据的url,一般为 RESTful CRUD API 中加载列表的url,也就是基础 url。加载列表数据的 url 与基础 url 不一致,可以通过options.baseUrl设定基础 url。

defaultValue

指定默认的列表分页数据,默认为:

[];

options

配置:

转换器可以用来定制你的 API 细节。会用一个章节来介绍。

转换器

如果你的 API 数据格式与@sinoui/use-rest-page-api 默认支持的不同,那么你可以使用转换器来实现定制,让@sinoui/use-rest-page-api 为你的 API 服务。

定制分页查询请求

使用transformListRequest来定制分页列表查询请求。例如下面的转换器:

import qs from 'qs';

export default function transformListRequest(
  searchParams: {
    [key: string]: string;
  },
  pageInfo: PageInfo,
) {
  return qs.stringify(
    {
      ...searchParams,
      pageSize: pageInfo.pageSize,
      pageNo: pageInfo.pageNo,
      sort: pageInfo.sorts.map(
        (sortInfo) =>
          `${sortInfo.property}${sortInfo.direction === 'desc' ? '_desc' : ''}`,
      ),
    },
    {
      arrayFormat: 'comma',
    },
  );
}

应用这个转换器后,发送的分页列表查询将会是下面的格式:

GET /users?sex=male&size=10&page=0&sort=firstName,lastName_desc

推荐使用qs来处理请求参数的序列化和解析。这里用到了arrayFormat配置,设定为comma,那么遇到数组时,则会采用”,”的方式将多个数据连接在一起。arrayFormat 的几个参数如下所示:

qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'indices' });
// 'a[0]=b&a[1]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'brackets' });
// 'a[]=b&a[]=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'repeat' });
// 'a=b&a=c'
qs.stringify({ a: ['b', 'c'] }, { arrayFormat: 'comma' });
// 'a=b,c'

transformListRequest 方法结构如下:

interface SearchParams {
  [key: string]: any;
}

/**
 * 转换列表查询的请求
 *
 * @param searchParams 查询条件
 * @param pageInfo 分页信息
 *
 * @return {string} 返回列表查询请求的查询字符串。需要是字符串格式的。
 */
function transformListRequest(
  searchParams: SearchParams,
  pageInfo: PageInfo,
): string;

定制分页查询响应转换器

使用transformListResponse来转换分页列表查询响应的数据格式。如下所示的Hacker News API转换器:

interface HackerNew {
  objectID: string;
  title: string;
  url: string;
  auth: string;
  tags: string[];
}

interface HackerNewsListResponse {
  hits: HackerNew[];
  nbHits: number;
  page: number;
  nbPages: number;
  hitsPerPage: number;
}

function transformListResponse(
  response: HackerNewsListResponse,
): PageResponse<HackerNew> {
  return {
    content: response.hits,
    totalElements: response.nbHits,
    number: response.page,
    totalPages: response.nbPages,
    size: response.hitsPerPage,
  };
}

transformListResponse 函数的结构如下:

function transformListPresponse<T, Response>(
  response: Response,
): PageResponse<T>;

定制请求单个数据响应转换器

使用transformFetchOneResponse定制请求单个数据响应的数据格式。例如下面的示例:

interface User {
  userId: string;
  firstName: string;
  lastName: string;
}

interface Response {
  result: User;
  status: boolean;
}

function transformFetchOneResponse(response: Response): User {
  return response.result;
}

transformFetchOneResponse 函数的结构如下:

function transformFetchOneResponse<T, Response>(respone: Response): T;

定制新增请求的数据转换器

使用transformSaveRequest定制新增数据请求转换器。例如:

interface SaveUserInfo {
  user: User;
  time: long;
}

function transformSaveRequest(user: User): SaveUserInfo {
  return {
    user,
    time: new Date().getTime(),
  };
}

transformSaveRequest 函数的数据结构如下:

function transformSaveRequest<T, NewRequestData>(
  data: T,
  headers: { [key: string]: string },
): NewRequestData;

定制新增响应的数据转换器

使用transformSaveResponse定义新增响应的数据格式转变。例如:

/**
 * 新增API响应返回数据的结构
 */
interface ResponseData {
  result: User;
  status: boolean;
  errorMessage?: string;
}

function transformSaveResponse(responseData: ResponseData): User {
  if (response.status) {
    return response.result;
  }

  throw new Error('获取数据失败');
}

transformSaveResponse 函数的数据结构如下:

function transformSaveResponse<T, Response>(response: Response): T;

定制更新请求的数据转换器

使用transformUpdateRequest定制更新请求。用法与transformSaveRequest一致。

定制更新响应的数据转换器

使用transformUpdateResponse定制更新请求。用法与transformSaveResponse一致。

定制删除响应的数据转换器

使用transformRemoveResponse转换删除数据的响应格式:

/** 删除API的响应数据结构 */
interface ResponseData {
    code:string;
    msg:string;
}

function transformRemoveResponse(response:ResponseData):void {
    if(code==='200'){
        alert('删除成功');
    } else {
        alert('删除失败')
        throw new Error(response.msg);
    }
}

dataSource 的属性和方法

const dataSource = useRestPageApi<User, ListRawResponse>('/users');

我们的组件可以通过dataSource与查询结果、查询条件、RESTful API 进行沟通。

获取查询数据

 // 获取当前页列表数据
const users: User[] = dataSource.items;

// 获取id为'1'的数据
const user: User = dataSource.getItemById('1');

// 更新id为'1'的数据
const newUser = {...user, 'sex': 'female'};
dataSource.updateItem(newUser);

// 更新部分字段
dataSource.setItem('1', 'sex', 'female');
dataSource.setItem('1', { birthday: '2000-10-12' });

//替换items
dataSource.setItems([{id:'1',birthday:'2019-01-01'},{id:'2',age:32}])

// 新增
dataSource.addItem({id: '5', firstName: '' lastName: ''});

// 删除id为'1'的数据
dataSource.removeItemById('3');

// 删除多条数据
dataSource.removeItemsByIds(['1', '2', '3']);

// 删除指定行的数据,从0开始
dataSource.removeItemAt(5)

// 获取原始响应数据
const rawResponse = dataSource.rawResponse;

// 获取是否正在加载列表数据的状态
const isLoading = dataSource.isLoading;

// 获取是否加载列表数据失败的状态
const isError = dataSource.isError;
// 设置默认查询条件
dataSource.setDefaultSearchParams({userName:'张三'});

注意:这里介绍的getItemByIdupdateItemsetItemaddItemremoveItemById这些方法只会与dataSource.items进行交互,不会与 RESTful CRUD API 进行交互。如果需要与 RESTful CRUD API 交互,参见与增删改查 API 交互

分页和排序

// 获取分页信息
const pageInfo: PageInfo = dataSource.pagination;

// 获取下一页数据
dataSoure.nextPage();

// 获取上一页数据
dataSource.prevPage();

// 获取第10页数据
dataSource.fetch(9);

// 按照姓氏倒序排序
dataSource.sortWith([
  {
    property: 'firstName',
    direction: 'desc',
  },
  {
    property: 'lastName',
    direction: 'asc',
  },
]);

列表查询

// 根据查询条件获取数据
dataSource.query(searchParams);

// 获取查询条件
dataSource.searchParams;
// 获取默认的查询条件
dataSource.defaultSearchParams;

// 获取第5页数据
dataSource.fetch(4);

// 重新获取当前页的数据
dataSource.reload();

fetch()方法是查询列表的基础方法,它的语法格式如下:

function fetch<T>(
  pageNo?: number,
  pageSize?: number,
  sort?: SortInfo[],
  searchParams?: SearchParams,
): PageResponse<T>;

与增删改查 API 交互

// 获取id为'1'的数据
const user = await dataSource.get('1');

// 新增用户数据
const user = await dataSource.save({ firstName: '', lastName: '' });

// 修改用户数据
const user = await dataSource.update({
  id: '1',
  firstName: '',
  lastName: '',
});

// 删除数据
await dataSource.remove('1');

// 删除多条数据
await dataSource.remove(['1', '2', '3']);

以上操作默认均会修改dataSource.items。如果不需要更新,则可以指定函数的第二个参数为false,如:

const user = await dataSource.get('1', false);