JS實(shí)現(xiàn)可恢復(fù)的文件上傳示例詳解
正文
使用 fetch 方法來上傳文件相當(dāng)容易。
連接斷開后如何恢復(fù)上傳?這里沒有對(duì)此的內(nèi)建選項(xiàng),但是我們有實(shí)現(xiàn)它的一些方式。
對(duì)于大文件(如果我們可能需要恢復(fù)),可恢復(fù)的上傳應(yīng)該帶有上傳進(jìn)度提示。由于 fetch 不允許跟蹤上傳進(jìn)度,我們將會(huì)使用 XMLHttpRequest。
不太實(shí)用的進(jìn)度事件
要恢復(fù)上傳,我們需要知道在連接斷開前已經(jīng)上傳了多少。
我們有 xhr.upload.onprogress 來跟蹤上傳進(jìn)度。
不幸的是,它不會(huì)幫助我們?cè)诖颂幓謴?fù)上傳,因?yàn)樗鼤?huì)在數(shù)據(jù) 被發(fā)送 時(shí)觸發(fā),但是服務(wù)器是否接收到了?瀏覽器并不知道。
或許它是由本地網(wǎng)絡(luò)代理緩沖的(buffered),或者可能是遠(yuǎn)程服務(wù)器進(jìn)程剛剛終止而無法處理它們,亦或是它在中間丟失了,并沒有到達(dá)服務(wù)器。
這就是為什么此事件僅適用于顯示一個(gè)好看的進(jìn)度條。
要恢復(fù)上傳,我們需要 確切地 知道服務(wù)器接收的字節(jié)數(shù)。而且只有服務(wù)器能告訴我們,因此,我們將發(fā)出一個(gè)額外的請(qǐng)求。
算法
首先,創(chuàng)建一個(gè)文件 id,以唯一地標(biāo)識(shí)我們要上傳的文件:
let fileId = file.name + '-' + file.size + '-' + file.lastModified;
在恢復(fù)上傳時(shí)需要用到它,以告訴服務(wù)器我們要恢復(fù)的內(nèi)容。
如果名稱,或大小,或最后一次修改時(shí)間發(fā)生了更改,則將有另一個(gè) fileId。
向服務(wù)器發(fā)送一個(gè)請(qǐng)求,詢問它已經(jīng)有了多少字節(jié),像這樣:
let response = await fetch('status', {
headers: {
'X-File-Id': fileId
}
});
// 服務(wù)器已有的字節(jié)數(shù)
let startByte = +await response.text();
這假設(shè)服務(wù)器通過 X-File-Id header 跟蹤文件上傳。應(yīng)該在服務(wù)端實(shí)現(xiàn)。
如果服務(wù)器上尚不存在該文件,則服務(wù)器響應(yīng)應(yīng)為 0。
然后,我們可以使用 Blob 和 slice 方法來發(fā)送從 startByte 開始的文件:
xhr.open("POST", "upload", true);
// 文件 id,以便服務(wù)器知道我們要恢復(fù)的是哪個(gè)文件
xhr.setRequestHeader('X-File-Id', fileId);
// 發(fā)送我們要從哪個(gè)字節(jié)開始恢復(fù),因此服務(wù)器知道我們正在恢復(fù)
xhr.setRequestHeader('X-Start-Byte', startByte);
xhr.upload.onprogress = (e) => {
console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`);
};
// 文件可以是來自 input.files[0],或者另一個(gè)源
xhr.send(file.slice(startByte));
這里我們將文件 id 作為 X-File-Id 發(fā)送給服務(wù)器,所以服務(wù)器知道我們正在上傳哪個(gè)文件,并且,我們還將起始字節(jié)作為 X-Start-Byte 發(fā)送給服務(wù)器,所以服務(wù)器知道我們不是重新上傳它,而是恢復(fù)其上傳。
服務(wù)器應(yīng)該檢查其記錄,如果有一個(gè)上傳的該文件,并且當(dāng)前已上傳的文件大小恰好是 X-Start-Byte,那么就將數(shù)據(jù)附加到該文件。
這是用 Node.js 寫的包含客戶端和服務(wù)端代碼的示例。
在本網(wǎng)站上,它只有部分能工作,因?yàn)?Node.js 位于另一個(gè)服務(wù) Nginx 后面,該服務(wù)器緩沖(buffer)上傳的內(nèi)容,當(dāng)完全上傳后才將其傳遞給 Node.js。
但是你可以下載這些代碼,在本地運(yùn)行以進(jìn)行完整演示:
server.js
let http = require('http');
let static = require('node-static');
let fileServer = new static.Server('.');
let path = require('path');
let fs = require('fs');
let debug = require('debug')('example:resume-upload');
let uploads = Object.create(null);
function onUpload(req, res) {
let fileId = req.headers['x-file-id'];
let startByte = +req.headers['x-start-byte'];
if (!fileId) {
res.writeHead(400, "No file id");
res.end();
}
// 我們將“無處”保存文件
let filePath = '/dev/null';
// 可以改用真實(shí)路徑,例如
// let filePath = path.join('/tmp', fileId);
debug("onUpload fileId: ", fileId);
// 初始化一個(gè)新上傳
if (!uploads[fileId]) uploads[fileId] = {};
let upload = uploads[fileId];
debug("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte)
let fileStream;
// 如果 startByte 為 0 或者沒設(shè)置,創(chuàng)建一個(gè)新文件,否則檢查大小并附加到現(xiàn)有的大小
if (!startByte) {
upload.bytesReceived = 0;
fileStream = fs.createWriteStream(filePath, {
flags: 'w'
});
debug("New file created: " + filePath);
} else {
// 我們也可以檢查磁盤上的文件大小以確保
if (upload.bytesReceived != startByte) {
res.writeHead(400, "Wrong start byte");
res.end(upload.bytesReceived);
return;
}
// 附加到現(xiàn)有文件
fileStream = fs.createWriteStream(filePath, {
flags: 'a'
});
debug("File reopened: " + filePath);
}
req.on('data', function(data) {
debug("bytes received", upload.bytesReceived);
upload.bytesReceived += data.length;
});
// 將 request body 發(fā)送到文件
req.pipe(fileStream);
// 當(dāng)請(qǐng)求完成,并且其所有數(shù)據(jù)都以寫入完成
fileStream.on('close', function() {
if (upload.bytesReceived == req.headers['x-file-size']) {
debug("Upload finished");
delete uploads[fileId];
// 可以在這里對(duì)上傳的文件進(jìn)行其他操作
res.end("Success " + upload.bytesReceived);
} else {
// 連接斷開,我們將未完成的文件保留在周圍
debug("File unfinished, stopped at " + upload.bytesReceived);
res.end();
}
});
// 如果發(fā)生 I/O error —— 完成請(qǐng)求
fileStream.on('error', function(err) {
debug("fileStream error");
res.writeHead(500, "File error");
res.end();
});
}
function onStatus(req, res) {
let fileId = req.headers['x-file-id'];
let upload = uploads[fileId];
debug("onStatus fileId:", fileId, " upload:", upload);
if (!upload) {
res.end("0")
} else {
res.end(String(upload.bytesReceived));
}
}
function accept(req, res) {
if (req.url == '/status') {
onStatus(req, res);
} else if (req.url == '/upload' && req.method == 'POST') {
onUpload(req, res);
} else {
fileServer.serve(req, res);
}
}
// -----------------------------------
if (!module.parent) {
http.createServer(accept).listen(8080);
console.log('Server listening at port 8080');
} else {
exports.accept = accept;
}uploader.js
class Uploader {
constructor({file, onProgress}) {
this.file = file;
this.onProgress = onProgress;
// 創(chuàng)建唯一標(biāo)識(shí)文件的 fileId
// 我們還可以添加用戶會(huì)話標(biāo)識(shí)符(如果有的話),以使其更具唯一性
this.fileId = file.name + '-' + file.size + '-' + file.lastModified;
}
async getUploadedBytes() {
let response = await fetch('status', {
headers: {
'X-File-Id': this.fileId
}
});
if (response.status != 200) {
throw new Error("Can't get uploaded bytes: " + response.statusText);
}
let text = await response.text();
return +text;
}
async upload() {
this.startByte = await this.getUploadedBytes();
let xhr = this.xhr = new XMLHttpRequest();
xhr.open("POST", "upload", true);
// 發(fā)送文件 id,以便服務(wù)器知道要恢復(fù)哪個(gè)文件
xhr.setRequestHeader('X-File-Id', this.fileId);
// 發(fā)送我們要從哪個(gè)字節(jié)開始恢復(fù),因此服務(wù)器知道我們正在恢復(fù)
xhr.setRequestHeader('X-Start-Byte', this.startByte);
xhr.upload.onprogress = (e) => {
this.onProgress(this.startByte + e.loaded, this.startByte + e.total);
};
console.log("send the file, starting from", this.startByte);
xhr.send(this.file.slice(this.startByte));
// return
// true —— 如果上傳成功,
// false —— 如果被中止
// 出現(xiàn) error 時(shí)將其拋出
return await new Promise((resolve, reject) => {
xhr.onload = xhr.onerror = () => {
console.log("upload end status:" + xhr.status + " text:" + xhr.statusText);
if (xhr.status == 200) {
resolve(true);
} else {
reject(new Error("Upload failed: " + xhr.statusText));
}
};
// onabort 僅在 xhr.abort() 被調(diào)用時(shí)觸發(fā)
xhr.onabort = () => resolve(false);
});
}
stop() {
if (this.xhr) {
this.xhr.abort();
}
}
}index.html
<!DOCTYPE HTML>
<script src="uploader.js"></script>
<form name="upload" method="POST" enctype="multipart/form-data" action="/upload">
<input type="file" name="myfile">
<input type="submit" name="submit" value="Upload (Resumes automatically)">
</form>
<button onclick="uploader.stop()">Stop upload</button>
<div id="log">Progress indication</div>
<script>
function log(html) {
document.getElementById('log').innerHTML = html;
console.log(html);
}
function onProgress(loaded, total) {
log("progress " + loaded + ' / ' + total);
}
let uploader;
document.forms.upload.onsubmit = async function(e) {
e.preventDefault();
let file = this.elements.myfile.files[0];
if (!file) return;
uploader = new Uploader({file, onProgress});
try {
let uploaded = await uploader.upload();
if (uploaded) {
log('success');
} else {
log('stopped');
}
} catch(err) {
console.error(err);
log('error');
}
};
</script>結(jié)果

正如我們所看到的,現(xiàn)代網(wǎng)絡(luò)方法在功能上已經(jīng)與文件管理器非常接近 —— 控制 header,進(jìn)度指示,發(fā)送文件片段等。
我們可以實(shí)現(xiàn)可恢復(fù)的上傳等。
以上就是JS實(shí)現(xiàn)可恢復(fù)的文件上傳示例詳解的詳細(xì)內(nèi)容,更多關(guān)于JS可恢復(fù)文件上傳的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
autojs使用intent發(fā)送郵件帶附件實(shí)現(xiàn)示例
這篇文章主要為大家介紹了autojs使用intent發(fā)送郵件帶附件實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-01-01
微信小程序 教程之wxapp視圖容器 scroll-view
這篇文章主要介紹了微信小程序 教程之wxapp視圖容器 scroll-view的相關(guān)資料,需要的朋友可以參考下2016-10-10
TypeScript與JavaScript對(duì)比及打包工具比較
這篇文章主要為大家介紹了TypeScript與JavaScript對(duì)比及打包工具比較,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-03-03
echart實(shí)現(xiàn)大屏動(dòng)效示例詳解
這篇文章主要為大家介紹了echart實(shí)現(xiàn)大屏動(dòng)效示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
JS前端使用canvas動(dòng)態(tài)繪制函數(shù)曲線示例詳解
這篇文章主要為大家介紹了JS前端使用canvas畫函數(shù)曲線的示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2022-08-08
JavaScript中七種流行的開源機(jī)器學(xué)習(xí)框架
今天小編就為大家分享一篇關(guān)于JavaScript中五種流行的開源機(jī)器學(xué)習(xí)框架,小編覺得內(nèi)容挺不錯(cuò)的,現(xiàn)在分享給大家,具有很好的參考價(jià)值,需要的朋友一起跟隨小編來看看吧2018-10-10
超越Node.js的JavaScript運(yùn)行環(huán)境Bun.js功能特性詳解
這篇文章主要為大家介紹了超越Node.js的JavaScript運(yùn)行環(huán)境Bun.js功能特性詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-09-09

