ToC
前置
在学习之前 fetch 之前需要提前了解一下以下 API
- Promise
- Headers
- Request
- Response
- DOMException:在调用方法或访问 Web API 属性时发生的不正常事件时抛出的错误类型
core
在来到这个章节以前,默认你已经了解过上面的 API 了。那么首先我们知道 fetch 函数接收两个参数并返回一个 Promise,我们先定义一下:
function fetch(input: RequestInfo | URL, init?: RequestInit) {
return new Promise(function(resolve, reject) {
const request = new Request(input, init)
// 如果这个请求已经被终止了则不继续
if (request.signal && request.signal.aborted) {
return reject(new DOMException('Aborted', 'AbortError'))
}
// 创建一个 xhr 请求对象
const xhr = new XMLHttpRequest()
// 当请求完成时
xhr.onload = function () {
const headers = new Headers(init?.headers || {})
const options = {
headers,
status: xhr.status,
statusText: xhr.statusText,
url: 'responseURL' in xhr ? xhr.responseURL : headers.get('X-Request-URL')
}
const body = xhr.response || xhr.responseText
// 如果状态正常, 并且是以 file:// 开头的则表示它可能是在请求本地文件, 正常返回即可
if (request.url.indexOf('file://') === 0 && (xhr.status < 200 || xhr.status > 599)) {
options.status = 200;
}
// 请求是属于宏任务, 因此这里包装一层成宏任务, 下面同理
setTimeout(() => {
resolve(new Response(body, options))
}, 0)
}
// 当请求出现错误时
xhr.onerror = function () {
setTimeout(function() {
reject(new TypeError('Network request failed'))
}, 0)
}
// 当请求出现超时时
xhr.ontimeout = function () {
setTimeout(function() {
reject(new TypeError('Network request timed out'))
}, 0)
}
// 当请求被手动或在内部终止时
xhr.onabort = function () {
setTimeout(function() {
reject(new DOMException('Aborted', 'AbortError'))
}, 0)
}
function abortXhr() {
xhr.abort()
}
function fixUrl(url: string): string {
try {
return url === '' && window.location.href ? window.location.href : url
} catch (e) {
return url
}
}
// 打开一个请求
xhr.open(request.method, fixUrl(request.url), true)
// 它的作用是决定是否允许跨域
if (request.credentials === 'include') {
xhr.withCredentials = true
} else if (request.credentials === 'omit') {
xhr.withCredentials = false
}
// 可能返回的是并不是字符串,而是二进制数据
if ('responseType' in xhr) {
// 如果是可读的二进制数据则设置
if ('FileReader' in window && 'Blob' in window) {
xhr.responseType = 'blob'
} else if('ArrayBuffer' in window) {
xhr.responseType = 'arraybuffer'
}
}
// 如果传入了请求头则设置
if (init && ({}).toString.call(init.headers) === '[object Object]') {
const names: string[] = [];
Object.getOwnPropertyNames(init.headers).forEach(function(name) {
names.push(name)
xhr.setRequestHeader(name, init.headers![name])
})
request.headers.forEach(function(value, name) {
if (names.indexOf(name) === -1) {
xhr.setRequestHeader(name, value)
}
})
} else if (Array.isArray(init?.headers)) {
// 设置数组格式的请求头
request.headers.forEach(function(value, name) {
xhr.setRequestHeader(name, value)
})
}
// 如果设置了 signal 则监听 abort 事件来终止请求
if (request.signal) {
request.signal.addEventListener('abort', abortXhr)
xhr.onreadystatechange = function() {
// DONE (success or failure)
if (xhr.readyState === 4) {
// 如果响应体正常结束则移除事件监听
request.signal.removeEventListener('abort', abortXhr)
}
}
}
// 发送请求
xhr.send(init!.body as XMLHttpRequestBodyInit)
})
}
这样我们就得到了一个可用的自定义 fetch 方法了,不过这里面还缺失了一部分功能,比如:redirect 、cache 和 keepalive 功能,但这并不影响我们对其原理实现的学习,在多数情况下,我们只需要了解其核心功能到底做了什么即可,如果要细究,那需要参照 W3C 的规范文档及搭配引擎的内部实现来学习才能达到最好的效果。
以上。