typescript重构axios学习记录

前言


本文记录自己用typescript造axios轮子的一些经验。如果你有耐心看完并一起实践,我敢保证你也能用typescript构建一个axios,并且熟悉typescript、axios、jest特性。

本次记录包括如下内容:

  • 造轮子的过程
  • web的基本知识,例如XMLHttpRequest()
  • 单元测试

过程记录

知识来源
构建思路来自基于TypeScript从零重构axios,有能力请支持正版。


先使用TypeScript library starter脚手架构建这个项目

TypeScript library starter

在你想要生成项目的文件夹下的控制台上输入

git clone https://github.com/alexjoverm/typescript-library-starter.git ts-axios
cd ts-axios

先通过 git clone 把项目代码拉下来到我们的 ts-axios 目录,进入ts-axios文件夹

初始化项目

npm install

初始化过程中有个选项让你填YES/NO 填YES就行

此时的项目结构是

typescript重构axios学习记录_第1张图片
  • src是代码目录
  • test是测试目录
  • tools是发布到Git 以及发布到npm的一些配置脚本工具

好处:

使用TypeScript library starter帮我们构建项目的好处是

  • 集成了很多优秀的开源工具
    例如:
    1、使用 RollupJS 帮助我们打包。
    2、使用 Prettier 和 TSLint 帮助我们格式化代码以及保证代码风格一致性。
    3、使用 TypeDoc 帮助我们自动生成文档并部署到 GitHub pages。
    4、使用 Jest帮助我们做单元测试。
    5、使用 Commitizen帮助我们生成规范化的提交注释。
    6、 使用 Semantic release帮助我们管理版本和发布。
    7、使用 husky帮助我们更简单地使用 git hooks。
    8、使用 Conventional changelog帮助我们通过代码提交信息自动生成 change log。

编写基本的请求代码


1、在src文件夹下创建index.ts文件作为主要文件,创建xhr.ts文件编写基本的请求代码。

2、同时在src文件夹下创建types文件夹,里面创建index.ts作为类型定义文件。

此时的src目录结构为

typescript重构axios学习记录_第2张图片

代码内容

定义 AxiosRequestConfig 接口类型

其中,url 为请求的地址,必选属性;而其余属性都是可选属性。method是请求的 HTTP 方法;datapostpatch 等类型请求的数据,放到 request body中的;paramsgethead等类型请求的数据,拼接到 urlquery string 中的。

types文件夹下的index.ts

export type Method = 'get' | 'GET'
    | 'delete' | 'DELETE'
    | 'head' | 'HEAD'
    | 'options' | 'OPTIONS'
    | 'post' | 'POST'
    | 'put' | 'PUT'
    | 'patch' | 'PATCH'

export interface AxiosRequestConfig {
    url: string
    method?: Method
    data?: any
    params?: any
}

我们并不想在 index.ts 中去实现发送请求的逻辑,利用模块化的编程思想,把这个功能拆分到一个单独的模块中。

导出一个 xhr 方法,它接受一个 config 参数,类型也是 AxiosRequestConfig 类型。

xhr.ts

import { AxiosRequestConfig } from "./types";

export default function xhr(config: AxiosRequestConfig): void {
    const { data = null, url, method = 'get' } = config

    const request = new XMLHttpRequest()

    request.open(method.toUpperCase(), url, true)

    request.send(data)
}

一些知识点的补充

XMLHttpRequest.open()

XMLHttpRequest.open() 方法初始化一个请求。

语法
xhrReq.open(method, url);
xhrReq.open(method, url, async);
xhrReq.open(method, url, async, user);
xhrReq.open(method, url, async, user, password);
参数
  • method
    要使用的HTTP方法,比如「GET」、「POST」、「PUT」、「DELETE」、等。对于非HTTP(S) URL被忽略。

  • url
    表示要向其发送请求的URL。

  • async(可选)
    一个可选的布尔参数,默认为true,表示要不要异步执行操作。如果值为false,send()方法直到收到答复前不会返回。如果true,已完成事务的通知可供事件监听器使用。如果multipart属性为true则这个必须为true,否则将引发异常。

  • user(可选)
    可选的用户名用于认证用途;默认为null。

  • password(可选)
    可选的密码用于认证用途,默认为null。

XMLHttpRequest.send()

