风和曰丽 發表於 2019-6-8 15:40:00

基于TypeScript从零重构axios

<h3>一、在GitHub上创建一个代码仓库</h3>
<p>找到仓库地址:git@github.com:QianDingweiCharles/ts-axios.git</p>
<h3>二、项目配置</h3>
<p>本地新建一个文件夹axios</p>
<p>用VScode打开,通过Typescript脚手架Typescript library starter搭建项目</p>
<p>命令行:&nbsp;git clone https://github.com/alexjoverm/typescript-library-starter.git axios</p>
<p>cd axios&nbsp;</p>
<p>查看远程分支:git remote -v,因为没有关联所以没有任何输出</p>
<p>关联远程分支:git remote add origin&nbsp;git@github.com:QianDingweiCharles/ts-axios.git</p>
<p>拉取远程分支并合并到当前的代码:git pull origin master</p>
<p>git branch 就可以看到本地也有master分支了</p>
<p>git push -u origin master</p>
<h3>三、编写请求代码</h3>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">//src/index.ts
import { AxiosRequestConfig } from './types'
import xhr from './xhr'
function axios(config: AxiosRequestConfig): void {
    xhr(config)
}
export default axios
</pre>
</div>
<p>&nbsp;</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">//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
}
</pre>
</div>
<h3> 四、处理请求参数</h3>
<p>4.1、参数是数组</p>
<p>params:{foo: ['bar,'baz'']},最终请求的url是/base/get?foo[]=bar&amp;foo[]=baz</p>
<p>4.2、参数是一个对象</p>
<p>params:{foo:{bar:'baz'}},最终请求的url是/base/get?foo=%7B........,foo后面拼接的是{“bar”:"baz"}&nbsp;encode后的结果。</p>
<p>4.3 、参数值是一个Date类型</p>
<p>params:{date}最终请求的url是/base/get?data=2019-04-01......,date后面拼接的是date.toISOString()的结果</p>
<p>4.4 特殊字符支持</p>
<p>对于字符@、:、¥、,空格,[,],我们是允许出现在url中的,不希望被encode</p>
<p>params:{foo:'@:$'}最终请求的url是/base/get?foo=@:$+,注意,我们会吧空格转成+</p>
<p>4.5空值忽略</p>
<p>params:{foo:bar,baz:null}最终的请求url是/base/get?foo=bar</p>
<p>4.6丢弃url中的哈希标记</p>
<p>axios({method:‘get’,url: '/base/get#hash',params:{foo:'bar'}})最终请求的url是/base/get?foo=bar</p>
<p>4.7保留url中已经存在的参数</p>
<p>axios({method:‘get’,url:'/base/get?foo=bar',params:{bar:'baz'}})最终的请求url是/base/get?foo=bar&amp;bar=baz</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">//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 =&gt; {
    const val = params
    if (val === null || typeof val === 'undefined') {
      return
    }
    //将所有的值都转成数组
    let values = []
    if (Array.isArray(val)) {
      values = val
      key += '[]'
    } else {
      values =
    }
    values.forEach(val =&gt; {
      if (isDate(val)) {
      val = val.toISOString()
      } else if (isPlainObject(val)) {
      val = JSON.stringify(val)
      }
      parts.push(`${encode(key)}=${encode(val)}`)
    })
})

let serializedParams = parts.join('&amp;')

if (serializedParams) {
    const markIndex = url.indexOf('#')
    //去掉哈希值
    if (markIndex !== -1) {
      url = url.slice(0, markIndex)
    }

    url += (url.indexOf('?') === -1 ? '?' : '&amp;') + serializedParams
}

return url
}
</pre>
</div>
<p>  </p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">//src/helpers/util.ts
const toString = Object.prototype.toString

export function isDate(val: any): val is Date {
return toString.call(val) === ''
}

// export function isObject (val: any): val is Object {
//   return val !== null &amp;&amp; typeof val === 'object'
// }

