1、上传和下载进度监控
- 需求
有些时候,当我们上传文件或者是请求一个大体积数据的时候,希望知道实时的进度,甚至可以基于此做一个进度条提示。
我们希望给axios的请求配置提供onDownloadProgress和onUploadProgress函数属性,用户可以通过这2个函数实现对上传进度和下载进度的监控。
axios.get('/more/get',{
onDownloadProgress(progressEvent) {
// 监听下载进度
}
})
axios.post('/more/post',{
onUploadProgress(progressEvent) {
// 监听上传进度
}
})
xhr对象提供了一个progress事件,我们可以监听此事件对数据的下载进度做监控;另外,xhr.upload对象也提供了progress事件,哦我们呢可以基于此对上传事件做监控。
- 代码实现
首先修改接口定义:
interface AxiosRequestConfig {
// ...
onDownloadProgress?: (e: ProgressEvent) => void
onUploadProgress?: (e: ProgressEvent) => void
}
接着,在发送请求前,给xhr对象添加属性
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise((resolve, reject) => {
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的方法:
function isFormData(val: any): boolean {
return typeof val !== 'undefined' && val instanceof FormData
}
然后添加相关逻辑
xhr.ts
if (isFormData(data)) {
delete headers['Content-Type']
}
我们发现,xhr函数内部随着需求越来越多,代码也越来越臃肿,我们可以把逻辑梳理一下,在代码内部做一层封装优化:
import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from "../types";
import { parseHeaders } from "../helpers/headers";
import { transformResponse } from "../helpers/data";
import { createError } from "../helpers/error";
import transform from './transform'
import { isURLSameOrigin } from "../helpers/url";
import cookie from "../helpers/cookie";
import { isFormData } from "../helpers/util";
export default function xhr(config: AxiosRequestConfig): AxiosPromise {
return new Promise((resolve, reject) => {
// method和data可能没有, 所以给出默认值
const {
url,
method = 'get',
data = null,
headers,
responseType,
timeout,
cancelToken,
withCredentials,
xsrfCookieName,
xsrfHeaderName,
onDownloadProgress,
onUploadProgress
} = config
const request = new XMLHttpRequest()
request.open(method.toUpperCase(), url!)
configureRequest()
addEvents()
processHeaders()
processCancel()
request.send(data)
function configureRequest(): void {
if (responseType) {
request.responseType = responseType
}
if (timeout) {
request.timeout = timeout
}
if (withCredentials) {
request.withCredentials = withCredentials
}
}
function addEvents(): void {
if (onDownloadProgress) {
request.onprogress = onDownloadProgress
}
if (onUploadProgress) {
request.upload.onprogress = onUploadProgress
}
request.onerror = function handleError() {
reject(createError('Network Error', config, null, request))
}
request.onreadystatechange = function handleLoad() {
// request.readyState不是4,说明请求还未返回
if (request.readyState !== 4) {
return
}
// 在请求未返回的时候,status为0,
// 当XMLHttpRequest出错的时候,status也为0
// 所以status为0的时候,不做处理
if (request.status === 0) {
return
}
const responseHeaders = parseHeaders(request.getAllResponseHeaders())
const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
const response: AxiosResponse = {
data: transform(responseData, responseHeaders, config.transformResponse),
headers: responseHeaders,
status: request.status,
statusText: request.statusText,
config,
request
}
handleResponse(response)
}
request.ontimeout = function handleTimeout() {
reject(createError(`Timeout of ${timeout}ms exceeded`, config, 'ECONNABORTED', request))
}
}
function processHeaders(): void {
if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName) {
const xsrfValue = cookie.read(xsrfCookieName)
headers[xsrfHeaderName!] = xsrfValue
}
if (isFormData(data)) {
delete headers['Content-Type']
}
Object.keys(headers).forEach((name) => {
request.setRequestHeader(name, headers[name])
})
}
function processCancel(): void {
if (cancelToken) {
cancelToken.promise.then(reason => {
request.abort()
reject(reason)
})
}
}
function handleResponse(response: AxiosResponse) {
if (response.status >= 200 && response.status < 300) {
resolve(response)
} else {
reject(createError(`Request failed width status code ${response.status}`, config, null, request, response))
}
}
})
}
可以看到,整个流程分为7步:
- 创建一个request实例
- 执行request.open方法初始化
- 执行configureRequest配置request对象
- 执行addEvents给request添加事件处理函数
- 执行processHeaders处理请求headers
- 执行processCancel处理请求取消逻辑
- 执行request.send方法发送请求
这样分割可以使代码逻辑更清晰。
demo
// server
var multipart = require('connect-multiparty');
const path = require('path')
var multipartMiddleware = multipart({
uploadDir: path.resolve(__dirname, '../upload-file')
});
router.post('/upload', multipartMiddleware, (req, res) => {
console.log(req.body, req.files)
res.send('upload success')
})
// client
import axios, { Canceler } from '../../src/index'
const instance = axios.create()
const url = 'http://p1.meituan.net/dpmerchantimage/32874e71-23df-4bd9-84a3-4530eb582848.jpg%40740w_2048h_0e_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20'
const downloadEl = document.getElementById('download')
downloadEl.addEventListener('click', e => {
instance.get(url, {
onDownloadProgress: (e) => {
console.log('onDownloadProgress', e)
}
})
})
const uploadEl = document.getElementById('upload')
uploadEl.addEventListener('click', (e) => {
const data = new FormData()
const fileEl = document.getElementById('file') as HTMLInputElement
if (fileEl.files) {
console.log(fileEl.files)
data.append('file', fileEl.files[0])
instance.post('/api/upload', data, {
onUploadProgress: (e) => {
console.log('onUploadProgress', e)
}
}).then(res => {
console.log(res)
})
}
})
//html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>demo2</title>
</head>
<body>
<a>demo2</a>
<button type="button" id="download">download</button>
<form>
<input type="file" id="file"/>
<button type="button" id="upload">upload</button>
</form>
<script src="/dist/demo2.js"></script>
</body>
</html>
这样,我们可以在上传和下载的时候,监听到进度变化。
网友评论