XMLHttpRequest.send() 方法用于发送 HTTP 请求。如果是异步请求(默认为异步请求),则此方法会在请求发送后立即返回;如果是同步请求,则此方法直到响应到达后才会返回。

XMLHttpRequest.send()方法接受一个可选的参数,其作为请求主体;如果请求方法是 GET 或者 HEAD,则应将请求主体设置为 null。

如果没有使用setRequestHeader()方法设置 Accept头部信息,则会发送带有* / *的Accept头部。

语法
xhr.send(null);

此时主程序代码

import { AxiosRequestConfig } from './types/index'
import xhr from './xhr';

function axios(config: AxiosRequestConfig): void {
    xhr(config)
}
export default axios

编写调用实例


基本的请求代码的编写完成了,现在我们来写一个例子调用这个基础的Axios

在代码主目录下创建文件夹examples

此时代码目录

typescript重构axios学习记录_第3张图片

安装依赖

首先我们先安装依赖

  • webpack
  • webpack-dev-middleware
  • webpack-hot-middleware
  • ts-loader
  • tslint-loader
  • express
  • body-parser

在命令行中输入

npm install webpack webpack-dev-middleware  webpack-hot-middleware ts-loader tslint-loader express body-parser --save-dev

其中,webpack 是打包构建工具,webpack-dev-middleware 和 webpack-hot-middleware 是 2 个 express 的 webpack 中间件,ts-loader 和 tslint-loader 是 webpack 需要的 TypeScript 相关 loader,express 是 Node.js 的服务端框架,body-parser 是 express 的一个中间件,解析 body 数据用的。

编写 webpack 配置文件

const fs = require('fs')
const path = require('path')
const webpack = require('webpack')

module.exports = {
    mode: 'development',

    /**
     * 我们会在 examples 目录下建多个子目录
     * 我们会把不同章节的 demo 放到不同的子目录中
     * 每个子目录的下会创建一个 app.ts
     * app.ts 作为 webpack 构建的入口文件
     * entries 收集了多目录个入口文件,并且每个入口还引入了一个用于热更新的文件
     * entries 是一个对象,key 为目录名
     */
    entry: fs.readdirSync(__dirname).reduce((entries, dir) => {
        const fullDir = path.join(__dirname, dir)
        const entry = path.join(fullDir, 'app.ts')
        if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
            entries[dir] = ['webpack-hot-middleware/client', entry]
        }

        return entries
    }, {}),

    /**
     * 根据不同的目录名称,打包生成目标 js,名称和目录名一致
     */
    output: {
        path: path.join(__dirname, '__build__'),
        filename: '[name].js',
        publicPath: '/__build__/'
    },

    module: {
        rules: [
            {
                test: /\.ts$/,
                enforce: 'pre',
                use: [
                    {
                        loader: 'tslint-loader'
                    }
                ]
            },
            {
                test: /\.tsx?$/,
                use: [
                    {
                        loader: 'ts-loader',
                        options: {
                            transpileOnly: true
                        }
                    }
                ]
            }
        ]
    },

    resolve: {
        extensions: ['.ts', '.tsx', '.js']
    },

    plugins: [
        new webpack.HotModuleReplacementPlugin(),
        new webpack.NoEmitOnErrorsPlugin()
    ]
}

编写 server 文件

const express = require('express')
const bodyParser = require('body-parser')
const webpack = require('webpack')
const webpackDevMiddleware = require('webpack-dev-middleware')
const webpackHotMiddleware = require('webpack-hot-middleware')
const WebpackConfig = require('./webpack.config')

const app = express()
const compiler = webpack(WebpackConfig)
const router = express.Router()

router.get('/simple/get', function (req, res) {
  res.json({
    msg: `hello world`
  })
})

app.use(webpackDevMiddleware(compiler, {
  publicPath: '/__build__/',
  stats: {
    colors: true,
    chunks: false
  }
}))

app.use(webpackHotMiddleware(compiler))

app.use(express.static(__dirname))

app.use(bodyParser.json())
app.use(bodyParser.urlencoded({ extended: true }))
app.use(router)


const port = process.env.PORT || 8081
module.exports = app.listen(port, () => {
  console.log(`Server listening on http://localhost:${port}, Ctrl+C to stop`)
})

编写 demo 代码

index.html



  
    
    ts-axios examples
    
  
  
    

ts-axios examples

global.css

html, body {
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
  color: #2c3e50;
}

