为什么会写这篇文章,或者说为什么要手撸axios库呢?很简单,看了公司项目中封装的axios之后,一脸懵啊。第一次见封装axios都有几百行的,这玩意儿有这么多吗?咋还有这么多方法呢?这方法咋看着这么面生呢?这还是我平常使用的axios吗?带着这么些疑问,我想了想,不如花点时间集中学习一下axios。于是乎,利用一周的假期时间去学习axios,跟着视频从零开始,一步一步实现axios库的大部分功能,最后发布到npm上。总体来说还是收获很大的,学到了不少东西。学完之后再看看公司项目封装的代码,嗷~,原来如此。大师,我悟了!
写下这篇文章,是为了记录下此次手撸axios库中所实现功能的核心代码以及方式方法,加深理解和记忆。
每天更新一个功能实现,万丈高楼平地起,一个功能强大的axios库也是由一个个小的功能实现的,加油!
01-编写基础请求代码
首先来实现一下最最基础的功能,发送请求功能。编写一个入口文件index.js,如下
function axios(config) {
}
export default axios
这就是整个axios库的入口文件了。接下来要做的是发送请求。利用模块化编程的思想,将发送请求的功能拆分出去,新建一个xhr.js文件,如下
export default function xhr(config){
const { data = null, url, method = 'get' } = config
const request = new XMLHttpRequest()
request.open(method.toUpperCase(), url, true)
request.send(data)
}
代码也很简单,就是接收一个config,根据配置去发送请求。然后回到入口文件,引入这个xhr方法。一个最基础的请求功能就完成了,如下,非常简单
import xhr from './xhr'
function axios(config){
xhr(config)
}
export default axios
02-处理请求中的url参数
url参数就是请求中的params,在发送请求过程中会将其拼接在url后面。那么我们就来列举一下常见的params参数有哪些:
1. 字符串
axios({
method: 'get',
url: '/base/get',
params: {
a: 1,
b: 2
}
})
请求时的url => /base/get?a=1&b=2
2. 数组
axios({
method: 'get',
url: '/base/get',
params: {
foo: ['bar', 'baz']
}
})
请求时的url => /base/get?foo[]=bar&foo[]=baz
3. 对象
axios({
method: 'get',
url: '/base/get',
params: {
foo: {
bar: 'baz'
}
}
})
请求时的url => /base/get?foo=%7B%22bar%22:%22baz%22%7D
foo 后面拼接的是{"bar":"baz"}被encode 后的结果
4. 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()的结果
5. 特殊字符 `@`、`:`、`$`、`,`、``、`[`、`]`
axios({
method: 'get',
url: '/base/get',
params: {
foo: '@:$, '
}
})
请求时的url => /base/get?foo=@:$+
空格` `会转换成 `+`
6. undefined null 会忽略
axios({
method: 'get',
url: '/base/get',
params: {
foo: 'bar',
baz: null,
bar: undefined
}
})
请求时的url => /base/get?foo=bar
7. 去除请求中的哈希标记
axios({
method: 'get',
url: '/base/get#hash',
params: {
foo: 'bar'
}
})
请求时的url => /base/get?foo=bar
8. 保留请求中已有的参数
axios({
method: 'get',
url: '/base/get?foo=bar',
params: {
bar: 'baz'
}
})
请求时的url => /base/get?foo=bar&bar=baz
分析好了params可能存在的类型,接下来要做的就是定义一些辅助函数来帮助处理。依照模块化编程思想,我们可以新建一个helpers目录,在helpers目录下再创建一个utils.js文件,将这些工具函数都放在utils文件中,以后一些公用的工具函数也都放在这里。
在utils.js中我们先定义这两个isDate和isObject函数,他们作用也是不言而喻,很简单,如下:
const toString = Object.prototype.toString
export function isDate (val){
return toString.call(val) === '[object Date]'
}
export function isObject (val) {
return val !== null && typeof val === 'object'
}
为什么要自己定义呢?因为简单,就不需要去使用lodash库了。定义好之后,就要处理将params拼接到url上的问题了。我们再新建一个url.js文件,专门存放处理url相关的函数。为了处理以上可能存在的params情况,在url.js中新建两个函数,分别是buildURL和encode函数,如下:
import { isDate, isObject } from './util'
// 为了处理特殊字符
function encode (val) {
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 bulidURL (url, params) {
if (!params) {
return url
}
const parts= []
// 遍历params
Object.keys(params).forEach((key) => {
let 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 markIndex = url.indexOf('#')
if (markIndex !== -1) {
url = url.slice(0, markIndex)
}
// 处理已有的参数
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
}
return url
}
写好了这个,然后运用起来。回到index.js文件中,引入这个函数,如下:
function axios (config){
processConfig(config)
xhr(config)
}
// 处理config
function processConfig (config){
config.url = transformUrl(config)
}
// 处理url
function transformUrl (config){
const { url, params } = config
return bulidURL(url, params)
}
这里新增的两个函数分别用来处理url和config。至于为什么要区分出另外两个函数,而不是直接在axios中编写。这是分工处理,未来还会有很多这样的处理函数,如果都直接放在axios函数中,则会非常臃肿,不便维护。我们可以将不同的功能放在各自的函数中,然后统一引入到axios中,这样看起来很舒服,也便于维护。
03-处理请求中的body数据
我们通过XMLHttpRequest对象实例的send方法来发送请求,并通过该方法的参数设置请求body数据。对于这个body数据类型,可以支持Document 和 BodyInit 类型。BodyInit 类型包括了 Blob, ArrayBuffer, TypedArray, URLSearchParams, FormData。当没有数据的时候,我们还可以传入 null。当然,肯定是支持普通对象的。目前仅考虑处理普通对象,其他的类型处理有点问题。
新建一个data.js文件,专门用来处理data。在send中是不能直接发送普通对象的,需要进行转换成json字符串。所以现在需要做两件事情,一是如何识别出普通对象,之前也写过一个isObject函数,但是对于 FormData、ArrayBuffer 这些类型,isObject 判断也为 true,但是这些类型的数据我们是不需要做处理的。因此需要重新写一个判别普通对象的函数。二是将识别出来的普通对象进行转换,不是普通对象的直接返回。
根据以上两件事情,首先在util.js中新增一个函数,如下:
export function isPlainObject (val) {
return toString.call(val) === '[object Object]'
}
在data.js中新增如下:
import { isPlainObject } from './util'
export function transformRequest (data){
if (isPlainObject(data)) {
return JSON.stringify(data)
}
return data
}
然后再回到index.js中加入这段处理函数,如下:
import { transformRequest } from './helpers/data'
```
function processConfig (config) {
config.url = transformURL(config)
config.data = transformRequestData(config)
}
// 处理data
function transformRequestData (config) {
return transformRequest(config.data)
}
可以看到,这里也是专门定义一个函数来处理data,和处理url一样。为了方便以后进行功能拓展,将各个功能区分开。
再回到之前说处理除了普通对象之外的类型会有问题,目前还不知道问题出在那里,感觉写法没啥问题,也不是跨域的问题,我加了处理跨域的还是无法解决,如下:
// 请求
const arr = new Int32Array([21, 31])
// var arrayBuffer = new ArrayBuffer(16)
// const data = new FormData()
// data.append('username', 'Groucho')
axios({
method: 'post',
url: '/base/buffer',
data: arr
})
// 返回
router.post('/base/buffer',function (req,res) {
// res.header("Access-Control-Allow-Origin","*");
let msg = []
req.on('data',(chunk) => {
if (chunk) {
msg.push(chunk)
}
})
req.on('end',() => {
let buf = Buffer.concat(msg)
res.json(buf.toJSON())
})
})
// app.all('*',function (req,res,next) {
// // 设置请求头为允许跨域
// res.header('Access-Control-Allow-Origin','*');
// // 设置服务器支持的所有头信息字段
// res.header('Access-Control-Allow-Headers','Content-Type, Content-Length, Authorization, Accept, X-Requested-With , yourHeaderFeild, sessionToken');
// // 设置服务器支持的所有跨域请求的方法
// res.header('Access-Control-Allow-Methods','PUT, POST, GET, DELETE, OPTIONS');
// if (req.method.toLowerCase() == 'options') {
// res.send(200); // 让options尝试请求快速结束
// } else {
// next();
// }
// });
04-处理请求中的header
在数据发送到服务端时,要想服务端能够正常的解析数据,就要给请求header设置正确的Content-Type。所以应该做到支持自定义header,且当传入的data为普通对象,而又没有传入Content-Type时,应该自动设置请求 header 的 Content-Type 字段为application/json;charset=utf-8。
那么开始实现这个需求。在helpers目录下新建一个header.js文件,文件写入以下代码:
import { isPlainObject } from './util'
function normalizeHeaderName (headers, normalizedName) {
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
}
normalizeHeaderName函数的作用时将header属性名规范化。因为header的属性名是大小写不敏感的,比如可以传入content-type的属性。那么可以通过normalizeHeaderName函数将其规范化,变成Content-Type,这样就方便做判断。然后在processHeaders函数中,判断是否传入header和设置Content-Type值,在data为普通对象的情况下,如果没有设置则自动添加上去。
然后再回到index.js文件中,将上面函数引入进去,如下:
function processConfig (config){
config.url = transformURL(config)
config.headers = transformHeaders(config)
config.data = transformRequestData(config)
}
function transformHeaders (config) {
const { headers = {}, data } = config
return processHeaders(headers, data)
}
新增一个transformHeaders函数,专门处理header对象。注意 config.headers = transformHeaders(config)必须写在config.data = transformRequestData(config)之前,因为在执行了transformRequestData(config)之后,config.data就是一个字符串了,因此processHeaders函数中的if (isPlainObject(data)) 就不能通过。
那么在这里处理完后,还要在真正发送请求的地方将这个header设置上去。回到xhr.js文件中,添加如下代码,给请求设置header,如下:
export default function xhr(config) {
const { data = null, url, method = 'get', headers } = config
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)
}
新增的代码很简单,就是遍历headers,然后逐个添加上去。并且当data为空的时候删除掉content-type的值。因为data为空,这个属性值也没有必要留着。
05-获取响应数据
上面几节都是处理发送请求,这节来处理响应数据。这节要做到能处理服务端响应的数据,并支持 Promise 链式调用的方式。还可以拿到 res 对象,并且该对象包括:服务端返回的数据 data,HTTP 状态码status,状态消息 statusText,响应头 headers、请求配置对象 config 以及请求的 XMLHttpRequest 对象实例 request。同时,对于该请求,还可以通过设置 XMLHttpRequest 对象的 responseType 属性,来指定它响应的数据类型。responseType 的类型值有这些个:"" | "arraybuffer" | "blob" | "document" | "json" | "text"。
那么总结一下需要实现的功能:拿到服务端响应的数据res,且支持链式调用。可以配置responseType属性。
回到xhr.js文件中,添加如下代码:
export default function xhr(config) {
// 返回promise
return new Promise((resolve) => {
// 添加responseType可选属性
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() {
// readyState不为4 表示请求还没结束
if (request.readyState !== 4) {
return
}
// 拿到所有响应标头
const responseHeaders = request.getAllResponseHeaders()
// 通过响应类型来拿到响应数据
const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
// 构建res
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
}
// 返回res
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)
})
}
到这里就全部实现了需求,很简单。 当然还有一步,那就是来到axios.js文件中,做一点小小的修改,代码如下:
function axios(config) {
processConfig(config)
return xhr(config)
}
因为xhr返回了一个promise,所以在axios中应该return出去,这样就可以实现链式调用了。
06-处理响应header
在上面拿到的res的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
每一行都是以回车符和换行符 \r\n 结束,它们是每个 header 属性的分隔符。那么就可以利用这点来处理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'
}
回到header.js中,添加如下代码:
export function parseHeaders(headers){
let parsed = Object.create(null)
if (!headers) return parsed
headers.split('\r\n').forEach(line => {
let [key, ...vals] = line.split(':')
key = key.trim().toLowerCase()
if (!key) return
const val = vals.join(':').trim()
parsed[key] = val
})
return parsed
}
这个代码很简单,就是将这段字符串分割,然后遍历来构建。那么再回到xhr.js中引入这个函数,做出如下修改:
const responseHeaders = parseHeaders(request.getAllResponseHeaders())
非常简单,nice!!!
07-处理响应data
当没有主动设置responseType时,服务端返回给我们的数据是字符串类型。那么可不可以自动的将其转换为JSON 对象呢?当然是可以的,而且也非常简单。在data.js中添加如下代码:
export function transformResponse(data){
if (typeof data === 'string') {
try {
data = JSON.parse(data)
} catch (error) {
// do nothing
}
}
return data
}
这段代码就是尝试将字符串类型数据转换为JSON对象,转换失败就原地返回。然后在axios.js中引入这段代码:
function axios(config) {
processConfig(config)
return xhr(config).then((res) => {
return transformResponseData(res)
})
}
function transformResponseData(res: AxiosResponse): AxiosResponse {
res.data = transformResponse(res.data)
return res
}
新增transformResponseData函数,专门用来处理响应的data。然后在axios函数中引用transformResponseData函数。这里有一个点要注意,为什么不像处理响应header一样,直接在xhr.js中引入,而是专门放到axios.js中引用呢?这是因为后续还会继续处理响应data数据。如果全都放在xhr.js中,就感觉功能模块划分不清晰。在xhr.js中应该保持其简洁,即只有其本身的功能。像这种后续会多次处理响应data数据的函数,不属于xhr应该有的功能。这个功能应该放在axios.js中,这才是真正的处理各种数据的地方。
08-异常情况处理
之前都是处理了正常接收请求的逻辑,现在需要对可能发生的一些错误情况进行处理。当发生错误请求时,能够在reject函数中获取到。那么现在来梳理一下可能有那些错误:
- 网络异常错误 当网络出现异常(比如不通)的时候,发送请求就会触发
XMLHttpRequest对象实例的error事件 - 超时错误 当发送请求后超过某个时间让仍然没有收到响应,则请求自动终止,并触发
timeout事件 - 处理非200状态码 当请求正常时,一般会返回200-300之间的HTTP状态码。对于不处于这个区间的状态码,也可以认为是一种错误的情况
那么理清了这些情况,就可以针对性的做出处理。那么在xhr.js中添加如下代码:
// 处理 网络异常错误
request.onerror = function handleError() {
reject(new Error('Network Error'))
}
const { data = null, url, method = 'get', headers, responseType,timeout } = config
// 默认下是0,即永不超时,也可以手动设置请求的超时时间
if (timeout) {
request.timeout = timeout
}
// 处理超时错误
request.ontimeout=function handleTimeout(){
reject(new Error(`Timeout of ${timeout}ms exceeded`))
}
request.onreadystatechange = function handleLoad() {
// readyState不为4 表示请求还没结束
if (request.readyState !== 4) {
return
}
// 拿到所有响应标头
const responseHeaders = request.getAllResponseHeaders()
// 通过响应类型来拿到响应数据
const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
// 构建res
const response = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
}
// 返回res
handleResponse(response)
}
// 处理非200-300之间状态码
function handleResponse(response) {
if (response.status >= 200 && response.status < 300) {
resolve(response)
} else {
reject(new Error(`Request failed with status code ${response.status}`))
}
}
当我们添加了以上代码后,就可以像下面这样使用了:
axios({
method: 'get',
url: '/error/get'
}).then((res) => {
console.log(res)
}).catch((e) => {
console.log(e)
})
非常简单,nice!!!
09-获取更多异常信息
对于上一节,我们虽然能够捕获到错误信息,但并不便于我们快速找到问题点。那么什么样的异常信息可以有助于找到我们找到问题呢?我觉得应该包含以下几个点:
- 错误文本信息
- 请求对象配置的config
- 错误代码code
- 发送的请求request
- 响应的对象response
如果能在异常信息中获取以上信息,那么排查错误就应该方便很多了。那么接下来就对上一个版本的错误信息进行一些修改,加点料。
在helpers目录下新建error.js文件,添加如下代码:
export class AxiosError {
constructor(
message,
config,
code = null,
request = null,
response = null
) {
this.message = message
this.config = config
this.code = code
this.request = request
this.response = response
this.isAxiosError = true
Object.setPrototypeOf(this, AxiosError.prototype) // 将当前对象的原型设置为 AxiosError.prototype
}
}
export function createError(
message,
config,
code = null,
request = null,
response = null
) {
const error = new AxiosError(message, config, code, request, response)
return error
}
这段代码中创建并导出了AxiosError和createError工厂函数。代码很简单,就是传入上面提到的信息并赋值就行。可以看到code、request、response都是默认赋值为null的。因为在不同的错误情况下他们不一定会有值。
然后再来实际去应用这段代码,回到xhr.js文件中,代码进行如下修改:
import { createError } from '../helpers/error'
request.onerror = function handleError() {
reject(createError('Network Error', config, null, request))
}
request.ontimeout = function handleTimeout() {
reject(createError(`Timeout of ${timeout} ms exceeded`, config, 'ECONNABORTED', request))
}
function handleResponse(response) {
if (!validateStatus || validateStatus(response.status)) {
resolve(response)
} else {
reject(
createError(
`Request failed with status code ${response.status}`,
config,
null,
request,
response
)
)
}
}
添加好了之后就能像下面这样使用了:
axios({
method: 'get',
url: '/error/timeout',
timeout: 2000
}).then((res) => {
console.log(res)
}).catch((e) => {
console.log(e.message)
console.log(e.code)
console.log(e.request)
})
非常简单,nice!!!
10-接口拓展
为了更方便的使用axios发送请求,比如像axios.post(url[, config])、axios.get(url[, config])或axios.request(config) 这几种方式,用起来就很简单了,也不必在config中指定url、method和data这些属性了。
那么接下来就来简单实现一下这个功能。这个功能是axios发送请求的功能,是一个核心功能,那么基于模块化编程思想,新建一个core目录,在这个目录下新增一个Axios,添加如下代码:
// 这个下面会说 就是发送请求功能
import dispatchRequest from './dispatchRequest'
export default class Axios {
request(config) {
return dispatchRequest(config)
}
get(url, config) {
return this._requestMethodWithoutData('get', url, config)
}
delete(url, config) {
return this._requestMethodWithoutData('delete', url, config)
}
head(url, config) {
return this._requestMethodWithoutData('head', url, config)
}
options(url, config) {
return this._requestMethodWithoutData('options', url, config)
}
post(url, data, config) {
return this._requestMethodWithData('post', url, data, config)
}
put(url, data, config) {
return this._requestMethodWithData('put', url, data, config)
}
patch(url, data, config) {
return this._requestMethodWithData('patch', url, data, config)
}
// get delete head options 请求不需要携带data
_requestMethodWithoutData(method, url, config) {
return this.request(
Object.assign(config || {}, {
method,
url
})
)
}
// post put patch 请求需要携带data
_requestMethodWithData(method, url, data, config) {
return this.request(
Object.assign(config || {}, {
method,
url,
data
})
)
}
}
可以看到所有调用的post,get方法等,最终调用的都是dispatchRequest方法。dispatchRequest也就是之前写的axios.js中的文件,只是现在将其拷贝到core下的dispatchRequest文件中,代码如下:
import xhr from './xhr'
import { buildURL } from '../helpers/url'
import { transformRequest,transformResponse } from '../helpers/data'
import { processHeaders } from '../helpers/headers'
export default function dispatchRequest(config){
// 发送请求前修改config
processConfig(config)
return xhr(config).then(res => {
// 拿到res后修改res
return transformResponseData(res)
})
}
function processConfig(config){
config.url = transformURL(config)
config.headers = transformHeaders(config)
config.data = transformRequestData(config)
}
function transformURL(config) {
const { url,params } = config
return buildURL(url,params)
}
function transformRequestData(config) {
return transformRequest(config.data)
}
function transformHeaders(config) {
const { headers = {},data } = config
return processHeaders(headers,data)
}
function transformResponseData(res) {
res.data = transformResponse(res.data)
return res
}
那么再来回想一下axios的用法,有axios(config),也有axios.post()或者axios.get()。可以看到,axios可以是一个函数(直接使用axios(config)),同时又是一个对象(可使用axios.post())。简单来说就是axios是一个混合对象,是函数,也有Axios 类的所有原型属性和实例属性。
下面就来实现一下axios。首先写一个辅助函数,作用是复制属性,将from里面的属性都复制到to中。在helpers/utils.js中添加一个函数,如下代码:
export function extend(to,from) {
for (const key in from) {
to[key] = from[key]
}
return to
}
接下来就是对axios.js文件做修改,用工厂模式去创建一个混合对象,代码如下:
import Axios from './core/Axios'
import { extend } from './helpers/util'
function createInstance() {
const context = new Axios()
const instance = Axios.prototype.request.bind(context)
extend(instance,context)
return instancee
}
const axios = createInstance()
export default axios
在createInstance工厂函数的内容,首先实例化了Axios实例的context,然后创建instance指向Axios.prototype.request方法,并绑定了上下文context;接着通过extend方法把context中的原型方法和实例方法都拷贝到instance上,这样就实现了一个混合对象:instance本身是一个函数,又拥有Axios类的所有原型和实例属性。最后工厂函数返回instance。
这样就通过createInstance工厂函数创建了axios,当直接调用axios方法就相当于执行了Axios类的request方法发送请求,也可以调用axios.get、axios.post等方法。
使用方法如下:
import axios from '../../src/index'
axios({
url: '/extend/post',
method: 'post',
data: {
msg: 'hi'
}
})
axios.request({
url: '/extend/post',
method: 'post',
data: {
msg: 'hello'
}
})
axios.get('/extend/get')
axios.options('/extend/options')
axios.delete('/extend/delete')
axios.head('/extend/head')
axios.post('/extend/post', { msg: 'post' })
axios.put('/extend/put', { msg: 'put' })
axios.patch('/extend/patch', { msg: 'patch' })
非常简单,nice!!!
11-axios函数重载
现在又有如下使用方法:
// 使用方法一
axios({
url: '/extend/post',
method: 'post',
data: {
msg: 'hi'
}
})
// 使用方法二
axios('/extend/post', {
method: 'post',
data: {
msg: 'hello'
}
})
那么要实现方法二的话,只能是对axios函数进行重载了,其实也就是多加一个参数。axios函数实际调用的是request函数,所以来修改request函数的实现。打开core中的Axios.js文件,修改request函数,如下代码:
// 支持两个参数,url必填,config选填
request(url,config) {
if (typeof url === 'string') {
if (!config) {
config = {}
}
config.url = url
} else {
config = url
}
return dispatchRequest(config)
}
判断url是否为字符串类型,如果为字符串类型,则对config判断,如果config没有传,则将config构造成空对象,同时将url添加到config.url中。如果config不是字符串类型,则表示传的为单个参数,此时url就是config,把url赋值给config就行。
非常简单,nice!!!
12-拦截器实现
有时候我们希望在发送请求前对请求头或响应数据做一些额外处理,这些处理可能是一个,也可能是多个。并且当前处理的数据是上一个函数返回的,可以理解成一个链式调用的过程。如下图:
可以看到每个拦截器都会返回一个对象供下一个拦截器处理,并且每个拦截器都可以支持同步和异步处理。当然,也是支持删除某个拦截器的。对于这种链式调用,首先想到的应该就是使用promise链的方式来实现这个过程了。
在这个 Promise 链的执行过程中,请求拦截器 resolve 函数处理的是 config 对象,而相应拦截器 resolve 函数处理的是 response 对象。
使用拦截器的方式如下:
// 添加一个请求拦截器
axios.interceptors.request.use(function (config) {
// 在发送请求之前可以做一些事情
return config;
}, function (error) {
// 处理请求错误
return Promise.reject(error);
});
// 添加一个响应拦截器
axios.interceptors.response.use(function (response) {
// 处理响应数据
return response;
}, function (error) {
// 处理响应错误
return Promise.reject(error);
});
// 删除
const myInterceptor = axios.interceptors.request.use(function () {/*...*/})
axios.interceptors.request.eject(myInterceptor)
那么现在就来具体实现一下这种使用方式。先分析一下,axios要拥有一个interceptors对象属性,该属性又有request和response两个属性,这俩属性又对外提供一个use方法来添加拦截器。use方法支持两个参数,第一个是resolve函数,第二个是reject函数。
那么先来实现一下这个interceptors对象,在core目录下,新建InterceptorManager.js文件,添加如下代码:
export default class InterceptorManager {
interceptors;
constructor() {
this.interceptors = []
}
use(resolved, rejected) {
this.interceptors.push({
resolved,
rejected
})
return this.interceptors.length - 1
}
forEach(fn) {
this.interceptors.forEach(interceptor => {
if (interceptor !== null) fn(interceptor)
})
}
eject(id) {
if (this.interceptors[id] !== null) this.interceptors[id] = null
}
}
分析一下代码,首先在初始化时将interceptors重置为[]。
use方法接收两个参数resolve和reject,然后将这两个函数同时放到同一个对象中,并这个对象添加到interceptors里,最后返回了这个对象在interceptors的index信息,以供删除使用。
forEach方法则是供内部使用,后面会用到,作用就是遍历interceptors并将里面的对象挨个放到fn中去执行。
eject方法则接收一个id,这个id就是use函数返回的index,通过id找到这个拦截器,然后置为null。
然后在core/Axios.js中新增代码:
import InterceptorManager from './InterceptorManager' // 新增
import dispatchRequest,{ transformURL } from './dispatchRequest'
import mergeConfig from './mergeConfig'
export default class Axios {
defaults;
interceptors;
constructor() {
this.interceptors = {
request: new InterceptorManager,
response: new InterceptorManager
} // 新增
}
request(url,config){
if (typeof url === 'string') {
if (!config) {
config = {}
}
config.url = url
} else {
config = url
}
config = mergeConfig(this.defaults,config)
config.method = config.method.toLowerCase()
// 链式 新增
const chain = [
{
resolved: dispatchRequest,
rejected: undefined
}
]
// 新增
this.interceptors.request.forEach(interceptor => {
chain.unshift(interceptor)
})
this.interceptors.response.forEach(interceptor => {
chain.push(interceptor)
})
// 新增
let promise = Promise.resolve(config)
// 新增
while (chain.length) {
const { resolved,rejected } = chain.shift()
promise = promise.then(resolved,rejected)
}
return promise
}
get(url,config) {
return this._requestMethodWidthoutData('get',url,config)
}
delete(url,config) {
return this._requestMethodWidthoutData('delete',url,config)
}
head(url,config) {
return this._requestMethodWidthoutData('head',url,config)
}
options(url,config) {
return this._requestMethodWidthoutData('options',url,config)
}
post(url,data,config) {
return this._requestMethodWidthData('post',url,data,config)
}
put(url,data,config) {
return this._requestMethodWidthData('put',url,data,config)
}
patch(url,data,config) {
return this._requestMethodWidthData('patch',url,data,config)
}
getUri(config) {
config = mergeConfig(this.defaults,config)
return transformURL(config)
}
_requestMethodWidthoutData(method,url,config) {
return this.request(
Object.assign(config || {},{
method,
url
})
)
}
_requestMethodWidthData(method,url,data,config) {
return this.request(
Object.assign(config || {},{
method,
url,
data
})
)
}
}
首先引入InterceptorManager这个类,然后在Axios类中新增interceptors属性,在初始化时,将这个interceptors属性赋值为对象,这个对象有request和response属性,这俩属性又分别是InterceptorManager对象。有了这个之后就可以使用axios.interceptors.request.use()和axios.interceptors.response.use()了。
新增一个chain变量来保存拦截器。首先这个chain中自带一个默认的拦截器,就是dispatchRequest函数。this.interceptors.request.forEach(interceptor => { chain.unshift(interceptor) }) 这段代码就是将request拦截器遍历插入到 chain 的前面,那么对于请求拦截器,就会先执行后添加的。this.interceptors.response.forEach(interceptor => { chain.push(interceptor) }) 这个同理,就是将response拦截器遍历插入到 chain 后面,则对于响应拦截器,就会先执行先添加的。然后while函数就是遍历chain数组,拿到每个拦截器对象,把它们的 resolved 函数和 rejected 函数添加到 promise.then 的参数中,这样就相当于通过 Promise 的链式调用方式,实现了拦截器一层层的链式调用的效果。
非常简单,nice!!!
13-实现配置化
我们希望也希望 axios 可以有默认配置,定义一些默认的行为。这样在发送每个请求,用户传递的配置可以和默认配置做一层合并。当然,对于这些默认配置,我们也是可以去直接修改的。
在src目录下新建defaults.js文件,添加如下代码:
const defaults = {
method: 'get',
timeout: 0,
headers: {
common: {
Accept: 'application/json, text/plain, */*'
}
},
}
const methodNoData = ['delete','get','head','options']
methodNoData.forEach(method => {
defaults.headers[method] = {}
})
const methodWithData = ['put','patch','post']
methodWithData.forEach(method => {
defaults.headers[method] = {
'Content-Type': 'application/x-www-form-urlencoded'
}
})
export default defaults
这些代码很简单,就是加一些默认的属性值,对于'delete','get','head','options'方法,默认为对象;而'put','patch','post'方法默认添加一个'Content-Type'属性。当然,在这里可以拓展一些项目特殊的属性。
那应该在那里使用这个默认配置呢?回想一下上一节的拦截器实现,有用到config配置。那么既然是默认配置,就是在拦截器使用之前来使用这个。
在core/Axios.js中新增代码:
import mergeConfig from './mergeConfig'
// 新增一个参数
constructor(initConfig) {
this.defaults = initConfig
this.interceptors = {
request: new InterceptorManager,
response: new InterceptorManager
}
}
// 在request函数中新增
if (typeof url === 'string') {
if (!config) {
config = {}
}
config.url = url
} else {
config = url
}
config = mergeConfig(this.defaults, config)
config.method = config.method.toLowerCase() // 统一将方法名小写
新增的代码很简单,就是做一层配置合并。但是,要注意合并策略,因为字段不同,合并的策略也是不一样的。这里要分析一下合并策略的使用。
在core目录下新建mergeConfig.js文件:
import { isPlainObject,deepMerge } from '../helpers/util'
import { AxiosRequestConfig } from '../type'
const strats = Object.create(null)
// 合并策略1
function defaultStrat(val1,val2) {
return typeof val2 !== 'undefined' ? val2 : val1
}
// 合并策略2
function fromVal2Strat(val1,val2) {
if (typeof val2 !== 'undefined') return val2
}
// 合并策略3
function deepMergeStrat(val1,val2) {
if (isPlainObject(val2)) {
return deepMerge(val1,val2)
} else if (typeof val2 !== 'undefined') {
return val2
} else if (isPlainObject(val1)) {
return deepMerge(val1)
} else {
return val1
}
}
// 默认取val2的值 --》 使用fromVal2Strat策略
const stratKeysFromVal2 = ['url','params','data']
stratKeysFromVal2.forEach(key => {
strats[key] = fromVal2Strat
})
// val2有则取val2 没有则取val1 --》 使用deepMergeStrat
const stratKeysDeepMerge = ['headers','auth']
stratKeysDeepMerge.forEach(key => {
strats[key] = deepMergeStrat
})
export default function mergeConfig(
config1,
config2
) {
if (!config2) config2 = {}
const config = Object.create(null) // 定义一个空对象
for (let key in config2) {
mergeFiled(key)
}
for (let key in config1) {
if (!config2[key]) mergeFiled(key)
}
function mergeFiled(key) {
const strat = strats[key] || defaultStrat // 如果即没有使用fromVal2Strat和deepMergeStrat,则使用defaultStrat
config[key] = strat(config1[key],config2[key])
}
return config
}
对于'url','params','data'属性肯定是优先取传入的值,而'headers','auth'则是有传入取传入,没有取默认的。对于其他的属性,比如timeout也是有传入取传入,没有取默认的。但是headers和timeout不同,timeout是属性,而headers是复杂的对象,所以不能用同一种策略。
经过合并后的配置中的 headers 是一个复杂对象,多了 common、post、get 等属性,而这些属性中的值才是我们要真正添加到请求 header 中的。比如:
// 压缩前
headers: {
common: {
Accept: 'application/json, text/plain, */*'
},
post: {
'Content-Type':'application/x-www-form-urlencoded'
}
}
// 压缩后
headers: {
Accept: 'application/json, text/plain, */*',
'Content-Type':'application/x-www-form-urlencoded'
}
要压缩header,可以在helpers/header.js中新建一个函数flattenHeaders函数,如下:
export function flattenHeaders(headers,method) {
if (!headers) {
return headers
}
headers = deepMerge(headers.common,headers[method],headers)
const methodToDelete = ['delete','get','head','options','post','put','patch','common']
methodToDelete.forEach(method => {
delete headers[method]
})
return headers
}
然后来到真正发送请求的地方来压缩header逻辑,在core/dispatchRequest.js中添加如下代码:
function processConfig(config) {
config.url = transformURL(config)
config.headers = transformHeaders(config)
config.data = transformRequestData(config)
config.headers = flattenHeaders(config.headers,config.method) // 新增
}
至于为什么不在拦截器那里使用这个方法,是因为在拦截器中还会对config进行修改。那么到这里就实现了config的默认配置了。
然后再来到axios.js文件中,使用这个默认配置,在createInstance函数中新增一个参数config即可,代码如下:
function createInstance(config){
const context = new Axios(config)
const instance = Axios.prototype.request.bind(context)
extend(instance, context)
return instance as AxiosStatic
}
const axios = createInstance(defaults)
很简单,当使用axios时即默认使用了基础配置。
使用方法如下:
axios.defaults.headers.common['test2'] = 123
axios({
url: '/config/post',
method: 'post',
data: qs.stringify({
a: 1
}),
headers: {
test: '321'
}
}).then((res) => {
console.log(res.data)
})
非常简单,nice!!!
14-扩展axios.create静态接口
目前为止,我们的axios都是一个单例,一旦修改了axios的默认配置,就会影响所有的请求。那么我们希望能提供一个axios.create的静态接口来允许创建一个新的axios实例,同时允许传入新的配置和默认配置合并,并作为新的默认配置。
其实这个也挺简单,调用createInstance函数并返回即可,在axios.js中新增如下代码:
axios.create = function create(config) {
return createInstance(mergeConfig(defaults, config))
}
内部调用了 createInstance 函数,并且把参数 config 与 defaults 合并,作为新的默认配置。
非常简单,nice!!!
15-实现取消功能
这个功能应该算是比较重要的了,也是比较难理解的地方。对我而言,我也是看了三遍视频再加上执行代码才稍微懂了。
请求的发送是一个异步过程,最终会执行xhr.send方法,xhr对象提供了abort方法,可以取消请求。但是我们在外部并不能访问到xhr对象,所以我们想要在执行cancel方法的时候,去执行xhr.abort方法。就需要在xhr异步请求过程中,插入一段代码。当我们在外部执行cancel函数的时候,会驱动这段代码的执行,然后去执行xhr.abort方法取消请求。
那么如何实现异步过程中插入代码,并在外部驱动这段代码执行呢? 可以利用promise实现异步分离,也就是在cancel中保存一个pending状态的Promise对象,然后当我们执行cancel方法时,能够访问到这个Promise对象,把它从pending状态变成resolve状态,这样就可以在then函数中实现取消请求的逻辑。
这里可能有点不太理解到底是个什么意思,没事,接着往下看,后面会详细介绍啥意思。
那么对于取消功能,有以下两种方式来实现:
// 第一种
const CancelToken = axios.CancelToken;
const source = CancelToken.source();
axios.get('/user/12345', {
cancelToken: source.token
}).catch(function (e) {
if (axios.isCancel(e)) {
console.log('Request canceled', e.message);
} else {
// 处理错误
}
});
// 取消请求 (请求原因是可选的)
source.cancel('Operation canceled by the user.');
// 第二种
const CancelToken = axios.CancelToken;
let cancel;
axios.get('/user/12345', {
cancelToken: new CancelToken(function executor(c) {
cancel = c;
})
});
// 取消请求
cancel();
首先来看第二种的实现吧,看着代码比较少。在core目录下新建CancelToken.js文件,添加以下代码:
export default class CancelToken {
promise
reason
constructor(executor) {
let resolvePromise
this.promise = new Promise(resolve => {
resolvePromise = resolve
})
executor(message => {
// 如果已经有reason 表示已经取消过了
if (this.reason) {
return
}
this.reason = message
resolvePromise(this.reason)
})
}
}
首先,在创建CancelToken对象的时候要传入一个executor函数,这个executor函数也接收一个message参数。在CancelToken对象中还有两个属性reason和promise。在constructor内部,new Promise(resolve => {resolvePromise = resolve})这段代码的意思是返回一个Promise对象,注意此时是pendding状态。同时将Promise中的resovle函数赋值给resolvePromise。那么我们就可以在外部调用这个resolvePromise函数,当resolvePromise函数被执行,则Promise对象会转变为resolve状态。然后执行executor函数时,就会调用resolvePromise函数,此时Promise对象就转变为了resolve状态。记住这里的Promise状态变化,下面会用到。
然后我们在core目录下的xhr.js中添加一段代码:
if (cancelToken) {
cancelToken.promise.then(reason => {
request.abort()
reject(reason)
})
}
这个cancelToken就是上面的CancelToken对象。如果这个对象存在,那么就去访问该对象的promise属性,这个属性是一个Promise对象,如果是resolve状态,就会往下执行then方法,从而执行request.abort();如果是pendding状态,则不会执行。
那么这里就实现了取消功能的第二种方法了。再来看看第一种,其实也是一样的,只是在内部做了封装,还是在core目录下CancelToken.js文件,添加如下代码:
export default class CancelToken {
//.........
static source() {
let cancel
const token = new CancelToken(c => {
cancel = c
})
return {
cancel,
token
}
}
}
仔细看看这个,有么有和第二种取消功能的方法长的一样。其实就是封装了一下,本质上还是一样。
那么这里就实现了取消功能。当然,还有一些地方要注意。比如,当捕获请求时,我们要怎么判断这个错误参数error是不是取消请求导致的呢?要实现这个功能,得加点判断。在core目录下新建Cancel.js文件,添加如下代码:
export default class Cancel {
message
constructor(message) {
this.message = message
}
}
export function isCancel(value) {
return value instanceof Cancel
}
代码很简单,就是做一层包裹,再加个判断函数就完事了。使用的话,就是在CancelToken类中使用,在core目录下CancelToken.js中稍微修改一下:
executor(message => {
if (this.reason) return
this.reason = new Cancel(message) // 修改部分
resolvePromise(this.reason)
})
然后给axios拓展一些静态方法,以供使用上面这些方法,在axios.js文件中添加以下代码:
import CancelToken from './cancel/CancelToken'
import Cancel, { isCancel } from './cancel/Cancel'
axios.CancelToken = CancelToken
axios.Cancel = Cancel
axios.isCancel = isCancel
还要注意到一个问题,比如当一个请求携带的 cancelToken 已经被使用过,那么我们甚至都可以不发送这个请求,只需要抛一个异常即可,并且抛异常的信息就是我们取消的原因,所以我们需要给 CancelToken 扩展一个方法。
在cancel/CancelToken.js文件中添加代码:
export default class CancelToken {
// ...
// 判断如果存在 `this.reason`,说明这个 `token` 已经被使用过了,直接抛错。
throwIfRequested(){
if (this.reason) {
throw this.reason
}
}
}
然后再发送请求的地方使用这个方法。在core/dispatchRequest.js中添加如下代码:
export default function dispatchRequest(config) {
throwIfCancellationRequested(config) // 新增 放在最前面判断
processConfig(config)
return xhr(config).then(
res => {
return transformResponseData(res)
},
e => {
if (e && e.response) {
e.response = transformResponseData(e.response)
}
return Promise.reject(e)
}
)
}
function throwIfCancellationRequested(config){
if (config.cancelToken) {
config.cancelToken.throwIfRequested()
}
}
那么到这里就实现了完整的取消功能了,也考虑了一些额外情况并做了处理。
非常简单,nice!!!
16-实现XSRF防御
CSRF 的防御手段有很多,比如验证请求的 referer,但是 referer 也是可以伪造的,所以杜绝此类攻击的一种方式是服务器端要求每次请求都包含一个 token,这个 token 不在前端生成,而是在我们每次访问站点的时候生成,并通过 set-cookie 的方式种到客户端,然后客户端发送请求的时候,从 cookie 中对应的字段读取出 token,然后添加到请求 headers 中。这样服务端就可以从请求 headers 中读取这个 token 并验证,由于这个 token 是很难伪造的,所以就能区分这个请求是否是用户正常发起的。
那么我们应该如何去实现呢?其实就是在发送请求时,从cookie中读取对应的token值,然后添加到请求header中。我们允许用户配置xsrfCookieName和xsrfHeaderName,其中xsrfCookieName表示存储token的cookie名称,xsrfHeaderName表示请求header中token对应的header名称。比如像下面这种:
axios.get('/more/get',{
xsrfCookieName: 'XSRF-TOKEN', // default
xsrfHeaderName: 'X-XSRF-TOKEN' // default
}).then(res => {
console.log(res)
})
那么接下来就来实现这一需求。首先,在defaults.js文件中添加这两个属性,并给出默认值,代码如下:
const defaults = {
// ...
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
}
然后我们还要做三件事情:
- 首先判断如果是配置
withCredentials为true或者是同域请求,我们才会请求headers添加xsrf相关的字段 - 如果判断成功,尝试从 cookie 中读取
xsrf的token值 - 如果能读到,则把它添加到请求
headers的xsrf相关字段中
先实现第一件事情,在helpers目录下新建url.js文件,添加如下代码:
// 对比url判断是否跨域
export function isURLSameOrigin(requestURL) {
const parsedOirgin = resolveURL(requestURL)
return (
parsedOirgin.host === currentOrigin.host && parsedOirgin.protocol === currentOrigin.protocol
)
}
const urlParsingNode = document.createElement('a')
const currentOrigin = resolveURL(window.location.href)
function resolveURL(url) {
urlParsingNode.setAttribute('href',url)
const { protocol,host } = urlParsingNode
return {
protocol,
host
}
}
同域名的判断主要利用了一个技巧,创建一个 a 标签的 DOM,然后设置 href 属性为我们传入的 url,然后可以获取该 DOM 的 protocol、host。当前页面的 url 和请求的 url 都通过这种方式获取,然后对比它们的 protocol 和 host 是否相同即可。
然后实现cookie的读取。在helpers目录下新建cookie.js文件,添加如下代码:
const cookie = {
read(name) {
const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'))
return match ? decodeURIComponent(match[3]) : null
}
}
export default cookie
最后在发送请求时给header加上这个cookie。在core/xhr.js中添加如下代码:
const {
/*...*/
xsrfCookieName,
xsrfHeaderName
} = config
if ((withCredentials || isURLSameOrigin(url)) && xsrfCookieName) {
const xsrfValue = cookie.read(xsrfCookieName)
if (xsrfValue) {
headers[xsrfHeaderName] = xsrfValue
}
}
非常简单,nice!!!
17-实现上传和下载的进度监控
有些时候,当我们上传文件或者是请求一个大体积数据的时候,希望知道实时的进度,甚至可以基于此做一个进度条的展示。(当然,进度条这里就不实现了)
xhr 对象提供了一个 progress 事件,我们可以监听此事件对数据的下载进度做监控;另外,xhr.uplaod 对象也提供了 progress 事件,我们可以基于此对上传进度做监控。
那么,基于此,我们就给axios 的请求配置提供 onDownloadProgress 和 onUploadProgress 2 个函数属性,用户可以通过这俩函数实现对下载进度和上传进度的监控。
在core/xhr.js中添加如下代码:
const {
/*...*/
onDownloadProgress,
onUploadProgress
} = config
if (onDownloadProgress) {
request.onprogress = onDownloadProgress
}
if (onUploadProgress) {
request.upload.onprogress = onUploadProgress
}
另外,如果请求的数据是 FormData 类型,我们应该主动删除请求 headers 中的 Content-Type 字段,让浏览器自动根据请求数据设置 Content-Type。比如当我们通过 FormData 上传文件的时候,浏览器会把请求 headers 中的 Content-Type 设置为 multipart/form-data。
所以,添加一个判断 FormData 的方法。在helpers/util.js中添加如下代码:
export function isFormData(val) {
return typeof val !== 'undefined' && val instanceof FormData
}
然后再回到core/xhr.js中添加如下代码:
if (isFormData(data)) {
delete headers['Content-Type']
}
使用方式如下:
axios.get('/more/get',{
onDownloadProgress(progressEvent) {
// 监听下载进度
}
})
axios.post('/more/post',{
onUploadProgress(progressEvent) {
// 监听上传进度
}
})
非常简单,nice!!!
18-自定义合法状态码
一般来说,在处理响应结果的时候,认为 HTTP status 在 200 和 300 之间是一个合法值,在这个区间之外则创建一个错误。有些时候我们想自定义这个规则,比如认为 304 也是一个合法的状态码,所以我们希望 axios 能提供一个配置,允许我们自定义合法状态码规则,使用方式如下:
axios.get('/more/304', {
validateStatus(status) {
return status >= 200 && status < 400
}
}).then(res => {
console.log(res)
}).catch((e: AxiosError) => {
console.log(e.message)
})
那么来实现这一需求,在defaults.js中添加如下代码:
const defaults = {
// ....
// 新增
validateStatus(status) {
return status >= 200 && status < 300
}
}
然后再请求后对响应数据的处理逻辑。在core/xhr.js添加如下代码:
const {
/*...*/
validateStatus
} = config
function handleResponse(response) {
if (!validateStatus || validateStatus(response.status)) {
resolve(response)
} else {
reject(
createError(
`Request failed with status code ${response.status}`,
config,
null,
request,
response
)
)
}
}
如果没有配置 validateStatus 以及 validateStatus 函数返回的值为 true 的时候,都认为是合法的,正常 resolve(response),否则都创建一个错误。
非常简单,nice!!!
19-自定义参数序列化
在之前的章节,我们对请求的 url 参数做了处理,我们会解析传入的 params 对象,根据一定的规则把它解析成字符串,然后添加在 url 后面。在解析的过程中,我们会对字符串 encode,但是对于一些特殊字符比如 @、+ 等却不转义,这是 axios 库的默认解析规则。当然,我们也希望自己定义解析规则,于是我们希望 ts-axios 能在请求配置中允许我们配置一个 paramsSerializer 函数来自定义参数的解析规则,该函数接受 params 参数,返回值作为解析后的结果,如下:
axios.get('/more/get', {
params: {
a: 1,
b: 2,
c: ['a', 'b', 'c']
},
paramsSerializer(params) {
return qs.stringify(params, { arrayFormat: 'brackets' })
}
}).then(res => {
console.log(res)
})
那么就来修改一下 buildURL 函数的实现,在helpers/url.js中修改代码:
export function bulidURL(
url,
params,
paramsSerializer
){
if (!params) {
return url
}
let serializedParams
if (paramsSerializer) {
serializedParams = paramsSerializer(params)
} else if (isURLSearchParams(params)) {
serializedParams = params.toString()
} else {
const parts = []
Object.keys(params).forEach(key => {
let 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)}`)
})
})
serializedParams = parts.join('&')
}
if (serializedParams) {
const markIndex = url.indexOf('#')
if (markIndex !== -1) {
url = url.slice(0,markIndex)
}
url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams
}
return url
}
这里我们给 buildURL 函数新增了 paramsSerializer 可选参数,另外我们还新增了对 params 类型判断,如果它是一个 URLSearchParams 对象实例的话,我们直接返回它 toString 后的结果。
对于isURLSearchParams函数,在helpers/util.js中新增如下代码:
export function isURLSearchParams(val) {
return typeof val !== 'undefined' && val instanceof URLSearchParams
}
最后一步就是修改 buildURL 调用的逻辑,在core/dispatchRequest.js中修改代码:
function transformURL(config) {
const { url, params, paramsSerializer } = config
return buildURL(url!, params, paramsSerializer)
}
非常简单,nice!!!
20-实现baseURL
有些时候,我们会请求某个域名下的多个接口,我们不希望每次发送请求都填写完整的 url,希望可以配置一个 baseURL,之后都可以传相对路径。如下:
const instance = axios.create({
baseURL: 'https://some-domain.com/api'
})
instance.get('/get')
instance.post('/post')
我们一旦配置了 baseURL,之后请求传入的 url 都会和我们的 baseURL 拼接成完整的绝对地址,除非请求传入的 url 已经是绝对地址。
那么要实现这个功能,先实现两个辅助功能,第一个是判断是否为绝对路径,第二个是拼接路径,在helpers/url.js中添加如下代码:
export function isAbsoluteURL(url){
return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url)
}
export function combineURL(baseURL,relativeURL) {
return relativeURL ? baseURL.replace(/\/+$/,'') + '/' + relativeURL.replace(/^\/+/,'') : baseURL
}
最后我们来调用这俩个辅助函数,在core/dispatchRequest.js中添加如下代码:
function transformURL(config){
let { url,params,paramsSerializer,baseURL } = config
if (baseURL && !isAbsoluteURL(url)) {
url = combineURL(baseURL,url)
}
return buildURL(url,params,paramsSerializer)
}
非常简单,nice!!!
21-完结
磕磕绊绊花了一个多月的时间才写完,期间还有几个月完全没有写,不然早就应该写完了吧。不过还好,终于是写完了。写完和看完真的是两种状态,看完就觉得非常简单,轻松拿捏;写完才知道,期间好多细节我都不知道,还得重新查找资料才能理解。
写完了,该干下一件事情了。希望下件事情不会这么拖拉。冲冲冲,干了这碗鸡汤,你我皆是黑马。