稀土掘金 稀土掘金

基于 Fetch + TypeScript 实现一个完整可应用的 useFetchData

这是我参与8月更文挑战的第1天,活动详情查看: 8月更文挑战

前言

如题,本文想要实现一个完整的,可在大型项目中实际应用的一个 useFetchData Hooks,网上有很多类似的实现,但是都没有找到最合适自己的(一千个读者就有一千个哈姆雷特,并非人家实现的不好),所以干脆不如自己撸一个,在此记录一下过程,希望能对大家有点帮助。

【实现的功能如下】:

  • 自定义 hooks,使用方式统一,内部捕获错误,返回数据和 loading
  • 基于 Typescript 封装了 fetch,实现了 cancel 请求,前端超时等功能

先看demo: awesome-use-fetch-data_Demo

Demo 仓库地址 -> awesome-use-fetch-data_Repo

2021-08-09 20.37.53.gif

本文大体上分为如下几个步骤讲解实现过程:

    1 - 为什么要封装一个 useFetchData Hooks
    2 - 使用 Typescript 封装一个前端超时取消、页面销毁/路由跳转取消、带 loading 的 fetch 库
    3 - 结合二者,实现一个完整版的 useFetchData

为什么要封装一个 useFetchData Hooks

首先,就来说说为什么要实现这样一个 Custom Hooks 呢?原因有两点:

  • 第一,如果你项目里大量使用 Hooks 开发代码,就避免不了封装高复用的 Custom Hooks

如果你还是一直在复制粘贴代码,那么看完本篇文章,相信对你还是有些许裨益的。

  • 第二,在业务场景里,基本上每个页面大部分组件都会请求数据接口渲染,所以对请求进行高复用封装能提高开发效率

具体为什么能提高开发效率,我这边简单写了两个伪代码,参考如下:

  • 封装前
// page1.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';

export default function Page1() {
   const [data, setData] = useState([]);
   const [loading, setLoading] = useState(false);
   
   // 组件加载完毕,请求数据
   useEffect(() => {
      setLoading(true);
      axios.get('/user/list', { params: { ID: 12345 } })
      // 更新数据
      .then(function(res) {
          setData(res.data);
          setLoading(false);
      })
      .catch(function(error) {
          console.log(error);
          setLoading(false);
      });
   }, [])
   
   return (
       <Table loading={loading} data={data} columns={columns} />
   )
}

// page2.jsx
import { useState, useEffect } from 'react';
import axios from 'axios';

export default function Page2() {
   const [list, setList] = useState([]);
   const [loading, setLoading] = useState(false);
   
   // 组件加载完毕,请求数据
   useEffect(() => {
      setLoading(true);
      axios.post('/article/list', { page: 'Fred', pageSize: 'Flintstone'})
      .then(function (res) {
          setList(res.data);
          setLoading(false);
      })
      .catch(function (error) {
          console.log(error);
          setLoading(false);
      });
   }, [])
   
   return (
       <Table loading={loading} data={list} columns={columns} />
   )
}
  • 封装后
// page1.jsx

// page1.jsx
import { useState, useEffect } from 'react';
import useFetchData from 'use-fetch-data';

export default function Page1() {
   const [data, setData] = useState([]);
   
  const options =  { params: { ID: 12345 } };
  const { data, loading } = useFetchData('/user/list', options);
   
   return (
       <Table loading={loading} data={data} columns={columns} />
   )
}

// page2.jsx
import { useState, useEffect } from 'react';
import useFetchData from 'use-fetch-data';

export default function Page2() {
   
  const options =  { method: 'POST', data: { page: 'Fred', pageSize: 'Flintstone'}};
  const { data, loading } = useFetchData('/article/list', options);
   
   return (
       <Table data={data} columns={columns} />
   )
}

从上面两段代码,可以非常清晰的看出来封装前后的代码对比,封装前,每个业务组件在内部处理请求的同时,还要处理数据和 loading 状态,那么一个组件两个组件还好,当项目庞大起来,几十个接口几百个组件都进行请求,那么项目就有了大量的 CV 操作冗余代码。

由此可见一个 useFetchData Hooks 可以大大精简我们的业务代码,合理的将一个常用的组件或者方法抽象成 Custom Hooks 真的是一个非常好的选择,希望大家也能在平时自己用起来,构建自己的 react-use 之类的 Hooks Utils

介绍完封装一个 useFetchData Hooks 的充分理由,接下来就是正式进行代码封装了,既然是封装请求库,那么就必须选择一个请求库,我这里以个人比较喜欢的 fetch 为例,此 hooks 核心其实不是请求库,大家只要选择自己擅长的请求库就可以了。

基于 Typescript 封装一个多功能 fetch