ul {
  line-height: 1.5em;
  padding-left: 1.5em;
}

a {
  color: #7f8c8d;
  text-decoration: none;
}

a:hover {
  color: #4fc08d;
}

然后在 examples 目录下创建 simple 目录,作为本章节的 demo 目录,在该目录下再创建 index.html 和 app.ts 文件

index.html 文件如下:



  
    
    Simple example
  
  
    
  

app.ts 文件如下:

import axios from '../../src/index'

axios({
  method: 'get',
  url: '/simple/get',
  params: {
    a: 1,
    b: 2
  }
})

运行

接着我们在 package.json 中去新增一个 npm script:

"dev": "node examples/server.js"

然后我们去控制台执行命令

npm run dev

要是觉得上面两步太麻烦可以直接执行

node examples/server.js

运行结果

可以看到请求已经发送成功了


解析Params


虽然我们完成了最基本的axios的功能,但是还有很多地方没有去做,例如params的解析。

Params示例:

比如说:

axios({
  method: 'get',
  url: '/base/get',
  params: {
    a: 1,
    b: 2
  }
})

我们希望最终请求的 url 是 /base/get?a=1&b=2,这样服务端就可以通过请求的 url 解析到我们传来的参数数据了。实际上就是把 params 对象的keyvalue 拼接到url上。

参数值是数组

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

最终请求的 url 是/base/get?foo[]=bar&foo[]=baz'
参数值是对象:

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

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

参数值是Date类型:

const date = new Date()

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

最终请求的 url 是/base/get?date=2019-04-01T05:55:39.030Z,date 后面拼接的是 date.toISOString()的结果。

特殊字符支持:

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

对于字符@:$,[],我们是允许出现在 url 中的,不希望被 encode。
最终请求的 url/base/get?foo=@:$+,注意,我们会把空格 转换成 +

空值忽略:

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

对于值为null或者 undefined的属性,我们是不会添加到 url 参数中的。

最终请求的 url/base/get?foo=bar

丢弃 url 中的哈希标记:

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

最终请求的 url/base/get?foo=bar

好,我们现在得到了七种情况,让我们来一步步实现。

实现

创建一个helper文件夹,来存储我们的处理函数.
里面再创建一个url文件,和有一个util文件。

文件功能说明

util文件现在有两个函数:

  • 判断是否是日期
  • 判断是否是对象。

url文件的思路是这样的:

  • 判断是否有params参数
  • 处理params每个可枚举的属性
  • 如果为空或者为undefined,那么返回
  • 如果是一个数组的话,先在key后面加一个[] key[]=
  • 剩下的值都放入数组里,一个个遍历判断
  • 如果是Date类型,则调用toISOString方法
  • 如果是Object类型,则调用JSON.stringify()转换成对应的json格式
  • 拼接参数
  • 做一个丢弃哈希标记的判断
  • 如果之前的url里有参数了,那么在这个参数后面进行拼接,(对?和&的判断)

知识点补充

Object.keys(obj)

描述

Object.keys 返回一个所有元素为字符串的数组,其元素来自于从给定的object上面可直接枚举的属性。这些属性的顺序与手动遍历该对象属性时的一致。

示例
// simple array
var arr = ['a', 'b', 'c'];
console.log(Object.keys(arr)); // console: ['0', '1', '2']

// array like object
var obj = { 0: 'a', 1: 'b', 2: 'c' };
console.log(Object.keys(obj)); // console: ['0', '1', '2']

// array like object with random key ordering
var anObj = { 100: 'a', 2: 'b', 7: 'c' };
console.log(Object.keys(anObj)); // console: ['2', '7', '100']

// getFoo is a property which isn't enumerable
var myObj = Object.create({}, {
  getFoo: {
    value: function () { return this.foo; }
  } 
});
myObj.foo = 1;
console.log(Object.keys(myObj)); // console: ['foo']

Array.prototype.slice()

描述

slice() 方法返回一个新的数组对象,这一对象是一个由 begin 和 end 决定的原数组的浅拷贝(包括 begin,不包括end)。原始数组不会被改变。

此时的目录结构

typescript重构axios学习记录_第4张图片

代码实现

url.ts
import { isDate, isObject } from './util'

/**
 * parsing special characters
 * @param string
 * @return {string}
 */
