我正在参加「掘金·启航计划」
经过了前面的章节,我们继续来完善axios的功能。
我们从文档中可以看到,axios还有哪些额外功能提供给用户。
实现cancel功能
为什么需要这个功能:
- 我们请求时,如果响应很慢,除了设置
timeout挂起之外,希望有一个功能,能够提前或者手动的将请求挂起,释放资源。
我们看文档得知,源码中使用了xhr提供的api接口AbortController
首先,修改AxiosRequestConfig
// types/index.ts
export interface AxiosRequestConfig {
url?: string
method?: Method
data?: any
params?: any
headers?: any
responseType?: XMLHttpRequestResponseType
timeout?: number
transformRequest?: AxiosTransformer | AxiosTransformer[]
transformResponse?: AxiosTransformer | AxiosTransformer[]
// new
signal?: AbortController['signal']
[propName: string]: any
}
// core/dispatchRequest.ts
function throwIfCancellationRequested(config: AxiosRequestConfig) {
if (config.signal && config.signal.aborted) {
throw createError(config.signal.reason || 'Request aborted', config, 'ECONNABORTED', config.request)
}
}
export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
// new
throwIfCancellationRequested(config)
processConfig(config)
return xhr(config).then(res => {
// new
throwIfCancellationRequested(config)
return transformResponseData(res)
})
}
// core/xhr.ts
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise((resolve, reject) => {
const {
// 省略...
signal
} = config
let onCanceled: (cancel?: Event) => any;
// 省略...
if (signal) {
const onCanceled = (cancel?: Event) => {
if (!request) {
return
}
reject(
!cancel || cancel.type
? createError('Request aborted', config, 'ECONNABORTED', request)
: cancel
)
request.abort()
}
signal.aborted ? onCanceled() : signal.addEventListener('abort', onCanceled)
}
// 省略...
function handleResponse(response: AxiosResponse): void {
if (response.status >= 200 && response.status < 300) {
resolve(response)
if (config.signal) {
// new
config.signal.removeEventListener('abort', onCanceled);
}
} else {
reject(
createError(
`Request fail with status code ${response.status}`,
config,
undefined,
request,
response
)
)
// new
config.signal.removeEventListener('abort', onCanceled);
}
}
测试用例
import axios from '../../src/index'
const controller = new AbortController()
axios
.get('/cancel/get', {
signal: controller.signal
})
.then(function () {
controller.abort('Operation canceled by the user.')
})
.catch(function (e) {
console.log(e)
})
axios
.get('/cancel/get', {
signal: controller.signal
})
.catch(function (e) {
console.log(e)
})
axios
.get('/cancel/get', {
signal: controller.signal
})
.catch(function (e) {
console.log(e)
})
可以看出,如果我们传入signal,然后执行controller.abort(),后面的请求就被终止,不会发出xhr请求。
实现withCredentials功能
为什么需要这个功能:
- 我们在发送请求中,可能有一些跨域的请求,而浏览器根据同源策略会限制这类请求,但是可以通过
CROS来实现跨域 - 同域的情况下,发送的请求会携带
cookies,但在跨域的情况下默认是不携带的
跨域资源共享 (CORS)是一种机制,它使用额外的HTTP头来告诉浏览器,让运行在一个origin (domain) 上的Web应用被准许访问来自另一个源服务器上的指定资源。只要服务器设置了相应的HTTP响应头,就可以通过CORS实现跨域访问。
因为xhr中自带这个属性,我们只需要传递过去即可
首先,修改AxiosRequestConfig
// types/index.ts
export interface AxiosRequestConfig {
// 省略...
withCredentials?: boolean
}
// core/xhr.ts
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise((resolve, reject) => {
// 省略...
if (withCredentials) {
request.withCredentials = withCredentials
}
实现XSRF防御功能
怎么理解XSRF,这是一种常见的攻击手段,设想一个场景:
- 假如我现在登录了银行
www.mybank.com,并操作了转账。 - 随后我又登录了一个网站
www.pictures.com,但是这个网站是恶意网站。 - 此时因为默认携带
cookies,会将mybank的cookies传到pictures中。 - 而攻击者恰好也请求
mybank,就可以通过我们的cookies去伪造用户操作。
为了杜绝这种情况,我们需要服务端每次去生成一个token并通过set-cookies的形式在客户端生成cookies
而我们axios库需要做的事:
- 从
cookies中取出token - 添加到对应请求的
headers中
我们想实现如下效果:
axios.get('/more/get',{
xsrfCookieName: 'XSRF-TOKEN', // default
xsrfHeaderName: 'X-XSRF-TOKEN' // default
}).then(res => {
console.log(res)
})
首先,修改代码
// types/index.ts
export interface AxiosRequestConfig {
// 省略...
xsrfCookieName?: string
xsrfHeaderName?: string
// 省略...
}
// defaults.ts
const defaults: AxiosRequestConfig = {
// 省略...
xsrfCookieName: 'XSRF-TOKEN',
xsrfHeaderName: 'X-XSRF-TOKEN',
// 省略...
}
然后,我们需要做以下处理:
- 同源 ||
withCredentials = true - 获取
token - 添加到请求中
// helpers/url.ts
// 判断是否同源
interface URLOrigin {
protocol: string
host: string
}
const urlParsingNode = document.createElement('a')
const currentOrigin = resolveURL(window.location.href)
function resolveURL(url: string): URLOrigin {
urlParsingNode.setAttribute('href', url)
const { protocol, host } = urlParsingNode
return {
protocol,
host
}
}
export function isURLSameOrigin(requestURL: string): boolean {
const parsedOrigin = resolveURL(requestURL)
return (
parsedOrigin.protocol === currentOrigin.protocol && parsedOrigin.host === currentOrigin.host
)
}
然后,我们来实现提取cookies的方法:
// helpers/cookie.ts
const cookie = {
read(name: string): string | null {
// ^ 匹配输入的开始
// | 匹配 | 左右两边的表达式
// \s* 匹配零个或多个空白字符
// ; 匹配分号
// ([^;]*) 匹配除了 ; 以外的任意字符
const match = document.cookie.match(new RegExp('(^|;\\s*)(' + name + ')=([^;]*)'))
return match ? decodeURIComponent(match[3]) : null
}
}
export default cookie
然后放入request中
// core/xhr.ts
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise((resolve, reject) => {
const {
data = null,
url,
method = 'get',
headers,
responseType,
timeout,
withCredentials,
signal,
xsrfCookieName,
xsrfHeaderName
} = config
// 省略...
if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName) {
const xsrfValue = cookie.read(xsrfCookieName)
if (xsrfValue) {
headers[xsrfHeaderName!] = xsrfValue
}
}
// 省略...
至此,我们就实现了XSRF防御功能
总结
通过上述的编码,我们拓展了axios库的更多功能,通过实现这个库,也拓展了自身的知识和视野。