上面提到了本文核心其实也不在封装 fetch 这里,因为 fetch 这个库并不是很多人喜欢用,可能大家很多人喜欢用 axios,也没关系,反正做的事情都是差不多的,到时候各位进行自己请求库的替换就可以了,我只是比较喜欢 fetch 而已,ts-fetch 实现了如下功能:

  • 1 - 内部处理异常(需要和后端约定好返回)

  • 2 - 前端自超时,当请求响应时间超过一定阈值,前端认为超时(可以通过传参覆盖)

  • 3 - 利用 AbortController 取消请求(页面销毁/路由跳转)

TS-Fetch 代码地址 -> useful-kit,个人 TypeScript 用的一般,如果有大佬们有更好的封装方法,可以留言或者仓库直接共建,非常感谢。

下面就直接贴封装的请求库代码,写文章,就只是简单的进行封装一下,大家在业务里还可以根据业务特性,再次进行改造到适合自己项目的程度,比如路径参数 api 这种也是可以支持的:

// 这个是同构 fetch,既能在服务端,又能在客户端
import fetch from 'isomorphic-unfetch';
// query 格式化的插件,其实可以自己实现
import qs from 'query-string';
// 捕获异常内部处理的一个提示,和你项目用的 ui 库一致就可以
import { message } from 'antd';

function filterObject(o: Record<string, string>, filter: Function) {
  const res: Record<string, string> = {};
  Object.keys(o).forEach(k => {
    if (filter(o[k], k)) {
      res[k] = o[k];
    }
  });
  return res;
};

export enum EHttpMethods {
  GET = 'GET',
  POST = 'POST',
  PUT = 'PUT',
  PATCH = 'PATCH',
  DELETE = 'DELETE'
}

type ICustomRequestError = {
  status: number;
  statusText: string;
  url: string;
}

function dealErrToast(err: Error & ICustomRequestError, abortController?: AbortController) {
  switch(err.status) {
    case 408: {
      abortController && abortController.abort();
      (typeof window !== 'undefined') && message.error(err.statusText);
      break;
    }
    default: {
      console.log(err);
      break;
    }
  }
}

/**
 * @description: 声明请求头header的类型
 */
interface IHeaderConfig {
  Accept?: string;
  'Content-Type': string;
  [propName: string]: any;
}

export interface IResponseData {
  code: number;
  data: any;
  message: string;
}

interface IAnyMap { 
  [propName: string]: any;
}

export interface IRequestOptions {
  headers?: IHeaderConfig;
  signal?: AbortSignal;
  method?: EHttpMethods;
  query?: IAnyMap;
  params?: IAnyMap;
  data?: IAnyMap;
  body?: string;
  timeout?: number;
  credentials?: 'include' | 'same-origin';
  mode?: 'cors' | 'same-origin';
  cache?: 'no-cache' | 'default' | 'force-cache';
}

/**
  * Http request
  * @param url request URL
  * @param options request options
  */
interface IHttpInterface {
  request<T = IResponseData>(url: string, options?: IRequestOptions): Promise<T>;
}

const CAN_SEND_METHOD = ['POST', 'PUT', 'PATCH', 'DELETE'];

class Http implements IHttpInterface {
  public async request<T>(url: string, options?: IRequestOptions, abortController?: AbortController): Promise<T> {
    const opts: IRequestOptions = Object.assign({
      method: 'GET',
      headers: {
        'Content-Type': 'application/x-www-form-urlencoded',
        Accept: 'application/json'
      },
      credentials: 'include',
      timeout: 10000,
      mode: 'cors',
      cache: 'no-cache'
    }, options);

    abortController && (opts.signal = abortController.signal);

    if (opts && opts.query) {
      url += `${url.includes('?') ? '&' : '?'}${qs.stringify(
        filterObject(opts.query, Boolean),
      )}`;
    }

    const canSend = opts && opts.method && CAN_SEND_METHOD.includes(opts.method);

    if (canSend && opts.data) {
      opts.body = JSON.stringify(filterObject(opts.data, Boolean));
      opts.headers && Reflect.set(opts.headers, 'Content-Type', 'application/json');
    }

    console.log('Request Opts: ', opts);

    try {
      const res = await Promise.race([
        fetch(url, opts),
        new Promise<any>((_, reject) => {
          setTimeout(() => {
            return reject({ status: 408, statusText: '请求超时,请稍后重试', url });
          }, opts.timeout);
        }),
      ]);
      const result = await res.json();
      return result;
    } catch (e) {
      dealErrToast(e, abortController);
      return e;
    }
  }
}

const { request } = new Http();

export { request as default };

高阶完整版的 useFetchData

