前言 我们在上传文件的时候,如果文件内容较小,我们可以直接采用将文件转化为字节流传输到服务端。但是如果遇到大文件,这样的方式是非常折磨用户的,而且万一中途传输中断,又要重新从 0
开始。
所以我们需要采用分片上传/断点续传来优化用户的使用体验。
断点续传 断点续传
就是将跳过上一次上传的文件内容,采用 Blob.slice
方法分割文件内容,直接上传剩余的字节数。
监听进度 如果要实现断点续传,我们就需要知道我们之前上传了多少进度。我们采用 XMLHttpRequest
来进行上传,因为 fetch
是无法监听 progress
事件的(也可能是怪我菜)。
我们采用 xhr.upload.onprogress
来实现进度监听。要实现断点续传,我们就需要知道服务端接收的字节数。所以除了上传请求,我们还需要增加一个请求来询问服务器上传了多少字节。
实现思路 首先我们先创建一个唯一的标识 id
来标识我们要上传的文件。1 let fileId = file.name + '-' + file.size + '-' + file.lastModified;
我们这里简单判定一下,当文件名,大小,最近修改日期发生更改的话,断点续传的时候就会判定这个文件就是一个新的文件,重新生成一个新的 fileId
。也就是说不会进行续传。
向服务端发送一个请求,请求服务端已经接收了多少个字节。1 2 3 4 5 6 7 8 let response = await fetch('status' , { headers: { 'X-File-Id' : fileId } }); let startByte = +await response.text();
服务器通过获取 X-File-Id
头信息获取我们刚刚设定的 fileId
来处理当前文件,当服务器中没有文件的时候,响应应为 0
。
然后我们采用 Blob.slice
方法发送 startByte
之后的文件。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 xhr.open("POST" , "upload" , true ); 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} ` ); }; xhr.send(file.slice(startByte));
在这里,我们将文件唯一标识 fileId
作为 X-File-Id
发送给服务端,这样它就知道我们要上传的文件,并且将 startByte
作为 X-Start-Byte
告知服务器,我们是最初上传还是在续传,主要取决于 startByte
是否为 0
。
服务端如果发现 startByte
不为 0 ,应该向 field 文件追加字节流。
这里的核心代码引用了一下 GiHub 中的代码。
index.html
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 <!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 >
uploader.js
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 class Uploader { constructor ({file, onProgress}) { this .file = file; this .onProgress = onProgress; 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 ); 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 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)); } }; xhr.onabort = () => resolve(false ); }); } stop() { if (this .xhr) { this .xhr.abort(); } } }
总结 这种断点续传的方式在传输大文件的时候,其实依然有一些缺点,比如当服务器如果有上传大小限制的时候,还是会无法上传。
既然采用了 Blob.slice
方法,干脆直接就对文件进行分块。并为每个文件块添加规则的文件头,然后分别上传。
在全部分块发送完成之后发送一个 merge
请求,让服务端拼接这些文件块。在这里也整理了几个比较好用的上传组件分享一下:
Copyright by @maybelence.