一、在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 事件的時候被調用。
readyState:屬性返回一個 XMLHttpRequest 代理當前所處的狀態。一個 XHR 代理總是處於下列狀態中的一個。

getAllResponseHeaders() :方法返回所有的響應頭,以\r\n分割的字符串,或者 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方法獲取的值是一串以\r\n的字符串,我們希望最終解析成一個對象結構。
在src/helpers/headers.ts中添加函數:
export function parseHeaders(headers: string): any {
let parsed = Object.create(null)
if (!headers) {
return
}
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
})
}
修改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