function encode(val: string): string {
    return encodeURIComponent(val)
        .replace(/%40/g, '@')
        .replace(/%3A/ig, ':')
        .replace(/%24/g, '$')
        .replace(/%20/g, '+')
        .replace(/%5B/ig, '[')
        .replace(/%5D/ig, ']')
}

/**
 * add params to url
 * @param string 
 * @param any
 * @return {string}
 */
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 (isObject(val)) {
                val = JSON.stringify(val)
            }
            parts.push(`${encode(key)}=${encode(val)}`)
        })
    })

    let serializedParams = parts.join('&')
    if (serializedParams) {
        const marIndex = url.indexOf('#')
        if (marIndex !== -1) {
            url = url.slice(0, marIndex)
        }
        url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
    }

    return url
}
util.ts
export function isDate(val: any): val is Date {
    return Object.prototype.toString.call(val) === '[object Date]'
}

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

测试

基本上params解析实现完毕,现在我们来用demo测试一下

加入新demo

在examples下加入base文件夹
新建app.ts和index.html两个文件

此时的目录结构

typescript重构axios学习记录_第5张图片
app.ts
import axios from '../../src/index'

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

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

const date = new Date()

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


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

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

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

axios({
  method: 'get',
  url: '/base/get?baz=foo',
  params: {
    foo: 'bar'
  }
})
index.html



  
  
  
  Base example


  


更新

同时要更新examples目录下的index.html





  
  ts-axios examples
  



  

ts-axios examples

测试结果

我们可以看到,params已经成功地被解析了

typescript重构axios学习记录_第6张图片

解析body数据


如果我们要传一个普通对象给服务端,例如:

axios({
  method: 'post',
  url: '/base/post',
  data: { 
    a: 1,
    b: 2 
  }
})

注意,上一节我们解决的是params解析,这一节是data。

目标

我们现在要把data传入到XMLHttpRequest的send方法里,但是send方法的参数类型是有限的。
MDN文档中给出了几个例子:

void send();
void send(ArrayBuffer data);
void send(ArrayBufferView data);
void send(Blob data);
void send(Document data);
void send(DOMString? data);
void send(FormData data);

send 方法的参数支持 DocumentBodyInit 类型,BodyInit 包括了 Blob, BufferSource,FormData, URLSearchParams, ReadableStreamUSVString,当没有数据的时候,我们还可以传入 null

这个时候 data是不能直接传给 send 函数的,我们需要把它转换成 JSON 字符串。

之前 isObject 的判断方式,对于 FormDataArrayBuffer 这些类型,isObject 判断也为 true,,但是这些类型的数据我们是不需要做处理的,而 isPlainObject 的判断方式,只有我们定义的普通 JSON 对象才能满足。

我们在helpers目录下的util.js加上isPlainObject的定义,来替代之前的isObject

export function isPlainObject (val: any): val is Object {
  return toString.call(val) === '[object Object]'
}

我们在helpers 目录新建data.ts文件。

import { isPlainObject } from './util'

export function transformRequest (data: any): any {
  if (isPlainObject(data)) {
    return JSON.stringify(data)
  }
  return data
}

好,我们做完这个转换就大功告成了,就是这么简单。

实现

index.ts中加入对data数据的处理

import { transformRequest } from './helpers/data'

```typescript
function processConfig (config: AxiosRequestConfig): void {
  config.url = transformURL(config)
  config.data = transformRequestData(config)
}

function transformRequestData (config: AxiosRequestConfig): any {
  return transformRequest(config.data)
}

编写demo

一个是对data对象的测试用例,一个是对buffer数据流的测试用例:

axios({
  method: 'post',
  url: '/base/post',
  data: {
    a: 1,
    b: 2
  }
})

const arr = new Int32Array([21, 31])

axios({
  method: 'post',
  url: '/base/buffer',
  data: arr
})

我们接着在 examples/server.js 中添加 2 个路由,分别针对这俩种请求,返回请求传入的数据。

router.post('/base/post', function(req, res) {
  res.json(req.body)
})

router.post('/base/buffer', function(req, res) {
  let msg = []
  req.on('data', (chunk) => {
    if (chunk) {
      msg.push(chunk)
    }
  })
  req.on('end', () => {
    let buf = Buffer.concat(msg)
    res.json(buf.toJSON())
  })
})

处理Header数据


在上一节的例子中,我们实现了对data属性的转换。
但是 base/post 请求的 response 里却返回的是一个空对象,这是什么原因呢?

实际上是因为我们虽然执行 send 方法的时候把普通对象 data 转换成一个 JSON 字符串,但是我们请求headerContent-Typetext/plain;charset=UTF-8,导致了服务端接受到请求并不能正确解析请求 body 的数据。

我们得给请求 header 设置正确的 Content-Type
怎么设置呢?
首先我们要支持发送请求的时候,可以支持配置 headers 属性。

axios({
  method: 'post',
  url: '/base/post',
  headers: {
    'content-type': 'application/json;charset=utf-8'
  },
  data: {
    a: 1,
    b: 2
  }
})

并且在当我们传入的 data 为普通对象的时候,headers 如果没有配置 Content-Type 属性,需要自动设置请求 headerContent-Type 字段为:application/json;charset=utf-8

分工

好现在就是两个任务:

  • 支持headers属性传入
  • 检测data属性,当是普通对象时,得自动帮忙配置一下Content-Type

解决任务

首先我们得在AxiosRequestConfig 接口类型,添加 headers 这个可选属性:

export interface AxiosRequestConfig {
  url: string
  method?: Method
  data?: any
  params?: any
  headers?: any
}

然后根据需求分析,我们要实现一个工具函数,对 request 中的 headers 做一层加工。我们在 helpers 目录新建 headers.ts 文件。

这里有个需要注意的点,因为请求 header 属性是大小写不敏感的,比如我们之前的例子传入 header 的属性名content-type就是全小写的,所以我们先要把一些 header属性名规范化。

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
}

index.ts文件中添加:
因为我们处理 header 的时候依赖了 data,所以要在处理请求body 数据之前处理请求 header

function processConfig (config: AxiosRequestConfig): void {
  config.url = transformURL(config)
  config.headers = transformHeaders(config)
  config.data = transformRequestData(config)
}

function transformHeaders (config: AxiosRequestConfig) {
  const { headers = {}, data } = config
  return processHeaders(headers, data)
}

xhr.ts文件中添加:

export default function xhr (config: AxiosRequestConfig): void {
  const { data = null, url, method = 'get', headers } = config

  const request = new XMLHttpRequest()

  request.open(method.toUpperCase(), url, true)

  Object.keys(headers).forEach((name) => {
    if (data === null && name.toLowerCase() === 'content-type') {
      delete headers[name]
    } else {
      request.setRequestHeader(name, headers[name])
    }
  })

  request.send(data)
}

测试demo编写

axios({
  method: 'post',
  url: '/base/post',
  data: {
    a: 1,
    b: 2
  }
})

axios({
  method: 'post',
  url: '/base/post',
  headers: {
    'content-type': 'application/json;'
  },
  data: {
    a: 1,
    b: 2
  }
})

const paramsString = 'q=URLUtils.searchParams&topic=api'
const searchParams = new URLSearchParams(paramsString)

axios({
  method: 'post',
  url: '/base/post',
  data: searchParams
})

获取响应数据


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

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

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

分工

我们现在要做的三件事

  • 定义响应数据类型
  • 获取响应的数据
  • 让返回值变成promise对象

解决任务

定义响应数据类型

定义接口类型

export interface AxiosResponse {
  data: any
  status: number
  statusText: string
  headers: any
  config: AxiosRequestConfig
  request: any
}

另外,axios 函数返回的是一个 Promise 对象,我们可以定义一个 AxiosPromise 接口,它继承于 Promise 这个泛型接口:

这样的话,当 axios 返回的是 AxiosPromise 类型,那么 resolve 函数中的参数就是一个 AxiosResponse 类型。

export interface AxiosPromise extends Promise {
}

对于一个 AJAX 请求的 response,我们是可以指定它的响应的数据类型的,通过设置 XMLHttpRequest对象的 responseType属性,于是我们可以给 AxiosRequestConfig 类型添加一个可选属性:

export interface AxiosRequestConfig {
  // ...
  responseType?: XMLHttpRequestResponseType
}
typescript重构axios学习记录_第7张图片

获取响应数据&改造成Promise类型

首先我们要在 xhr 函数添加onreadystatechange事件处理函数,并且让 xhr 函数返回的是 AxiosPromise 类型。

我们主要在下面代码中做了几件事情

  • 返回了一个promise实例
  • 我们之前在request接口中定义了responseType,如果responseType有值,我们得把他设置到XMLHttpRequest的实例里的responseType
  • XMLHttpRequest实例的onreadystatechange方法中获取响应数据
    • 响应数据data
    • HTTP 状态码status
    • 状态消息statusText
    • 响应头 headers
    • 请求配置对象 config
    • 以及请求的 XMLHttpRequest对象实例 request

xhr.ts

export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise((resolve) => {
    const { data = null, url, method = 'get', 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 && 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)
  })
}

修改了 xhr 函数,我们同样也要对应修改 axios 函数:
index.ts

function axios(config: AxiosRequestConfig): AxiosPromise {
  processConfig(config)
  return xhr(config)
}

大功告成

测试demo

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

axios({
  method: 'post',
  url: '/base/post',
  responseType: 'json',
  data: {
    a: 3,
    b: 4
  }
}).then((res) => {
  console.log(res)
})

我们打开浏览器运行 demo,看一下结果,发现我们可以正常 log 出这个 res 变量,它包含 AxiosResponse 类型中定义的那些属性,不过我们发现 2 个小问题:

  • 第一个是 headers 属性是一个字符串,我们需要把它解析成对象类型;
  • 第二个是在第一个请求中,得到的数据是一个JSON字符串,我们也需要把它转换成对象类型。

处理响应Header


来,转换成对象类型

我们通过 XMLHttpRequest 对象的 getAllResponseHeaders 方法获取到的值是如下一段字符串:
每一行都是以回车符和换行符\r\n结束,它们是每个header属性的分隔符。

date: Fri, 05 Apr 2019 12:40:49 GMT
etag: W/"d-Ssxx4FRxEutDLwo2+xkkxKc4y0k"
connection: keep-alive
x-powered-by: Express
content-length: 13
content-type: application/json; charset=utf-8

我们得解析成下面这个结构:

{
  date: 'Fri, 05 Apr 2019 12:40:49 GMT'
  etag: 'W/"d-Ssxx4FRxEutDLwo2+xkkxKc4y0k"',
  connection: 'keep-alive',
  'x-powered-by': 'Express',
  'content-length': '13'
  'content-type': 'application/json; charset=utf-8'
}

我们需要定义一个工具函数:
helpers/headers.ts

export function parseHeaders(headers: string): any {
  let parsed = Object.create(null)
  if (!headers) {
    return parsed
  }

  headers.split('\r\n').forEach(line => {
    let [key, val] = line.split(':')
    key = key.trim().toLowerCase()
    if (!key) {
      return
    }
    if (val) {
      val = val.trim()
    }
    parsed[key] = val
  })

  return parsed
}

然后我们在xhr.ts使用这个工具函数:
xhr.ts:

const responseHeaders = parseHeaders(request.getAllResponseHeaders())

处理响应data


同样的,要将字符串转换成json对象
例如:

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

转换成

data: {
  a: 1,
  b: 2
}

根据需求分析,我们要实现一个 transformResponse工具函数。

helpers/data.ts

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

index.ts

function axios(config: AxiosRequestConfig): AxiosPromise {
  processConfig(config)
  return xhr(config).then((res) => {
    return transformResponseData(res)
  })
}

function transformResponseData(res: AxiosResponse): AxiosResponse {
  res.data = transformResponse(res.data)
  return res
}

接着我们再去看刚才的 demo,发现我们已经把响应的 data 字段从字符串解析成 JSON 对象结构了。

那么至此,我们的 ts-axios 的基础功能已经实现完毕。

错误处理


在上一章节,我们实现了 ts-axios的基础功能,但目前为止我们都是处理了正常接收请求的逻辑,并没有考虑到任何错误情况的处理,这对于一个程序的健壮性而言是远不够的,因此我们这一章需要对 AJAX 各种错误情况做处理。

并且我们希望程序也能捕获到这些错误,做进一步的处理。

axios({
  method: 'get',
  url: '/error/get'
}).then((res) => {
  console.log(res)
}).catch((e) => {
  console.log(e)
})

如果在请求的过程中发生任何错误,我们都可以在reject回调函数中捕获到。

我们把错误分成了几类,接下来我们就来分别处理这些错误情况。

  • 处理网络异常错误
  • 处理超时错误
  • 处理非 200 状态码
    未完待续

扩展接口

你可能感兴趣的:(typescript重构axios学习记录)