过去要以XMLHttpRequest
来上传文件,并没有一个标准作法,各家浏览器各出奇招,现在若能使用 XMLHttpRequest Level 1 的FormData
,XMLHttpRequest
可以轻松地以标准方式进行文件上传。
FormData
可以用来收集表单信息,如果有个form
代表着<form>
标签的 DOM,可以直接作为FormData
构造之用:
let formData = new FormData(form);
或者是构造FormData
实例之后,自行加入想要的表单内容:
let formData = new FormData();
formData.append('username', 'Justin');
formData.append('password', '123456');
使用XMLHttpRequest
来进行POST
,调用send
方法时,可以将FormData
实例当成实参传入,这时请求的Content-Type
一定是multipart/form-data
,无需也不能自行设置请求标头Content-Type
。
如果只是使用FormData
作为一种表单序列化时的简便 API,伺服端必须能处理multipart/form-data
内容,而不是单纯透过请求参数的 API 来获取相关请求参数。
如果表单中有type = "file"
的input
标签,当表单 DOM 对象被当成FormData
构造时的实参,可以直接进行文件上传,如果是使用append
方法,加入type = "file"
的input
标签选取之文件,例如只选取一个文件的情况,可以如下编写:
let photo = document.getElementById('photo');
let formData = new FormData();
formData.append('photo', photo.files[0]);
下面这个范例是个简单的文件上传(可搭配〈getPart()、getParts()〉中的 Servlet):
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
</head>
<body>
<form id="f" action="upload" method="post" enctype="multipart/form-data">
Photo :<input type="file" name="photo"/><br>
<input id="upload" type="submit"/>
</form>
<span id="message"></span>
<script type="text/javascript">
// 对 XMLHttpRequest 做简单封装
class XHR {
constructor() {
let xhr = new XMLHttpRequest();
let handlers = {
'readystatechange' : new Set(),
'load' : new Set()
};
xhr.onreadystatechange = function(evt) {
handlers['readystatechange']
.forEach(handler => handler.call(xhr, evt));
};
xhr.onload = function(evt) {
handlers['load']
.forEach(handler => handler.call(xhr, evt));
};
this.xhr = xhr;
this.handlers = handlers;
}
addEvt(evtType, handler) {
this.handlers[evtType].add(handler);
return this;
}
removeEvt(evtType, handler) {
this.handlers[evtType].delete(handler);
return this;
}
open(method, url, paraObj, async = true, username = null, password = null) {
let openUrl = paraObj ? `${url}?${params(paraObj)}` : url;
this.xhr.open(method, openUrl, async, username, password);
return this;
}
addHeaders(headers) {
Object.keys(headers)
.forEach(name => this.xhr.setRequestHeader(name, headers[name]));
return this;
}
send(body = null) {
this.xhr.send(body);
return this;
}
}
document.getElementById('upload').onclick = function(evt) {
let formData = new FormData(document.getElementById('f'));
let xhr = new XHR();
xhr.addEvt('load', evt => {
let req = evt.target;
if(req.status === 200) {
document.getElementById('message').innerHTML = 'File Uploaded';
}
})
.open('POST', 'upload')
.send(formData);
evt.preventDefault();
};
</script>
</body>
</html>
如果想要实现文件上传进度,要使用的不是XMLHttpRequest
的onprogress
,而是使用XMLHttpRequestUpload
的onprogress
,前者是有关于响应的进度,后者是才是有关于上传的进度,每个XMLHttpRequest
实例都会关联着一个XMLHttpRequestUpload
实例,可透过XMLHttpRequest
实例的upload
来获取,因此,想要实现上传进度,基本上可以如下:
xhr.upload.onprogress = function(evt) {
console.log(evt.lengthComputable);
console.log(evt.loaded);
console.log(evt.total);
};
在标准规范中,XMLHttpRequest
与XMLHttpRequestUpload
都继承了XMLHttpRequestEventTarget
接口,XMLHttpRequestEventTarget
主要是规范onloadstart
、onprogress
、onabort
、onerror
、onload
、ontimeout
、onloadend
这些事件处理器,而XMLHttpRequestUpload
单纯继承XMLHttpRequestEventTarget
,什么也没新增,XMLHttpRequest
则是在继承之后,增加了onreadystatechange
、open
、send
等。
底下的范例也针对XMLHttpRequestUpload
做了封装:
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width">
</head>
<body>
<form id="f" action="upload" method="post" enctype="multipart/form-data">
Photo :<input type="file" name="photo"/><br>
<input id="upload" type="submit"/>
</form>
<span id="message"></span>
<script type="text/javascript">
class XHREventTarget {
constructor(xhr) {
let evtTypes = ['loadstart', 'progress', 'abort', 'error', 'load', 'time', 'loadend'];
let handlers = evtTypes.reduce((handlers, evtType) => {
handlers[evtType] = new Set();
return handlers;
}, {});
evtTypes.forEach(evtType => {
xhr[`on${evtType}`] = function(evt) {
handlers[evtType].forEach(handler => handler.call(xhr, evt));
};
});
this.xhr = xhr;
this.handlers = handlers;
}
addEvt(evtType, handler) {
this.handlers[evtType].add(handler);
return this;
}
removeEvt(evtType, handler) {
this.handlers[evtType].delete(handler);
return this;
}
}
class XHRUpload extends XHREventTarget {
constructor(xhr) {
super(xhr);
}
}
// 对 XMLHttpRequest 做简单封装
class XHR extends XHREventTarget {
constructor() {
super(new XMLHttpRequest());
let xhr = this.xhr;
let handlers = this.handlers;
handlers['readystatechange'] = new Set();
xhr.onreadystatechange = function(evt) {
handlers['readystatechange']
.forEach(handler => handler.call(xhr, evt));
};
}
open(method, url, paraObj, async = true, username = null, password = null) {
let openUrl = paraObj ? `${url}?${params(paraObj)}` : url;
this.xhr.open(method, openUrl, async, username, password);
return this;
}
addHeaders(headers) {
Object.keys(headers)
.forEach(name => this.xhr.setRequestHeader(name, headers[name]));
return this;
}
send(body = null) {
this.xhr.send(body);
return this;
}
uploadXHR() {
if(this.upload === undefined) {
this.upload = new XHRUpload(this.xhr.upload);
}
return this.upload;
}
}
document.getElementById('upload').onclick = function(evt) {
let formData = new FormData(document.getElementById('f'));
let xhr = new XHR();
xhr.uploadXHR().addEvt('progress', evt => {
console.log(evt.lengthComputable);
console.log(evt.loaded);
console.log(evt.total);
});
xhr.addEvt('load', evt => {
let req = evt.target;
if(req.status === 200) {
document.getElementById('message').innerHTML = 'File Uploaded';
}
})
.open('POST', 'upload')
.send(formData);
evt.preventDefault();
};
</script>
</body>
</html>