菜鸟对于用ts构建axios的总结

我花了几天时间去学习ts,并学习用ts构建axios,先说说整体感受,前期与后期还是相对中期是比较好理解,前期是基础,后期补充,中期就是重中之重了。细的不多说,就说说自己学习过程中该如何分析,这是我写的源码地址

处理url参数问题

我们需要知道该如何将params传来参数拼接到url上,最简单的方式是直接将key与value拼接到url上:

/base/get?a=1&b=2

如果是数组的话:

axios({
  method: 'get',
  url: '/base/get',
  params: {
    foo: ['bar', 'baz']
  }
})

/base/get?foo[]=bar&foo[]=baz’

参数为对象时

axios({
  method: 'get',
  url: '/base/get',
  params: {
    foo: {
      bar: 'baz'
    }
  }
})

**/base/get?foo=%7B%22bar%22:%22baz%22%7D **

参数为Date时

const date = new Date()

axios({
  method: 'get',
  url: '/base/get',
  params: {
    date
  }
})

** /base/get?date=2019-04-01T05:55:39.030Z**

参数特殊字符时

对于字符 @、:、$、,、、[、],我们是允许出现在 url 中的

axios({
  method: 'get',
  url: '/base/get',
  params: {
    foo: '@:$, '
  }
})

/base/get?foo=@:$+

传入空值忽略

axios({
  method: 'get',
  url: '/base/get',
  params: {
    foo: 'bar',
    baz: null
  }
})

/base/get?foo=bar

url中带有哈希值

axios({
  method: 'get',
  url: '/base/get#hash',
  params: {
    foo: 'bar'
  }
})

/base/get?foo=bar

如果已经有参数

axios({
  method: 'get',
  url: '/base/get#hash',
  params: {
    foo: 'bar'
  }
})

/base/get?foo=bar

既然知道url该如何拼接,接下来就需要在src/helpers/url.ts实现url拼接功能,在处理url之前,我们需要几个工具函数,在src/helpers/utils

const toString = Object.prototype.toString;
//需要注意 我们在这用了类型保护 val is Date,
//而不用boolean是因为在之后我们可以方便调用val的方法
//大家可以查查“类型保护”的好处,我就不多说了
export function isDate(val: any): val is Date {
    return toString.call(val) === "[object Date]";
}
// 也可能传入的是URLSearchParams对象
export function isURLSearchParams(val: any): val is URLSearchParams {
    return typeof val !== "undefined" && val instanceof URLSearchParams
}
// 判断是否为普通对象
export function isPlainObject(val: any): val is Object {
    return toString.call(val) === "[object Object]";
}


import {isDate,isPlainObject, isURLSearchParams } from "./utils";

// 该函数是把字符串作为 URI 组件进行编码,而其中刚刚所说到的特殊字符不做处理
function encode(val: any): string {
    return encodeURIComponent(val).
        replace(/%40/gi, '@').
        replace(/%3A/gi, ':').
        replace(/%24/g, '$').
        replace(/%2C/gi, ',').
        replace(/%20/g, '+').
        replace(/%5B/gi, '[').
        replace(/%5D/gi, ']');
}

//该函数是将参数与url做拼接
export function buildURL(url: string, params?: any, paramsSerializer?: (params: any) => string): string {
// 当不存在参数直接将其返回
    if (!params) {
        return url;
    }
    // 利用一个变量存放处理后的参数
    let serializedParams = "";
    // paramsSerializer是一个负责 `params` 序列化的函数,
    //用户自定义决定将如何处理参数
    //如果用户传入了这个函数,就直接用该函数处理参数
    if (paramsSerializer) {
        serializedParams = paramsSerializer(params);
    } else if (isURLSearchParams(params)) {
    // 如果用户传入的是URLSearchParams对象,那我们就直接转换字符串就行了
        serializedParams = params.toString();
    } else {
        const parts: string[] = [];
        //遍历params对象
        Object.keys(params).forEach(key => {
            let vals = [];
            let val = params[key];
            //如果params中传入空值我们就直接返回,遍历下一个值
            if (!val) {
                return
            }
            //如果传入的是数组,我们将其直接赋值给创建的空数组
            if (Array.isArray(val)) {
                vals = val;
                key += "[]";
            } else {
            //如果只是普通的值,我们就将其变成一个length为1的数组
            //这样做的好处是方便我们在后续处理val,不管它是数组还是单个值,
            //都可以遍历获取处理val
                vals = [val];
            };
      
            vals.forEach(val => {
            // 如果为Date,我们将其转换为ISO格式后的字符串
                if (isDate(val)) {
                    val = val.toISOString();
                }
                //如果为对象,将其转换为json字符串
                if (isPlainObject(val)) {
                    val = JSON.stringify(val);
                }
                //将其拼接好后放入创建的数组
                //放入数组的好处是我们可以在后面用"&"将其拼接成完整的字符串
                parts.push(`${encode(key)}=${encode(val)}`);
            })
        })
        serializedParams = parts.join("&");
    }
//如果存在处理好的参数
    if (serializedParams) {
    //判断url中是否有哈希值
        const markIndex = url.indexOf("#");
        if (markIndex !== -1) {
        //存在哈希值,则截取url
            url = url.slice(0, markIndex);
        }
        //在拼接到url时,需要判断url是否带有参数
        url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams;
    }
    // 将其返回
    return url;
}