export function isPlainObject(val: any): val is Object {
return toString.call(val) === ''
}</pre>
</div>
<h3>五、处理请求的body数据</h3>
<p>需要将请求的data进行处理,如果是普通的对象,需要转换成JSON格式的数据</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">//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
}
</pre>
</div>
<p>  </p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">//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
</pre>
</div>
<h3>六 、处理请求头</h3>
<p>上一步对data进行了处理,但是content-type 为plain-text而不是application/json浏览器无法处理</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">//src/helpers/headers.ts
import { isPlainObject } from './util'

function normalizeHeaderName(headers: any, normalizedName: string): void {
if (!headers) {
    return
}
Object.keys(headers).forEach(name =&gt; {
    if (name !== normalizedName &amp;&amp; name.toUpperCase() === normalizedName.toUpperCase()) {
      headers = headers
      delete headers
    }
})
}

export function processHeaders(headers: any, data: any): any {
normalizeHeaderName(headers, 'Content-Type')
if (isPlainObject(data)) {
    if (headers &amp;&amp; !headers['Content-Type']) {
      headers['Content-Type'] = 'application/json;charset=utf-8'
    }
}
return headers
}
</pre>
</div>
<p>&nbsp;</p>
<p>在配置接口中添加headers字段,用户可以设置headers</p>
<p>&nbsp;<img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608160534514-1200028950.png"></p>
<p>在src/index.ts加入处理请求头逻辑:</p>
<p>&nbsp;<img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608160352913-2049274623.png"></p>
<p>&nbsp;这样就可以了吗?其实并没有,真正发送request headers 的xhr我们并没有修改,下一步,修改xhr:</p>
<p>&nbsp;<img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608160823963-1129022008.png"></p>
<h3>七、获取响应数据</h3>
<p>在此之前我们发送的请求都是从网络层面接手服务端返回的数据,单数代码层面并没有做任何关于返回数据的处理,我们希望能处理服务端响应的数据,并支持Promise链式调用的方式,如下:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">axios({
    method: 'post',
    url: '/base/post',
    data: {
      a: 1,
      b: 2
    }
}).then((res:any)=&gt; {console.log(res)})
</pre>
</div>
<p>  我们可以拿到res对象,并且我们希望该对象包括:服务端返回的数据data,HTTP状态码status,状态消息,响应头headers,请求配置对象config,以及请求的XMLHttpRequest对象实例request。</p>
<p>7.1、定义接口类型</p>
<p>在src/types/index.ts</p>
<p><img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608181525536-2008516889.png"></p>
<p>responseType让用户定义返回的类型,AxiosResponse是回调函数resolve传出去,也就是then方法里面得到的参数。AxiosPromise是返回的promise对象。</p>
<p>补充知识:</p>
<p>onreadystatechange:<strong><code>XMLHttpRequest.onreadystatechange</code></strong>&nbsp;会在&nbsp;<code>XMLHttpRequest</code>&nbsp;的<code>readyState</code>&nbsp;属性发生改变时触发&nbsp;<code>readystatechange</code>&nbsp;事件的时候被调用。</p>
<p>ready​State:属性返回一个 XMLHttpRequest&nbsp; 代理当前所处的状态。一个&nbsp;XHR</abbr>&nbsp;代理总是处于下列状态中的一个。</p>
<p><img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608182305804-1961680297.png"></p>
<p><strong>getAllResponseHeaders()</strong>&nbsp;:方法返回所有的响应头,以\r\n分割的字符串,或者&nbsp;<code>null</code>&nbsp;如果没有收到任何响应.</p>
<p class="document-actions">&nbsp;修改src/xhr.ts:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">import { AxiosRequestConfig, AxiosPromise,AxiosResponse } from './types'
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
    return new Promise((resolve) =&gt; {
      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) =&gt; {
            if (data === null &amp;&amp; name.toLowerCase() === 'content-type') {
                delete headers
            } else {
                request.setRequestHeader(name, headers)
            }
      })
      request.send(data)
    })

}
</pre>
</div>
<p>  修改src/index.ts</p>
<p><img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608183426828-210648191.png"></p>
<h3>&nbsp;八、处理响应header+处理响应data</h3>
<p>8.1&nbsp;处理响应header</p>
<p>通过XMLHTTPRequest的getAllResponseHeaders方法获取的值是一串以\r\n的字符串,我们希望最终解析成一个对象结构。</p>
<p>在src/helpers/headers.ts中添加函数:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">export function parseHeaders(headers: string): any {
    let parsed = Object.create(null)
    if (!headers) {
      return
    }
    headers.split('\r\n').forEach((line) =&gt; {
      let = line.split(':')
      key = key.trim().toLowerCase()
      if (!key) {
            return
      }
      if (val) {
            val = val.trim()
      }
      parsed = val
    })
}
</pre>
</div>
<p> 修改xhr.ts修改responseHeaders:</p>
<p><img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608193111888-1195676283.png"></p>
<p>8.2处理响应data</p>
<p>&nbsp;在我们不设置responseType的情况下,当服务端返回给我们的数据是字符串类型,我们可以尝试再把他转换成一个JSON 对象,例如:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">data:"{"a":1,"b":2}"
转成:
data:{
a:1,
b:2
}
</pre>
</div>
<p> 再src/helpers/data.ts中增加函数:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">export function transformResponse(data: any): any {
    if (typeof data === 'string') {
      try {
            data = JSON.parse(data)
      } catch (e) {
            //
      }
    }
    return data
}</pre>
</div>
<p>修改src/index.ts如下</p>
<p>  <img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608195423303-844120412.png"></p>
<h3>九、异常情况处理</h3>
<p>9.1 网络异常错误</p>
<p>&nbsp;当网络出现异常,比如不通的时候发送请求会触发XMLHttpRequest对象实例的error事件,于是我们可以在onerror的事件回调函数找那个捕获此类错误</p>
<p>修改src/xhr.ts如下:</p>
<p><img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608202030287-189718873.png"></p>
<p>9.2处理超时错误</p>
<p>当用户配置了超时时间时,如果超过了这个时间,那么将触发onTimeout事件。</p>
<p>在src/types/index.ts中的AxiosRequestConfig接口,添加timeout</p>
<p><img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608202825393-324729876.png"></p>
<p>修改xhr.ts</p>
<p>&nbsp;<img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608203025135-887687467.png"></p>
<p>9.3 处理非200状态码</p>
<p><img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608203213651-1019957515.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608203218573-2042755264.png"></p>
<p>9.4错误信息增强</p>
<p>希望提供的错误信息不仅仅包含错误文本信息,还包括请求对象配置config、错误代码code,XMLHttpRequest对象实例request,以及自定义响应对象response。</p>
<p>在src/type/index.ts中增加接口:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">export interface AxiosError extends Error {
    config: AxiosRequestConfig
    code?: string
    request?: any
    response?: AxiosResponse
    isAxiosError?: boolean
}
</pre>
</div>
<p>  新增./src/helpers/error.ts</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">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)
}
</pre>
</div>
<p>  <img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608212832698-1402272594.png"></p>
<p><img src="https://img2018.cnblogs.com/blog/1476644/201906/1476644-20190608212921814-201834226.png"></p>
<p>然后新建src/axios.ts从index.ts中复制所有</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">//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) =&gt; {
      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
</pre>
</div>
<p>  将src/index.ts改为:</p>
<div class="cnblogs_Highlighter">
<pre class="brush:javascript;gutter:true;">import axios from './axios'
export * from './types'

export default axios
</pre>
</div>
<h3>&nbsp; 其他请下载:https://files.cnblogs.com/files/QianDingwei/ts-axios-doc-master.zip</h3><br><br>
来源:https://www.cnblogs.com/QianDingwei/p/10990849.html
頁: [1]
查看完整版本: 基于TypeScript从零重构axios