表格请求数据组件(ant design 版)

2,041 阅读5分钟

在中后台管理系统中,我们常常会遇到下面的业务场景:

那就是上面是搜索,下面是带分页的表格。ant design 组件库已经非常贴心的帮我们把表格组件和分页组件封装在了一起。我们只需要修改 column、dataSource 等属性就可以实现不同的页面。写的页面多了,会发现在仍然有一些重复逻辑,那就是请求数据的逻辑:页码修改的时候需要重新请求数据;查询条件修改的时候还要触发请求数据的逻辑。鉴于此,我们将这些逻辑抽取出来,借助 ant design 组件封装一个包含请求数据的表格组。效果:

初始化

npx create-react-app page-table
npm install antd -D

create-react-app 初始化项目,并安装 antd 组件库。

fetch 与 XMLHttpRequest

其实,我们可以用 umi-request、axios 发送请求。本文采用原生的 fetch 发送请求(只要是为了学习😄),推荐阮一峰的《Fetch API 教程》博客( www.ruanyifeng.com/blog/2020/1… )和 MDN 文档( developer.mozilla.org/zh-CN/docs/… )。

这里说下我是使用的几点总结:

  1. fetch 会返回一个 Promise 对象。即使后端返回的是 404 或者 500,不会报错,只是将返回Promise 对象标记为 resolved,且会将 resolve 的返回值的 ok 属性设置为 false。仅当网络故障时或请求被阻止时,才会标记为 reject。

  2. 跨域时,无论 fetch 还是 xhr 都不会自动带上 Cookie。fetch 请求需要设置 credentials: 'include'XMLHttpRequest 请求需要设置:xhr.withCredentials = true;

  3. 取消 fetch 请求,可以借助 AbortController 取消请求;而 XMLHttpRequest 请求是通过 xhr.abort() 取消请求。

  4. AbortController 取消 fetch 请求的时候会导致 fetch 返回的 Promise 状态为 reject。 具体看如下代码:

  const url = "http://localhost:8081/pageTable/v1/famousInfo2?current=1&pageSize=8"
  const xhr = new XMLHttpRequest();
  xhr.onreadystatechange = function () {
      console.log(xhr, '-- respone')
  }
  //请求带上 Cookie
  xhr.withCredentials = true;
  xhr.open('GET', url, true);
  xhr.send();
  // 取消请求
  setTimeout(() => {
      xhr.abort();
  }, 1500);

  const controller = new AbortController();
  const signal = controller.signal;
  fetch(url, {
      method: 'GET',
      mode: 'cors',
      credentials: 'include',
      signal: signal
  }).then(res => {
      console.log(res);
  });

  setTimeout(() => {
      controller.abort();
  }, 1500)

比较详细介绍 XMLHttpRequest 请求: segmentfault.com/a/119000000…

node 接口

使用 exress 编写了 Restful 风格的查询接口。设置跨域代码 👇:

// 设置跨域访问
app.all("*", function (request, response, next) {
  // 设置跨域的域名,* 代表允许任意域名跨域;http://localhost:3000 表示前端请求的 Origin 地址
  response.header("Access-Control-Allow-Origin", "http://localhost:3000");
  //设置请求头 header 可以加那些属性
  response.header('Access-Control-Allow-Headers', 'Content-Type, Content-Length, Authorization, Accept, X-Requested-With');
  // 设置跨域可以携带 Cookie 信息
  response.header('Access-Control-Allow-Credentials', "true");
  //设置请求头哪些方法是合法的
  response.header(
    "Access-Control-Allow-Methods",
    "PUT,POST,GET,DELETE,OPTIONS"
  );
  response.header("Content-Type", "application/json;charset=utf-8");
  next();
});

page-table

API

不卖关子,先上封装的表格组件(自带请求数据)的使用 API:

