xhr文件上传小记

带着文笔差工科男的特质,已经要求自己写了几篇东西了。也希望自己能够经常习惯性的把遇到的问题记录下来,养成积累的习惯。今天在测试文件上传逻辑时经常会发现重复上传文件时总会出现进度错乱的问题,在此进行记录并将代码好好梳理下。

使用XHR上传文件

xhr,也就是所谓的XMLHttpRequest对象,用于与后台交换数据,我们可以通过原生方式进行文件上传,数据请求等操作,包括我们常见的jquery ajax等。xhr分为level1和level2两套标准,在此我们主要对Level2进行介绍。

1.使用方式

首先对使用方式进行简单介绍。废话不多说,直接上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
//建立xhr对象
var xhr = new XMLHttpRequest();
//建立连接,参数
xhr.open(type, action, isAsync);
//注册事件监听
//1.onload回调
xhr.onload = function() {...};
//2.错误回调
xhr.onerror = function() {...};
//3.设置超时
xhr.timeout = 3000; //设置超时时间
xhr.ontimeout = function() {...}; //注册超时监听
//4.上传进度监听
xhr.upload.onprogress = function() { ... };
//发送数据
xhr.send(file);
//终端请求
xhr.abort();

2.遇到的问题

在对文件进行上传时,我将文件上传函数进行了基本的封装,像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
export const uploadeFile = function({
url,
file,
isAsync = true,
onSuccess,
onError,
timeout = 3000,
onTimeout,
}) {
var xhr = new XMLHttpRequest();
xhr.open()
xhr.onload = function() {...}
xhr.send(file)
...略
}

在业务中有这样一个需求,就是每当选择新的文件进行上传时,就要把上一个上传进程干掉,再进行新的上传进程。因此我在开始上传文件时,设置了一个isUploading开关,来判断是否正在进行上传进程的判断,并将xhr abort掉。而此时我需要获取到xhr对象,因此我把添加了这么一句话,尝试将xhr返回:

1
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
//----- 封装的上传文件函数 -----
export const uploadeFile = function(cfg){
var xhr = new XMLHttpRequest();
//尝试将xhr返回,不要这么用
return xhr;
}
//------ 业务代码 ------
let isUploading = false
xhr = null;
//判断若正在上传,则abort;
if (isUploading) {
xhr.abort();
}
//上传文件,接收xhr对象
xhr = uploadeFile({
...
onSuccess: function(){
//关闭isUploading
isUploading = false;
}
})
isUploading = true;

整个逻辑看起来还OK,没什么问题。但是在测试过程中发现abort并没有起作用???为什么?!!调试了下发现,返回回来的xhr为undefined。 具体原因进行了相关查询,猜想大概是因为建立了对象但没有open成功等等。 现在先做个TODO,后面会详细进行讨论。

好,让我们来冷静一下看之前的代码。我们会发现,在每次进行文件上传文件时,都需要重新建立xhr对象,并且在想要abort时,需要引用内部对象而并非暴露接口进行abort,这样做显然是不好的。因此,我们可以重新对这些代码进行梳理,使用面向对象的方式去解决这类问题。

最终代码:

使用面向对象方式进行统一梳理,最终代码如下:

1
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
export class xhr4File {
constructor() {
const that = this;
this.xhr = new XMLHttpRequest();
this.promise = {
progress: function (onProgress) {
that.bindProgress(onProgress);
},
done: function (onSuccess) {
that.bindLoad(onSuccess);
},
error: function (onError) {
that.bindError(onProgress);
},
timeout: function (ontimeout, time = 10000) {
that.bindTimeOut(time, ontimeout);
}
}
}
send ({
url,
async = true,
file,
onProgress,
onSuccess,
onError,
time = 10000,
onTimeout
}) {
const that = this;
this.xhr.open('post', url, async);
this.bindProgress(onProgress)
.done(onSuccess)
.error(onError)
.timeout(time, onTimeout);
this.xhr.send(file);
return this.promise;
}
//绑定进度提醒事件
bindProgress(onProgress) {
if(onProgress) {
this.xhr.upload.onprogress = function(e) {
if (e.lengthComputable) {
var p = parseInt( e.loaded / e.total * 100 );
onProgress(p);
}
};
}
return this.promise;
}
//绑定发布成功
bindLoad (onSuccess) {
this.xhr.onload = function (r) {
let json = {};
if (this.readyState == 4 && this.status == 200) {
try {
json = JSON.parse(xhr.responseText);
} catch (e) {
json = { 'code': 500, message: 'json解析失败' };
}
sendMsg('success');
sendMsg(onSuccess)
onSuccess(json);
}
};
return this.promise;
}
//绑定失败回调
bindError (onError) {
if( onError ) {
this.xhr.onerror = function(e) {
onError(e);
};
}
return this.promise;
}
bindTimeOut (time, onTimeout) {
const xhr = this.xhr;
if(onTimeout) {
xhr.timeout = time;
xhr.ontimeout = function(e) {
onTimeout(e);
};
}
return this.promise;
}
}