useAxios-使用TypeScript封装react-hook公共请求函数

前言

使用react函数组件搭配react-hook外加typeScript这样的组合方式去完成功能需求已经有一年多的时间了,对hook从陌生到熟悉的过程中个人也感觉受益良多。不得不说,react-hook采用声明式的语法定义相关变量,独立于函数存放的方式去引入状态并进行控制的代码逻辑可以很大程度上降低代码的耦合度,状态定义于函数头部,没有this,引用时无需重复声明。在对组件进行抽象时可以非常明确的归纳出哪一部分代码需要进行提取,外加TypeScript所提供的静态类型检查,对象接口声明,函数参数类型定义,方法或对象声明时可以定义范型,通过实际调用时传入的类型来对其进行替换,从而达到一段使用范型的程序能适用多种不同情况的目的。ts的出现无疑是对js注入灵魂的一个操作,有了typeScript的加持,可以大大增强项目的开发体验。

自定义useAxios钩子函数

项目开发中我们会把重复的代码进行抽象,维护一份公共的组件或函数,按需要导入并使用,比如公共的ajax请求函数。而现在有了react-hook,进而在接受了hook概念之后的表现之一就是喜欢将公用的方法封装成一个个以use开头的自定义hook函数,内部则基于react提供的顶层hook进行驱动。以下是自己在项目开发中一步步封装得出的useAxios钩子函数。

直接看效果,带你云吸猫 --> codesandbox

// useAxios.ts

import { useState, useEffect, useRef } from 'react';
import axios, {
  AxiosRequestConfig,
  AxiosResponse,
  CancelTokenSource,
  Canceler,
} from 'axios';

interface useAxiosConfig extends AxiosRequestConfig {
  cancelable?: boolean;
}

interface useAxiosReturnObj<T = any> {
  response?: AxiosResponse<T>;
  loading: boolean;
  error: boolean;
  cancelFn?: Canceler;
  refetchFn: (params?: any) => void;
}

// 表示元组具有一个可选的 boolean 类型数组项和随后任意多个任意类型元素
type useAxiosDeps = [boolean?, ...any[]];

const isPlainObject = (val: any): val is Object => {
  return !!val && typeof val === 'object' && val.constructor === Object;
};

/**
 * @template T 请求结果类型定义
 * @param { useAxiosConfig } config 请求配置,继承了 axios 的请求配置对象
 * @param { boolean } config.cancelable 是否需要取消请求的功能,设置为true时,外部可接收一个cancelFn方法对请求进行取消
 * @param { useAxiosDeps } deps 依赖项数组
 * @param { boolean } deps[0] 数组第一项为是否发起初始化请求
 * @return { useAxiosReturnObj } 返回值
 */

export const useAxios = <T = any>(
  config: useAxiosConfig,
  deps: useAxiosDeps
): useAxiosReturnObj<T> => {
  const [loading, setLoading] = useState<boolean>(false);
  const [response, setResponse] = useState<AxiosResponse<T>>();
  const [error, setError] = useState<boolean>(false);
  const { cancelable = false, ...axiosConfig } = config;

  const cancelRef = useRef<CancelTokenSource>();

  if (cancelable) {
    cancelRef.current = axios.CancelToken.source();
  }

  const fetch = (params?: any) => {
    if (!isPlainObject(params)) {
      params = {};
    }
    setLoading(true);
    axios
      .request<T>({
        ...axiosConfig,
        data: { ...axiosConfig.data, ...params },
        cancelToken: cancelRef.current?.token,
      })
      .then((res) => setResponse(res))
      .catch(() => setError(true))
      .finally(() => setLoading(false));
  };

  useEffect(() => {
    const [initialRequest = true] = deps;
    if (initialRequest) {
      fetch();
    }
  }, deps);

  useEffect(() => {
    return () => cancelRef.current?.cancel();
  }, []);

  const returnValues: useAxiosReturnObj = {
    response,
    loading,
    error,
    refetchFn: fetch,
  };

  if (cancelable) {
    returnValues.cancelFn = cancelRef.current?.cancel;
  }

  return returnValues;
};

复制代码

useAxios接收两个参数

config 请求配置