属性说明类型默认值
url必填。请求数据的 URL 地址String
columns必填。表格列的配置描述Array[ ]
pageInfo选填。请求的分页参数对象Object{current: 1,pageSize: 8}
params选填。请求的参数对象Object{}
isPagination选填。是否需要分页Booleantrue
type选填。第一列显示 check 还是 radio,可选值有 checkbox | radio。不填写什么也不显示。String
rowKey选填。表格行 key 的取值;默认会给表格数据添加 ROW_ID 字段作为行数据的 key。String"ROW_ID"

一个比较使用的事件 API:onSelectChange;如果第一列是 checkbox | radio 点击时的回调函数。

  const onSelectChange = (selectedRowKeys, selectedRows)=>{
      console.log(selectedRowKeys, selectedRows);
  }

参数 selectedRowKeys 表示点击选中数据行的 rowKeys 数组;selectedRows 表示点击选中数据行的 rows 数组。

发送数据请求

我们不难想到利用 useEffect 钩子函数在页面加载的时候发送请求,页面卸载的时候取消正在发送的请求。这里非常巧妙的三点:

  • 🍓 useEffect(()=>{ fetch() }, [url, params, pageOpitons]); 巧妙地将依赖的变量添加到第二个参数中。其中 url、params 是从外部父组件传递进来的参数,也就是父组件的 url 或者查询参数 params 修改的时候,会触发 page-table 组件重新请求后台数据。pageOpitons 是 page-table 组件持有的 state 数据,也就是 page-table 组件可以切换分页触发重新请求后台数据。

  • 🍒 对于 page-table 组件的分页功能,父组件也可以通过修改 pageInfo 去触发分页修改。本质上是 React Hook 中父组件 props 的修改触发子组件 state 变化的问题。怎么解决呢?看下面的代码 👇:

  // 在 useState 中初始化 pageOpitons 的值
  const [pageOpitons, setPageOptions] = useState({
    current: pageInfo && pageInfo.current ?  pageInfo.current : 1,
    pageSize: pageInfo && pageInfo.pageSize ? pageInfo.pageSize : 8
  });
  
  useEffect(()=>{
    // 当 pageInfo 变化且有值的时候,先判断是否和 pageOpitons 相同,不相同再进行赋值操作。
    // 避免重复赋值导致 fetch 数据重复请求。 
    // isEqual 函数判断 pageInfo, pageOpitons 的 key 和 value 是否一样;一样则表示是相同的。
    if(pageInfo && !isEqual(pageInfo, pageOpitons)){
      setPageOptions({
        current: pageInfo.current,
        pageSize: pageInfo.pageSize
      })
    }
  }, [pageInfo]);
// common.js
function isObject(obj) {
    return typeof obj === 'object' && obj !== null;
}
// 判断 obj1 和 obj2 相同:key 和 value 是否完全相同
export function isEqual(obj1, obj2) {
    if (!isObject(obj1) && !isObject(obj1)) {
        // 值类型比较
        return obj1 === obj2;
    }
    if (obj1 === obj2) {
        return true;
    }
    const keys1 = Object.keys(obj1);
    const keys2 = Object.keys(obj2);
    if (keys1.length !== keys2.length) {
        return false;
    }
    for (let key of keys1) {
        const res = isEqual(obj1[key], obj2[key]);
        if (!res) {
            return res;
        }
    }
    return true;
}
  • 🍍 页面切换的时候取消正在发送的请求。这个时候,上面提到的 AbortController 就起到作用了。利用 useEffect(()=>{ return ()=>{ abort();}}, []); 模拟 componentWillUnmount ,同时注意 ❗️ 使用 useRef 来保留 AbortController 对象,这样才能保证取消的时候是同一个。
  const abortControllerRef = useRef(new AbortController());
  //取消请求
  useEffect(() => {
    return () => abortControllerRef.current.abort()
  }, []);

源码地址

git: github.com/YY88Xu/page… 欢迎大家 fork,提宝贵建议 ❤️。