上面已经封装完了一个基于 fetch 的请求库,接下来就是利用它写一个 useFetchData Hooks。这里思考如下:

  • 1 - 使用起来要简单,统一,尽可能的减少请求库逻辑代码在业务组件内
  • 2 - 在 hooks 内部进行错误处理,组件业务层级无需关心处理
  • 3 - 返回响应数据、异常错误和 loading

有个上面三点目标,接下来就是实现了,具体代码如下:

/**
 * /hooks/useFetchData.tsx
 */
import { useState, useEffect, useRef } from 'react';
import request, { IRequestOptions, IResponseData } from '../utils/request';

interface IFetchResData {
  data: T | undefined; 
  loading: boolean;
  error: any;
}

function useFetchData<T = any>(url: string, options?: IRequestOptions): IFetchResData {
  // 如果是一个通用的 fetchData,那么使用any是没办法的,如果只是针对list,any可以替换为对应的数据范型
  const [data, setData] = useState<T>();
  const [loading, setLoading] = useState<boolean>(false);
  const [error, setError] = useState<any>(null);
  /**
   * 超时或者页面销毁/路由跳转,取消请求
   */
  const abortControllerRef = useRef<AbortController>();

  function destory() {
    setData(undefined);
    setLoading(false);
    setError(null);
    abortControllerRef.current && abortControllerRef.current.abort();
  }

  useEffect(() => {
    setLoading(true);
    abortControllerRef.current = new AbortController();
    request(url, options || {}, abortControllerRef.current).then(res => {
      const { code, message, data } = res as IResponseData;
      if (code !== 0) {
        console.log('Error Msg: ', message);
        throw new Error(message);
      }
      setData(data);
      setLoading(false);
    }).catch(err => {
      setError(err);
    }).finally(() => {
      setLoading(false);
    });

    return () => destory();
  }, [url, JSON.stringify(options)]);

  return { loading, data, error };
}
 
export default useFetchData;

代码就是上面那个样子,我们来看看是不是满足上面四点目标:

1 - 使用起来简单、统一,减少请求逻辑在业务组件里

// 1 - 基础使用 GET 无参数
const { loading, data } = useFetchData('/user/list');

// 2 - 进阶使用 GET 有参数
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number | undefined>(10);

const options = { query: { page, pageSize } };
const { loading, data } = useFetchData(getUserList, options);

// 3 - 进阶使用 POST
const [page, setPage] = useState<number>(1);
const [pageSize, setPageSize] = useState<number | undefined>(10);

const options = useMemo(() => ({
method: EHttpMethods.POST,
data: { page, pageSize }
}), [page, pageSize]);

const { loading, data } = useFetchData(postUserList, options);

可以看到,使用上应该可以算是清晰简单,你只需要传递对应请求的参数和 api 地址即可获取到数据以及 loading 状态。并且请求的逻辑在业务组件里只有必要的 api url 和参数,这两个是无法减少的,至于其他逻辑,完全封装到了 hooks 内部。

这里需要注意的是,因为 options 也就是 hooks 的第二个参数 是一个对象,因此会存在一个问题,就是组件重复渲染的时候,hooks 会重复发请求,因为每一次 options 都是一个新对象,即使没有改变也是一个新的内存地址,所以为了避免这种情况,有两种解决方案。

  • 第一种:hooks 层解决,我这边使用了 useEffect + JSON.stringify 来确保参数发生变化才重新请求。
  • 第二种:业务层解决,业务代码使用 useMemo 来进行处理。 个人而言更倾向第二种,目前因为是 Demo 阶段,所以两种方法我里面都用了,大家按需使用即可。

2 - hooks 内部进行错误处理

错误处理集中在请求库和 hooks 内部,业务组件不需要关心以及处理错误,大大减少了业务代码复杂度,核心代码如下:

// 请求层错误处理
/**
 * 错误处理
 * @param err 
 * @param abortController 
 */
function dealErrToast(err: Error & ICustomRequestError, abortController?: AbortController) {
  switch(err.status) {
    case 408: {
      abortController && abortController.abort();
      (typeof window !== 'undefined') && message.error(err.statusText);
      break;
    }
    default: {
      console.log(err);
      break;
    }
  }
}

// hooks 层错误处理
const { code, message, data } = res as IResponseData;
if (code !== 0) {
    // do something
    console.log('Error Msg: ', message);
    throw new Error(message);
}

返回响应数据和 loading + TS 增强数据提示

这个看代码就很简单了,每一个请求都返回了响应的数据以及 loading 状态,同样的大大程度精简了业务代码,非常简便。

这里可以说明一下,为了清晰明了,我在 hooks 内部增设了 loading 这个 state,其实可以通过 dataerror 两个字段聚合出 loading 状态,这里大家可以参考 swr。不过我觉得增加一个 loading 更清晰,所以就这么写了。

接下来就是 TS 的好处了,如果你在数据模型里设置里返回的数据类型,那么你获取到的 data 就是一个带提示的数据,在业务开发里非常的好用,只需要在编辑器里 . 一下,就能看到返回的这个数据对象的各种属性,开发简直不要太爽,再也不用边开发边查阅 API 文档了!效果如下图:

image.png

具体代码如下:

// 数据接口层
export interface IUserStruct {
  id: number;
  name: string;
  age: number;
}


export interface IUserListResData {
  list: IUserStruct[];
  total: number
}

// 业务代码层
const { loading, data } = useFetchData<IUserListResData>(getLimitUserList, options);

最后你的 data 就如上图所示了~

总结

这篇文章也算是个人想总结想写很久的一篇文章了,毕竟我真的很喜欢用 fetch,但是在公司里很少有人用,所以意难平吧。本文说技术含量其实也没啥技术含量,更多的应该是经验分享吧,代码较多,建议大家 Clone 仓库自己跑一跑,希望对大家有用处。

玻璃钢生产厂家商场开业美陈孔雀造型杭州玻璃钢浮雕人物山水雕塑济源玻璃钢雕塑加工厂家玻璃钢 大象 雕塑莱芜公园玻璃钢雕塑厂家龙王玻璃钢雕塑深圳玻璃钢雕塑摆件批发宿迁玻璃钢人物雕塑批发德州玻璃钢雕塑公司陕西玻璃钢仿铜雕塑价格江苏走廊商场美陈供货商揭阳玻璃钢长颈鹿雕塑亳州定做玻璃钢雕塑厂价格上海玻璃钢雕塑制作厂定制椭圆形玻璃钢花盆延安小区玻璃钢雕塑厂家衢州学校玻璃钢雕塑制作郑州商场美陈植物墙金华玻璃钢海豚雕塑如皋玻璃钢雕塑生产厂家云南步行街玻璃钢雕塑市场标识玻璃钢雕塑定做价格福州园林玻璃钢雕塑采购云南标识标牌玻璃钢雕塑广州动物造型玻璃钢卡通熊猫雕塑汕头玻璃钢卡通雕塑推荐厂家贵州步行街玻璃钢雕塑生产厂家吉安玻璃钢雕塑工厂富阳哪里有做玻璃钢雕塑厂家黑龙江佛像玻璃钢雕塑设计香港通过《维护国家安全条例》两大学生合买彩票中奖一人不认账让美丽中国“从细节出发”19岁小伙救下5人后溺亡 多方发声单亲妈妈陷入热恋 14岁儿子报警汪小菲曝离婚始末遭遇山火的松茸之乡雅江山火三名扑火人员牺牲系谣言何赛飞追着代拍打萧美琴窜访捷克 外交部回应卫健委通报少年有偿捐血浆16次猝死手机成瘾是影响睡眠质量重要因素高校汽车撞人致3死16伤 司机系学生315晚会后胖东来又人满为患了小米汽车超级工厂正式揭幕中国拥有亿元资产的家庭达13.3万户周杰伦一审败诉网易男孩8年未见母亲被告知被遗忘许家印被限制高消费饲养员用铁锨驱打大熊猫被辞退男子被猫抓伤后确诊“猫抓病”特朗普无法缴纳4.54亿美元罚金倪萍分享减重40斤方法联合利华开始重组张家界的山上“长”满了韩国人?张立群任西安交通大学校长杨倩无缘巴黎奥运“重生之我在北大当嫡校长”黑马情侣提车了专访95后高颜值猪保姆考生莫言也上北大硕士复试名单了网友洛杉矶偶遇贾玲专家建议不必谈骨泥色变沉迷短剧的人就像掉进了杀猪盘奥巴马现身唐宁街 黑色着装引猜测七年后宇文玥被薅头发捞上岸事业单位女子向同事水杯投不明物质凯特王妃现身!外出购物视频曝光河南驻马店通报西平中学跳楼事件王树国卸任西安交大校长 师生送别恒大被罚41.75亿到底怎么缴男子被流浪猫绊倒 投喂者赔24万房客欠租失踪 房东直发愁西双版纳热带植物园回应蜉蝣大爆发钱人豪晒法院裁定实锤抄袭外国人感慨凌晨的中国很安全胖东来员工每周单休无小长假白宫:哈马斯三号人物被杀测试车高速逃费 小米:已补缴老人退休金被冒领16年 金额超20万

玻璃钢生产厂家 XML地图 TXT地图 虚拟主机 SEO 网站制作 网站优化