• 基于TypeScript从零重构axios


    一、在GitHub上创建一个代码仓库

    找到仓库地址:git@github.com:QianDingweiCharles/ts-axios.git

    二、项目配置

    本地新建一个文件夹axios

    用VScode打开,通过Typescript脚手架Typescript library starter搭建项目

    命令行: git clone https://github.com/alexjoverm/typescript-library-starter.git axios

    cd axios 

    查看远程分支:git remote -v,因为没有关联所以没有任何输出

    关联远程分支:git remote add origin git@github.com:QianDingweiCharles/ts-axios.git

    拉取远程分支并合并到当前的代码:git pull origin master

    git branch 就可以看到本地也有master分支了

    git push -u origin master

    三、编写请求代码

    //src/index.ts
    import { AxiosRequestConfig } from './types'
    import xhr from './xhr'
    function axios(config: AxiosRequestConfig): void {
        xhr(config)
    }
    export default axios
    
    //src/types/index.ts这是声明文件
    export type Method = 'get' | 'GET'
        | 'delete' | 'Delete'
        | 'head' | 'HEAD'
        | 'post' | 'POST'
        | 'put' | 'PUT'
        | 'patch' | 'PATCH'
    export interface AxiosRequestConfig {
        url: string
        method?: Method
        data?: any
        params?: any
    }
    

     四、处理请求参数

    4.1、参数是数组

    params:{foo: ['bar,'baz'']},最终请求的url是/base/get?foo[]=bar&foo[]=baz

    4.2、参数是一个对象

    params:{foo:{bar:'baz'}},最终请求的url是/base/get?foo=%7B........,foo后面拼接的是{“bar”:"baz"} encode后的结果。

    4.3 、参数值是一个Date类型

    params:{date}最终请求的url是/base/get?data=2019-04-01......,date后面拼接的是date.toISOString()的结果

    4.4 特殊字符支持

    对于字符@、:、¥、,空格,[,],我们是允许出现在url中的,不希望被encode

    params:{foo:'@:$'}最终请求的url是/base/get?foo=@:$+,注意,我们会吧空格转成+

    4.5空值忽略

    params:{foo:bar,baz:null}最终的请求url是/base/get?foo=bar

    4.6丢弃url中的哈希标记

    axios({method:‘get’,url: '/base/get#hash',params:{foo:'bar'}})最终请求的url是/base/get?foo=bar

    4.7保留url中已经存在的参数

    axios({method:‘get’,url:'/base/get?foo=bar',params:{bar:'baz'}})最终的请求url是/base/get?foo=bar&bar=baz

    //src/helpers/url.ts
    import { isDate, isPlainObject } from './util'
    //将特殊的字符转换回来
    function encode(val: string): string {
      return encodeURIComponent(val)
        .replace(/%40/g, '@')
        .replace(/%3A/gi, ':')
        .replace(/%24/g, '$')
        .replace(/%2C/gi, ',')
        .replace(/%20/g, '+')
        .replace(/%5B/gi, '[')
        .replace(/%5D/gi, ']')
    }
    
    export function buildURL(url: string, params?: any): string {
      if (!params) {
        return url
      }
    
      const parts: string[] = []
    
      Object.keys(params).forEach(key => {
        const val = params[key]
        if (val === null || typeof val === 'undefined') {
          return
        }
        //将所有的值都转成数组
        let values = []
        if (Array.isArray(val)) {
          values = val
          key += '[]'
        } else {
          values = [val]
        }
        values.forEach(val => {
          if (isDate(val)) {
            val = val.toISOString()
          } else if (isPlainObject(val)) {
            val = JSON.stringify(val)
          }
          parts.push(`${encode(key)}=${encode(val)}`)
        })
      })
    
      let serializedParams = parts.join('&')
    
      if (serializedParams) {
        const markIndex = url.indexOf('#')
        //去掉哈希值
        if (markIndex !== -1) {
          url = url.slice(0, markIndex)
        }
    
        url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
      }
    
      return url
    }
    

      

    //src/helpers/util.ts
    const toString = Object.prototype.toString
    
    export function isDate(val: any): val is Date {
      return toString.call(val) === '[object Date]'
    }
    
    // export function isObject (val: any): val is Object {
    //   return val !== null && typeof val === 'object'
    // }
    
    export function isPlainObject(val: any): val is Object {
      return toString.call(val) === '[object Object]'
    }

    五、处理请求的body数据

    需要将请求的data进行处理,如果是普通的对象,需要转换成JSON格式的数据

    //src/helpers/data.ts
    import { isPlainObject } from './util'
    
    export function transformRequest(data: any): any {
      if (isPlainObject(data)) {
        return JSON.stringify(data)
      }
      return data
    }
    
    export function transformResponse(data: any): any {
      if (typeof data === 'string') {
        try {
          data = JSON.parse(data)
        } catch (e) {
          // do nothing
        }
      }
      return data
    }
    

      

    //src/index.ts
    import { AxiosRequestConfig } from './types'
    import xhr from './xhr'
    import { buildURL } from './helpers/url'
    import { transformRequest } from './helpers/data'
    function axios(config: AxiosRequestConfig): void {
        processConfig(config)
        xhr(config)
    }
    
    function processConfig(config: AxiosRequestConfig) {
        config.url = transformURL(config)
        config.data = transformRequestData(config)
    }
    
    function transformURL(config: AxiosRequestConfig) {
        const { url, params } = config
        return buildURL(url, params)
    }
    
    function transformRequestData(config: AxiosRequestConfig) {
        return transformRequest(config.data)
    }
    export default axios
    

    六 、处理请求头

    上一步对data进行了处理,但是content-type 为plain-text而不是application/json浏览器无法处理

    //src/helpers/headers.ts
    import { isPlainObject } from './util'
    
    function normalizeHeaderName(headers: any, normalizedName: string): void {
      if (!headers) {
        return
      }
      Object.keys(headers).forEach(name => {
        if (name !== normalizedName && name.toUpperCase() === normalizedName.toUpperCase()) {
          headers[normalizedName] = headers[name]
          delete headers[name]
        }
      })
    }
    
    export function processHeaders(headers: any, data: any): any {
      normalizeHeaderName(headers, 'Content-Type')
      if (isPlainObject(data)) {
        if (headers && !headers['Content-Type']) {
          headers['Content-Type'] = 'application/json;charset=utf-8'
        }
      }
      return headers
    }
    

    在配置接口中添加headers字段,用户可以设置headers

     

    在src/index.ts加入处理请求头逻辑:

     

     这样就可以了吗?其实并没有,真正发送request headers 的xhr我们并没有修改,下一步,修改xhr:

     

    七、获取响应数据

    在此之前我们发送的请求都是从网络层面接手服务端返回的数据,单数代码层面并没有做任何关于返回数据的处理,我们希望能处理服务端响应的数据,并支持Promise链式调用的方式,如下:

    axios({
        method: 'post',
        url: '/base/post',
        data: {
            a: 1,
            b: 2
        }  
    }).then((res:any)=> {console.log(res)})
    

      我们可以拿到res对象,并且我们希望该对象包括:服务端返回的数据data,HTTP状态码status,状态消息,响应头headers,请求配置对象config,以及请求的XMLHttpRequest对象实例request。

    7.1、定义接口类型

    在src/types/index.ts

    responseType让用户定义返回的类型,AxiosResponse是回调函数resolve传出去,也就是then方法里面得到的参数。AxiosPromise是返回的promise对象。

    补充知识:

    onreadystatechange:XMLHttpRequest.onreadystatechange 会在 XMLHttpRequest 的readyState 属性发生改变时触发 readystatechange 事件的时候被调用。

    ready​State:属性返回一个 XMLHttpRequest  代理当前所处的状态。一个 XHR 代理总是处于下列状态中的一个。

    getAllResponseHeaders() :方法返回所有的响应头,以 分割的字符串,或者 null 如果没有收到任何响应.

     修改src/xhr.ts:

    import { AxiosRequestConfig, AxiosPromise,AxiosResponse } from './types'
    export default function xhr(config: AxiosRequestConfig): AxiosPromise {
        return new Promise((resolve) => {
            const { method = 'get', url, data = null, headers,responseType } = config
            const request = new XMLHttpRequest()
            if(responseType){
                request.responseType = responseType
            }
            request.open(method.toUpperCase(), url, true)
            request.onreadystatechange = function handleLoad(){
                if(request.readyState !==4){
                    return
                }
                const responseHeaders = request.getAllResponseHeaders()
                const responseData = responseType !== 'text' ? request.response: request.responseText
                const response: AxiosResponse = {
                    data: responseData,
                    status: request.status,
                    statusText: request.statusText,
                    headers: responseHeaders,
                    config,
                    request
                }
                resolve(response)
            }
            Object.keys(headers).forEach((name) => {
                if (data === null && name.toLowerCase() === 'content-type') {
                    delete headers[name]
                } else {
                    request.setRequestHeader(name, headers[name])
                }
            })
            request.send(data)
        })
    
    }
    

      修改src/index.ts

     八、处理响应header+处理响应data

    8.1 处理响应header

    通过XMLHTTPRequest的getAllResponseHeaders方法获取的值是一串以 的字符串,我们希望最终解析成一个对象结构。

    在src/helpers/headers.ts中添加函数:

    export function parseHeaders(headers: string): any {
        let parsed = Object.create(null)
        if (!headers) {
            return
        }
        headers.split('
    ').forEach((line) => {
            let [key, val] = line.split(':')
            key = key.trim().toLowerCase()
            if (!key) {
                return
            }
            if (val) {
                val = val.trim()
            }
            parsed[key] = val
        })
    }
    

     修改xhr.ts修改responseHeaders:

    8.2处理响应data

     在我们不设置responseType的情况下,当服务端返回给我们的数据是字符串类型,我们可以尝试再把他转换成一个JSON 对象,例如:

    data:"{"a":1,"b":2}"
    转成:
    data:{
    a:1,
    b:2
    }
    

     再src/helpers/data.ts中增加函数:

    export function transformResponse(data: any): any {
        if (typeof data === 'string') {
            try {
                data = JSON.parse(data)
            } catch (e) {
                //
            }
        }
        return data
    }

    修改src/index.ts如下

      

    九、异常情况处理

    9.1 网络异常错误

     当网络出现异常,比如不通的时候发送请求会触发XMLHttpRequest对象实例的error事件,于是我们可以在onerror的事件回调函数找那个捕获此类错误

    修改src/xhr.ts如下:

    9.2处理超时错误

    当用户配置了超时时间时,如果超过了这个时间,那么将触发onTimeout事件。

    在src/types/index.ts中的AxiosRequestConfig接口,添加timeout

    修改xhr.ts

     

    9.3 处理非200状态码

    9.4错误信息增强

    希望提供的错误信息不仅仅包含错误文本信息,还包括请求对象配置config、错误代码code,XMLHttpRequest对象实例request,以及自定义响应对象response。

    在src/type/index.ts中增加接口:

    export interface AxiosError extends Error {
        config: AxiosRequestConfig
        code?: string
        request?: any
        response?: AxiosResponse
        isAxiosError?: boolean
    }
    

      新增./src/helpers/error.ts

    import { AxiosRequestConfig, AxiosResponse } from '../types'
    export class AxiosErros extends Error {
        config: AxiosRequestConfig
        code?: string |null
        request?: any
        response?: AxiosResponse
        isAxiosError: boolean
        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,AxiosErros.prototype)
        }
    }
    //工程函数
    export function createError(
        message: string,
        config: AxiosRequestConfig,
        code?: string |null,
        request?: any,
        response?: AxiosResponse): AxiosErros{
        return new AxiosErros(message,config,code,request,response)
    }
    

      

    然后新建src/axios.ts从index.ts中复制所有

    //src/index.ts
    import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from './types'
    import xhr from './xhr'
    import { buildURL } from './helpers/url'
    import { transformRequest, transformResponse } from './helpers/data'
    import { processHeaders } from './helpers/headers'
    function axios(config: AxiosRequestConfig): AxiosPromise {
        processConfig(config)
        return xhr(config).then((res) => {
            return transformResponseData(res)
        })
    }
    
    function processConfig(config: AxiosRequestConfig) {
        config.url = transformURL(config)
        config.headers = transformHeaders(config)
        config.data = transformRequestData(config)
    }
    
    function transformURL(config: AxiosRequestConfig) {
        const { url, params } = config
        return buildURL(url, params)
    }
    
    function transformRequestData(config: AxiosRequestConfig) {
        return transformRequest(config.data)
    }
    
    function transformHeaders(config: AxiosRequestConfig): void {
        const { headers, data } = config
        return processHeaders(headers, data)
    }
    
    function transformResponseData(res: AxiosResponse) {
        res.data = transformResponse(res)
        return res
    }
    export default axios
    

      将src/index.ts改为:

    import axios from './axios'
    export * from './types'
    
    export default axios
    

      其他请下载:https://files.cnblogs.com/files/QianDingwei/ts-axios-doc-master.zip

  • 相关阅读:
    numpy用法介绍-未完待续
    GeoJSON相关操作
    awk日志分析
    awk获取外部变量
    Shell编程二
    Shell编程
    Linux监控平台搭建
    Linux集群架构
    Linux集群
    MySQL主从(MySQL proxy Lua读写分离设置,一主多从同步配置,分库分表方案)
  • 原文地址:https://www.cnblogs.com/QianDingwei/p/10990849.html
Copyright © 2020-2023  润新知