<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
使用 fetch
方法來上傳檔案相當容易。
連線斷開後如何恢復上傳?這裡沒有對此的內建選項,但是我們有實現它的一些方式。
對於大檔案(如果我們可能需要恢復),可恢復的上傳應該帶有上傳進度提示。由於 fetch
不允許跟蹤上傳進度,我們將會使用 XMLHttpRequest。
要恢復上傳,我們需要知道在連線斷開前已經上傳了多少。
我們有 xhr.upload.onprogress
來跟蹤上傳進度。
不幸的是,它不會幫助我們在此處恢復上傳,因為它會在資料 被傳送 時觸發,但是伺服器是否接收到了?瀏覽器並不知道。
或許它是由本地網路代理緩衝的(buffered),或者可能是遠端伺服器程序剛剛終止而無法處理它們,亦或是它在中間丟失了,並沒有到達伺服器。
這就是為什麼此事件僅適用於顯示一個好看的進度條。
要恢復上傳,我們需要 確切地 知道伺服器接收的位元組數。而且只有伺服器能告訴我們,因此,我們將發出一個額外的請求。
首先,建立一個檔案 id,以唯一地標識我們要上傳的檔案:
let fileId = file.name + '-' + file.size + '-' + file.lastModified;
在恢復上傳時需要用到它,以告訴伺服器我們要恢復的內容。
如果名稱,或大小,或最後一次修改時間發生了更改,則將有另一個 fileId
。
向伺服器傳送一個請求,詢問它已經有了多少位元組,像這樣:
let response = await fetch('status', { headers: { 'X-File-Id': fileId } }); // 伺服器已有的位元組數 let startByte = +await response.text();
這假設伺服器通過 X-File-Id
header 跟蹤檔案上傳。應該在伺服器端實現。
如果伺服器上尚不存在該檔案,則伺服器響應應為 0
。
然後,我們可以使用 Blob
和 slice
方法來傳送從 startByte
開始的檔案:
xhr.open("POST", "upload", true); // 檔案 id,以便伺服器知道我們要恢復的是哪個檔案 xhr.setRequestHeader('X-File-Id', fileId); // 傳送我們要從哪個位元組開始恢復,因此伺服器知道我們正在恢復 xhr.setRequestHeader('X-Start-Byte', startByte); xhr.upload.onprogress = (e) => { console.log(`Uploaded ${startByte + e.loaded} of ${startByte + e.total}`); }; // 檔案可以是來自 input.files[0],或者另一個源 xhr.send(file.slice(startByte));
這裡我們將檔案 id 作為 X-File-Id
傳送給伺服器,所以伺服器知道我們正在上傳哪個檔案,並且,我們還將起始位元組作為 X-Start-Byte
傳送給伺服器,所以伺服器知道我們不是重新上傳它,而是恢復其上傳。
伺服器應該檢查其記錄,如果有一個上傳的該檔案,並且當前已上傳的檔案大小恰好是 X-Start-Byte
,那麼就將資料附加到該檔案。
這是用 Node.js 寫的包含使用者端和伺服器端程式碼的範例。
在本網站上,它只有部分能工作,因為 Node.js 位於另一個服務 Nginx 後面,該伺服器緩衝(buffer)上傳的內容,當完全上傳後才將其傳遞給 Node.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'; // 可以改用真實路徑,例如 // let filePath = path.join('/tmp', fileId); debug("onUpload fileId: ", fileId); // 初始化一個新上傳 if (!uploads[fileId]) uploads[fileId] = {}; let upload = uploads[fileId]; debug("bytesReceived:" + upload.bytesReceived + " startByte:" + startByte) let fileStream; // 如果 startByte 為 0 或者沒設定,建立一個新檔案,否則檢查大小並附加到現有的大小 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; } // 附加到現有檔案 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 傳送到檔案 req.pipe(fileStream); // 當請求完成,並且其所有資料都以寫入完成 fileStream.on('close', function() { if (upload.bytesReceived == req.headers['x-file-size']) { debug("Upload finished"); delete uploads[fileId]; // 可以在這裡對上傳的檔案進行其他操作 res.end("Success " + upload.bytesReceived); } else { // 連線斷開,我們將未完成的檔案保留在周圍 debug("File unfinished, stopped at " + upload.bytesReceived); res.end(); } }); // 如果發生 I/O error —— 完成請求 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; }
class Uploader { constructor({file, onProgress}) { this.file = file; this.onProgress = onProgress; // 建立唯一標識檔案的 fileId // 我們還可以新增使用者對談識別符號(如果有的話),以使其更具唯一性 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); // 傳送檔案 id,以便伺服器知道要恢復哪個檔案 xhr.setRequestHeader('X-File-Id', this.fileId); // 傳送我們要從哪個位元組開始恢復,因此伺服器知道我們正在恢復 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 —— 如果被中止 // 出現 error 時將其丟擲 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() 被呼叫時觸發 xhr.onabort = () => resolve(false); }); } stop() { if (this.xhr) { this.xhr.abort(); } } }
<!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>
結果
正如我們所看到的,現代網路方法在功能上已經與檔案管理器非常接近 —— 控制 header,進度指示,傳送檔案片段等。
我們可以實現可恢復的上傳等。
以上就是JS實現可恢復的檔案上傳範例詳解的詳細內容,更多關於JS可恢復檔案上傳的資料請關注it145.com其它相關文章!
相關文章
<em>Mac</em>Book项目 2009年学校开始实施<em>Mac</em>Book项目,所有师生配备一本<em>Mac</em>Book,并同步更新了校园无线网络。学校每周进行电脑技术更新,每月发送技术支持资料,极大改变了教学及学习方式。因此2011
2021-06-01 09:32:01
综合看Anker超能充系列的性价比很高,并且与不仅和iPhone12/苹果<em>Mac</em>Book很配,而且适合多设备充电需求的日常使用或差旅场景,不管是安卓还是Switch同样也能用得上它,希望这次分享能给准备购入充电器的小伙伴们有所
2021-06-01 09:31:42
除了L4WUDU与吴亦凡已经多次共事,成为了明面上的厂牌成员,吴亦凡还曾带领20XXCLUB全队参加2020年的一场音乐节,这也是20XXCLUB首次全员合照,王嗣尧Turbo、陈彦希Regi、<em>Mac</em> Ova Seas、林渝植等人全部出场。然而让
2021-06-01 09:31:34
目前应用IPFS的机构:1 谷歌<em>浏览器</em>支持IPFS分布式协议 2 万维网 (历史档案博物馆)数据库 3 火狐<em>浏览器</em>支持 IPFS分布式协议 4 EOS 等数字货币数据存储 5 美国国会图书馆,历史资料永久保存在 IPFS 6 加
2021-06-01 09:31:24
开拓者的车机是兼容苹果和<em>安卓</em>,虽然我不怎么用,但确实兼顾了我家人的很多需求:副驾的门板还配有解锁开关,有的时候老婆开车,下车的时候偶尔会忘记解锁,我在副驾驶可以自己开门:第二排设计很好,不仅配置了一个很大的
2021-06-01 09:30:48
不仅是<em>安卓</em>手机,苹果手机的降价力度也是前所未有了,iPhone12也“跳水价”了,发布价是6799元,如今已经跌至5308元,降价幅度超过1400元,最新定价确认了。iPhone12是苹果首款5G手机,同时也是全球首款5nm芯片的智能机,它
2021-06-01 09:30:45