参数名称数据类型是否必填默认值描述
cancelablebooleanfalse是否需要取消请求的功能,传true的话可以接收一个名为 cancenlFn 取消正在发起的请求的方法

config参数还继承了axios请求库的AxiosRequestConfig对象,可自行 查看

deps 依赖项数组

参数名称数据类型是否必填默认值描述
arr[0]booleantrue数组的第一个参数为是否需要发起初始化请求,传false的话将不会在组件初次渲染时发起请求

返回值

参数名称数据类型描述
responseAxiosResponse<T>假设声明useAxios时传递了范型(T)定义响应结果,则响应对象的data属性即是传递的范型。关于AxiosResponse,请 查看
loadingboolean是否正在请求中,不论是否成功获取到数据,在请求结束后均会被设置为false表示请求完毕
errorboolean请求是否出错
cancelFn(message?: string): void假设声明useAxios时config参数配置了cancelable为true,则在请求过程中可调用此方法对正在请求中的ajax进行取消
refetchFn(params?: any) => void调用此方法可重新发起一次请求,可传递一个对象字面量参数对上一次的请求参数进行覆盖

示例

// Example.tsx

import React, { useState } from "react";
import { useAxios } from "./useAxios";

export interface CatObj {
  breeds: any[];
  id: string;
  url: string;
  width: number;
  height: number;
}

const Example = () => {
  const [initialRequest, setInitialRequest] = useState(true);
  const {
    loading: catLoading,
    response,
    cancelFn,
    refetchFn,
    error
  } = useAxios<CatObj[]>(
    {
      cancelable: true,
      url: "https://api.thecatapi.com/v1/images/search",
      params: {
        size: "small",
        limit: 6
      }
    },
    /**
     * 不传递initialRequest则发起初始化请求,在实际开发中,请求参数可能需要动态获取
     * 可以先声明请求函数,在请求条件满足之后,调用 setInitialRequest 将状态设置为true
     * 在useAxios内部的useEffect依赖会相应的发生变化并发起请求获取数据
     */
    [initialRequest]
  );

  const handleClick = () => {
    cancelFn && cancelFn();
  };

  if (error) {
    return <span>请求出错</span>;
  }

  return (
    <>
      <button onClick={refetchFn}>发起请求</button>
      <button onClick={handleClick}>取消请求</button>
      <div>
        {catLoading ? (
          <span>loading...</span>
        ) : (
          <>
            {response?.data ? (
              <div
                style={{
                  display: "flex",
                  flexFlow: "row wrap",
                  alignContent: "flex-start"
                }}
              >
                {response.data.map((item) => (
                  <div style={{ flex: "0 0 33%" }}>
                    <div
                      key={item.id}
                      style={{
                        width: "100%",
                        height: "300px",
                        background: `url(${item.url}) center / cover no-repeat`
                      }}
                    ></div>
                  </div>
                ))}
              </div>
            ) : (
              <span>空白缺省页</span>
            )}
          </>
        )}
      </div>
    </>
  );
};

export default Example;


复制代码

以上的useAxios钩子函数其实还有很多值得扩展的地方,比方我们在实际开发中会跟后端定义好response.data基础的响应格式,这时可将发起请求传递的范型修改为:

// useAxios.ts


// 1.定义一个公共响应的接口
interface responseData<T = any> {
  code: number;
  msg: string;
  data: T;
}

// 2.修改接收响应的变量数据类型
const [response, setResponse] = useState<AxiosResponse<responseData<T>>>();

// 3.修改发起请求时的request方法接收的范型
axios.request<responseData<T>>({
  	// something code
  })

复制代码

总结

声明式语法带来的好处是非常明确的代码逻辑。缺点是它并不能满足所有情况,比如有个场景是假设请求参数不满足请求条件,可能连声明useAxios的必要都没有。此方法适用于明确的知道打开一个页面时需要发起哪一些请求。这时候就可以在函数头部进行定义。

早期使用typeScript时也可能会带来很多的困惑,这个不行那个不行...,但是一旦度过了那段震荡期,个人的编程范式会得到极大的提升,它会一直伴你左右,陪着你度过这枯燥中又有很多惊喜发现的程序员生涯。

分类:
前端
标签: