前言
本文记录自己用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就行
此时的项目结构是
-
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目录结构为
代码内容
定义 AxiosRequestConfig 接口类型
其中,url
为请求的地址,必选属性;而其余属性都是可选属性。method
是请求的 HTTP
方法;data
是 post
、patch
等类型请求的数据,放到 request body
中的;params
是 get
、head
等类型请求的数据,拼接到 url
的 query 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
此时代码目录
安装依赖
首先我们先安装依赖
- 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
对象的key
和 value
拼接到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)。原始数组不会被改变。
此时的目录结构
代码实现
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两个文件
此时的目录结构
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已经成功地被解析了
解析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
方法的参数支持 Document
和 BodyInit
类型,BodyInit
包括了 Blob
, BufferSource
,FormData
, URLSearchParams
, ReadableStream
、USVString
,当没有数据的时候,我们还可以传入 null
。
这个时候 data
是不能直接传给 send 函数的,我们需要把它转换成 JSON 字符串。
之前 isObject
的判断方式,对于 FormData
、ArrayBuffer
这些类型,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 字符串
,但是我们请求header
的 Content-Type
是 text/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
属性,需要自动设置请求 header
的 Content-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
对象,并且我们希望该对象包括:服务端返回的数据 data
,HTTP
状态码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
}
获取响应数据&改造成Promise类型
首先我们要在 xhr
函数添加onreadystatechange
事件处理函数,并且让 xhr
函数返回的是 AxiosPromise
类型。
我们主要在下面代码中做了几件事情
- 返回了一个
promise
实例 - 我们之前在
request
接口中定义了responseType
,如果responseType
有值,我们得把他设置到XMLHttpRequest
的实例里的responseType
中 - 在
XMLHttpRequest
实例的onreadystatechange
方法中获取响应数据- 响应数据
data
- HTTP 状态码
status
- 状态消息
status
Text - 响应头
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 状态码
未完待续