这段代码实现的逻辑其实并不难,重点是在于是否能够像到各种各样的情况,然后再根据情况进行相应的判断。

需要处理哪些错误?

  • 处理网络异常错误
request.onerror = function handleError() {
  //
}
  • 处理超时错误
//用户是否传入超时时间
if (timeout) {
  request.timeout = timeout
}

request.ontimeout = function handleTimeout() {
  //
}
  • 处理非200状态码
if (response.status >= 200 && response.status < 300) {
  //
 } else {
   //
 }
}

我们可以创建一个类来获取更多的错误信息。
先定义一个接口在src/types/index.ts:

export interface AxiosError extends Error {
    isAxiosError: boolean;
    //配置信息
    config: AxiosRequestConfig;
    //错误码
    code?: string | null;
    //请求信息
    request?: any
    //响应信息
    response?: AxiosResponse
}

需要注意的是在我们创建AxiosError类的时候, Object.setPrototypeOf(this, AxiosError.prototype)是必须的,听说是ts自身的问题

export class AxiosError extends Error {
    isAxiosError: boolean
    config: AxiosRequestConfig
    code?: string | null
    request?: any
    response?: AxiosResponse
    constructor(message: string, config: AxiosRequestConfig,
        code?: string | null,
        request?: any,
        response?: AxiosResponse, ) {
        super(message);
        this.config = config;
        this.code = code;
        this.request = request;
        this.response = response;
        this.isAxiosError = true;
        Object.setPrototypeOf(this, AxiosError.prototype);
    }
}

我们可以用一个工厂函数来方便创建多个AxiosError实例

export function createError(message: string, config: AxiosRequestConfig,
    code?: string | null,
    request?: any,
    response?: AxiosResponse, ) {
    return new AxiosError(message, config,
        code,
        request,
        response);
}

混合对象实现

混合对象:首先这个对象是一个函数,并且它拥有一个类的的所有原型属性和实例属性。而axios就是一个混合对象,下面两种方式都可以:

// 发送 POST 请求
axios({
  method: 'post',
  url: '/user/12345',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});
// 发送 POST 请求
axios.post('/user/12345',{
  method: 'post',
  data: {
    firstName: 'Fred',
    lastName: 'Flintstone'
  }
});

我们需要做的是将Axios类与axios函数结合,就需要定义一个辅助函数在helpers/utils.ts下:

//to 为函数,from 为对象
export function extend<T, U>(to: T, from: U): T & U {
    for (const key in from) {
    //这里我们需要将获取的值强制转换一下,否则会报错
        ; (to as T & U)[key] = from[key] as any;
    }
    return to as T & U;
}

创建混合对象

function createInstance(): AxiosStaic{
  	//创建实例
  	const context = new Axios()
  	//创建函数,并将创建的实例绑定到该函数上,让函数的this指向这个实例
  	const instance = Axios.prototype.request.bind(context)
	//将实例的属性和方法绑定到该函数上
 	extend(instance, context)
//将其返回
  return instance as AxiosStaic
}

const axios = createInstance()

export default axios

我们通过工厂函数创建一个axios混合对象

在接口中添加泛型好处

export interface AxiosResponse<T = any> {
    data: T;
    status: number;
    statusText: string;
    headers: any;
    config: AxiosRequestConfig;
    request: any
}
export interface Axios {
    defaults: AxiosRequestConfig
    interceptors: {
        request: AxiosInterceptorManager<AxiosRequestConfig>
        response: AxiosInterceptorManager<AxiosResponse>
    }
    request<T = any>(config: AxiosRequestConfig): AxiosPromise<T>
    get<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
    options<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
    delete<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
    head<T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>

