🌗 实现上传和下载进度监控
# 需求分析
当需要上传文件或者请求一个大体积数据时,希望知道实时的进度,或者一个进度条展示。
可以给 axios
的请求配置提供一个 onDownloadProgress
和 onUploadProgress
两个函数属性,用户通过这两个函数实现对下载进度和上传进度的监控。
例如:
axios.get('/more/get',{ onDownloadProgress(progressEvent) { // 监听下载进度 } }) axios.post('/more/post',{ onUploadProgress(progressEvent) { // 监听上传进度 } })
1
2
3
4
5
6
7
8
9
10
11
xhr
对象提供了一个 progress
事件,可以通过监听这个事件对数据的下载进度做监控。另外, xhr.upload
对象也提供了 progress
事件,可以基于此对上传进度做监控。
# 代码实现
修改 AxiosRequestConfig
的类型定义, src/types/index.ts
:
export interface AxiosRequestConfig {
// ...
onDownloadProgress?: (e: ProgressEvent) => void
onUploadProgress?: (e: ProgressEvent) => void
}
2
3
4
5
然后修改 src/core/xhr.ts
,在发送请求之前,给 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.onprogress = onUploadProgress
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
另外,如果请求的数据是 FormData
类型,应该主动删除请求 headers
中的 Content-Type
字段,让浏览器自动根据请求数据设置 Content-Type
。比如当通过 FormData
上传文件的时候,浏览器会把请求 headers
中的 Content-Type
设置为 multipart/form-data
。
先添加一个判断 FormData
的方法,在 src/helpers/util.ts
:
export function isFormData(val: any): boolean {
return typeof val !== 'undefined' && val instanceof FormData
}
2
3
然后再添加逻辑到 src/core/xhr.ts
:
if(isFormData(data)) {
delete headers['Content-Type']
}
2
3
xhr.ts
随着需求越来越多,代码越来越臃肿,可以把逻辑梳理一下,把内部代码做一层封装优化。
点击查看
import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from '../types'
import { parseHeaders } from '../helpers/headers'
import { createError } from '../helpers/error'
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) => {
const {
data = null,
url,
method = 'get',
headers,
responseType,
timeout,
cancelToken,
withCredentials,
xsrfCookieName,
xsrfHeaderName,
onDownloadProgress,
onUploadProgress
} = config
const request = new XMLHttpRequest()
request.open(method.toUpperCase(), url, true)
configureRequest()
addEvents()
processHeaders()
processCancel()
request.send(data)
function configureRequest(): void {
if (responseType) {
request.responseType = responseType
}
if (timeout) {
request.timeout = timeout
}
if (withCredentials) {
request.withCredentials = true
}
}
function addEvents(): void {
request.onreadystatechange = function handleLoad() {
if (request.readyState !== 4) return
if (request.status === 0) return
const responseHeaders = parseHeaders(request.getAllResponseHeaders())
const responseData =
responseType && responseType !== 'text' ? request.response : request.responseText
const response: AxiosResponse = {
data: responseData,
status: request.status,
statusText: request.statusText,
headers: responseHeaders,
config,
request
}
handleResponse(response)
}
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))
}
if (onDownloadProgress) {
request.onprogress = onDownloadProgress
}
if (onUploadProgress) {
request.onprogress = onUploadProgress
}
}
function processHeaders(): void {
if (isFormData(data)) {
delete headers['Content-Type']
}
if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName) {
const xsrfValue = cookie.read(xsrfCookieName)
if (xsrfValue) headers[xsrfHeaderName!] = xsrfValue
}
Object.keys(headers).forEach(name => {
if (data === null && name.toLowerCase() === 'content-type') {
delete headers[name]
} else {
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 with status code ${response.status}`,
config,
null,
request,
response
)
)
}
}
})
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
把整个流程分为 7 部:
- 创建一个
request
实例。- 执行
request.open
方法初始化。- 执行
configureRequest
配置request
对象。- 执行
addEvents
给request
添加事件处理函数。- 执行
processHeaders
处理请求headers
。- 执行
processCancel
处理请求取消逻辑。- 执行
request.send
方法发送请求。
# 编写测试 DEMO
examples/more/index.html
:
<!DOCTYPE html>
<html lang='en'>
<head>
<meta charset='UTF-8'>
<title>more example</title>
<link rel='stylesheet' type='text/css'
href='//maxcdn.bootstrapcdn.com/bootstrap/3.2.0/css/bootstrap.min.css' />
</head>
<body>
<h1>file download</h1>
<div>
<button id='download' class='btn btn-primary'>download</button>
</div>
<h1>file upload</h1>
<div>
<form role='form' class='form' onsubmit='return false'>
<input type='file' id='title' class='form-control'>
<button id='upload' class='btn btn-primary'>upload</button>
</form>
</div>
<script src='/__build__/more.js'></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
添加下载按钮和上传表单。
examples/more/app.ts
,为了展示上传和下载的进度,引入 nprogress
,在页面的顶部展示进度条:
const instance = axios.create()
function calculatePercentage(loaded: number, total: number) {
return Math.floor(loaded * 1.0) / total
}
function loadProgressbar() {
const setupStartProgress = () => {
instance.interceptors.request.use(config => {
NProgress.start()
return config
})
}
const setupUpdateProgress = () => {
const update = (e: ProgressEvent) => {
console.log(e)
NProgress.set(calculatePercentage(e.loaded, e.total))
}
instance.defaults.onDownloadProgress = update
instance.defaults.onUploadProgress = update
}
const setupStopProgress = () => {
instance.interceptors.response.use(response => {
NProgress.done()
return response
}, error => {
NProgress.done()
return Promise.reject(error)
})
}
setupStartProgress()
setupUpdateProgress()
setupStopProgress()
}
loadProgressbar()
const downloadEl = document.getElementById('download')
downloadEl!.addEventListener('click', e => {
instance.get('https://cdn.jsdelivr.net/gh/simon1uo/image-flow@master/image/jSQm90.jpeg')
})
const uploadEl = document.getElementById('upload')
uploadEl!.addEventListener('click', e => {
const data = new FormData()
const fileEl = document.getElementById('file') as HTMLInputElement
if(fileEl.files) {
data.append('file', fileEl.files[0])
instance.post('/more/upload', data)
}
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
对于
progress
事件参数e
,会有e.total
和e.loaded
属性,表示进程总体的工作量和已经执行的工作量,可以根据这 2 个值算出当前进度,然后通过Nprogess.set
设置。另外,通过配置请求拦截器和响应拦截器执行NProgress.start()
和NProgress.done()
。给下载按钮绑定了一个
click
事件,请求一张图片,可以看到实时的进度;另外也给上传按钮绑定了一个click
事件,上传选择的文件,同样也能看到实时进度。
为了处理上传请求,需要下载安装一个 express
的中间件 connect-multiparty
,然后使用它。
examples/server.js
:
const multipart = require('connect-multiparty')
app.use(multipart({
uploadDir: path.resolve(__dirname, 'upload-file')
}))
router.post('more/upload', function(req, res) {
console.log(req.body, req.files)
res.end('upload success!')
})
2
3
4
5
6
7
8
9
需要在
examples
目录下创建一个文件夹upload-file
作为上传文件的存储目录。通过这个中间件,就可以处理上传请求并且可以把上传的文件存储在upload-file
目录下。为了保证代码正常运行,我们还需要在
examples/webpack.config.js
中添加css-loader
和css-loader
,不要忘记先安装它们。{ test: /\.css?$/, use: [ { loader: ['style-loader', 'css-loader'] } ] }
1
2
3
4
5
6
7
8