過去要以 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>