    post<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>
    put<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>
    patch<T = any>(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise<T>
    
    getUri: (config: AxiosRequestConfig) => string
}

export interface AxiosInstance extends Axios {
    <T = any>(config: AxiosRequestConfig): AxiosPromise<T>
    <T = any>(url: string, config?: AxiosRequestConfig): AxiosPromise<T>
}

有些时候我们需要换个角度,不是去想为什么要这么做,而是想这么做能带来什么。上面接口中添加泛型,可以在接收返回的数据时写入数据类型,让ts推断出我们想要的类型,这听起来很抽象,例子:

//这是后端返回的数据格式接口
interface ResponseData<T = any> {
  code: number
  //返回的数据存放在result
  result: T
  message: string
}
//result中包含的数据格式定义成一个接口
interface User {
  name: string
  age: number
}
function getUser<T>() {
//我们将定义好的接口类型告诉给axios
  return axios<ResponseData<T>>('/extend/user')
    .then(res => res.data)
    .catch(err => console.error(err))
}


async function test() {
//将result中包含的数据的数据格式告诉给axios
//也就是说数据类型是ResponseData<User>
  const user = await getUser<User>()
  if (user) {
    console.log(user.result.name)
  }
}
test()

这听起来还是很绕,简单点说返回的数据给了变量user,而我们需要推断的user的数据类型,而单看的话,它的数据类型 ResponseData,重点我们需要推断的是user.result的数据类型,所以它的数据类型是User。

拦截器中的不解

看接口定义:

export interface AxiosInterceptorManager<T> {
  use(resolved: ResolvedFn<T>, rejected?: RejectedFn): number

  eject(id: number): void
}

export interface ResolvedFn<T=any> {
  (val: T): T | Promise<T>
}

export interface RejectedFn {
  (error: any): any
}

当我学习写到这里时,又开始疑惑,为什么resolved函数需要用到泛型,这个问题得以解决是因为写到后面了我才得以释怀的,先看文档的例子:

// 添加请求拦截器
axios.interceptors.request.use(function (config) {
    // 在发送请求之前做些什么
    return config;
  }, function (error) {
    // 对请求错误做些什么
    return Promise.reject(error);
  });

// 添加响应拦截器
axios.interceptors.response.use(function (response) {
    // 对响应数据做点什么
    return response;
  }, function (error) {
    // 对响应错误做点什么
    return Promise.reject(error);
  });

拦截器是需要两种类型的:

  • 一种是请求:AxiosRequestConfig
  • 另一种是响应:AxiosResponse

也就是一个是请求拦截器,一个是响应拦截器

取消功能中的问题

在构建axios中最头疼的是就是实现取消功能,因为用的少,先来个例子

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // executor 函数接收一个 cancel 函数作为参数
    cancel = c;
  })
});

// cancel the request
cancel('Operation canceled by the user.);

首先我们一看就知道CancelToken是一个类(这儿就说成类吧),它需要传入一个函数,假设这个函数叫executor吧,然后里面传入了一个参数,将它给了cancel,然后我们发现cancel是一个方法,也就是CacnelToken中传入了一个函数,而这个函数的参数又是一个函数,那么最里面的函数是一个带有message的函数,

export interface Canceler{
	(message?:string):void
}

那么executor呢:

export interface CancelExecutor{
	(executor:Canceler):void
}

在根据这个例子:

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function(thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
     // 处理错误
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// 取消请求(message 参数是可选的)
source.cancel('Operation canceled by the user.');

我们就知道了它静态接口(假设我们先定义好实例接口CancelToken):

export interface CancelTokenStaic {
    new(executor: CancelExecutor): CancelToken
    source(): ?
}

而根据观察source的接口定义:

export interface CancelTokenSource {
    token: CancelToken
    cancel: Canceler
}

那么静态接口就是

export interface CancelTokenStaic {
    new(executor: CancelExecutor): CancelToken
    source(): CancelTokenSource
}

那么我们在回过头想想CancelToken的实例接口该如何定义 ,下面的话是重点:

我们知道想要实现取消某次请求,我们需要为该请求配置一个 cancelToken,然后在外部调用一个 cancel 方法。

请求的发送是一个异步过程,最终会执行 xhr.send 方法,xhr 对象提供了 abort 方法,可以把请求取消。因为我们在外部是碰不到 xhr 对象的,所以我们想在执行 cancel 的时候,去执行 xhr.abort 方法。

现在就相当于我们在 xhr 异步请求过程中,插入一段代码,当我们在外部执行 cancel 函数的时候,会驱动这段代码的执行,然后执行 xhr.abort 方法取消请求。

我们可以利用 Promise 实现异步分离,也就是在 cancelToken 中保存一个 pending 状态的 Promise 对象,然后当我们执行 cancel 方法的时候,能够访问到这个 Promise 对象,把它从 pending 状态变成 resolved 状态,这样我们就可以在 then 函数中去实现取消请求的逻辑

代码如下:

if (cancelToken) {
  cancelToken.promise
    .then(reason => {
      request.abort()
      reject(reason)
    })
}

那么根据实现原理我们应该就知道该如何定义接口

export interface CancelToken{
	promise:Primise<string>
	reason?:string
}

我只是截取了部分,我觉得比较难理解,或者说一些思路的地方。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值