历史上,JavaScript 无法处理二进制数据。如果一定要处理的话,只能使用 String.prototype.charCodeAt()
方法,逐个地将字节从文字编码转成二进制数据,还有一种办法是将二进制数据转成 Base64 编码,再进行处理。这两种方法不仅速度慢,而且容易出错。因此 ECMAScript 5 引入了 Blob 对象,允许直接操作二进制数据。
File API 使访问包含 File 对象的 FileList 成为可能,FileList 代表被用户选择的文件列表。
如果用户只选择了一个文件,那么只需要考虑 FileList 中的第一个 File 对象。
<!-- 只可以选择单个文件 --><input id="single" type="file" /><!-- 可选择多个文件 --><input id="multipart" /><script type="text/javascript">const input = document.querySelector('#single');// 监听表单输入框的 `change` 事件访问 FileListinput.addEventListener('change',function(e) {handleFiles(e.target.files);},false);// 监听 document 的 `dragover` 和 `drop` 事件通过拖拽选择文件document.addEventListener('dragover',function(e) {e.preventDefault();e.stopPropagation();},false);document.addEventListener('drop',function(e) {e.preventDefault();e.stopPropagation();handleFiles(e.dataTransfer.files);},false);function handleFiles(files) {const fileReader = new FileReader();form.append('file', files[0]);axios.post('https://localhost:8080/upload', form).then(res => console.log(res));}</script>
对于大文件上传考虑到上传时间太久、超出浏览器响应时间、提高上传效率、优化上传用户体验等问题进行了深入探讨,以下初略罗列各个知识点的实现思路:
Blob.prototype.slice
实现大文件的上传切分为多个小文件的上传requestIdleCallback
API 来计算)abort
方法进行请求的取消来实现catch
捕获方式,来对上传失败的内容进行重试,重试三次后再失败就进行放弃node-schedule
来实现<!-- 单选文件 --><input id="fileInput" type="file" />
const fileInput = document.querySelector('#fileInput');// 1. 点击输入框选择文件后触发fileInput.addEventListener('change', e => {const [file] = e.target.files;if (!file) return;const chunkList = sliceFileChunk(file);});// 2. 文件切片function sliceFileChunk(file) {// 文件大小const FILE_SIZE = file.size;// 文件切片大小const CHUNK_SIZE = 2 * 1024 * 1024;// 切片的个数const CHUNKS = Math.ceil(FILE_SIZE / CHUNK_SIZE);const blobSlice = Fil.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice;// 生成 MD5const spark = new SparkMD5.ArrayBuffer();// 实例化读取文件对象const reader = new FileReader();const currentChunk = 0;reader.onload = function(e) {const resul = e.target.result;spark.append(result);currentChunk++;if (currentChunk < chunks) {loadNext();console.log(`第${currentChunk}个分片解析完成`);} else {const md5 = spark.end();console.log('解析完成');}};function loadNext() {const start = currentChunk * CHUNK_SIZE;const end = start + CHUNK_SIZE > file.size ? file.size : start + CHUNK_SIZE;reader.raedAsArrayBuffer(blobSlice.call(file, start, end));}loadNext();}// 上传切片async function uploadChunkus() {const requestList = this.data.map(({ chunk, hash }) => {const formData = new FormData();formData.append('chunk', chunk);formData.append('hash', hash);formData.append('filename', this.container.file.name);return { formData };}).map(async ({ formData }) => {return this.request({url: 'http://localhost:3000',data: formData,});});// 并发上传文件切片await Promise.all(requestList);}async function handleUpload() {}
const http = require('http');const path = require('path');const fse = require('fs-extra');const multiparty = require('multiparty');const server = http.createServer();// 大文件存储目录const UPLOAD_DIR = path.resolve(__dirname, '..', 'target');server.on('request', async (req, res) => {res.setHeader('Access-Control-Allow-Oriign', '*');res.setHeader('Access-Control-Allow-Headers', '*');if (req.method === 'OPTIONS') {res.status = 200;res.end();return;}const multipart = new multiparty.Form();multipart.parse(req, async (err, fields, files) => {if (err) return;const [chunk] = files.chunk;const [hash] = fields.hash;const [filename] = fields.filename;const chunkDir = path.resolve(UPLOAD_DIR, filename);// 切片目录不存在,创建切片目录if (!fse.existsSync(chunkDir)) {await fse.mkdirs(chunkDir);}// fs-extra 专用方法,类似 fs.rename 并且跨平台// fs-extra 的 rename 方法 windows 平台会有权限问题await fse.move(chunk.path, `${chunkDir}/${hash}`);res.end('Received file chunk');});});server.listen(3000, () => console.log('Server is listening port 3000.'));
由于前端在发送合并请求时会携带文件名,服务端根据文件名可以找到上一步创建的切片文件夹。
接着使用 fs.createWriteStream
创建一个可写流,可写流文件名就是 切片文件夹名 + 后缀名 组合而成。
随后遍历整个切片文件夹,将切片通过 fs.createReadStream
创建可读流,传输合并到目标文件中。
值得注意的是每次可读流都会传输到可写流的指定位置,这是通过 createWriteStream
的第二个参数 start/end
控制的,目的是能够并发合并多个可读流到可写流中,这样即使流的顺序不同也能传输到正确的位置,所以这里还需要让前端在请求的时候多提供一个 size
参数。
断点续传的原理在于前端/服务端需要 记住 已上传的切片,这样下次上传就可以跳过之前已上传的部分,有两种方案实现记忆的功能:
localStorage
记录已上传的切片 hash
hash
,前端每次上传前向服务端获取已上传的切片无论是前端还是服务端,都必须要生成文件和切片的 Hash,之前我们使用 文件名 + 切片下标
作为切片 Hash,这样做文件名一旦修改就失去了效果,而事实上只要文件内容不变,Hash 就不应该变化,所以正确的做法是根据文件内容生成 hash,所以我们修改一下 Hash 的生成规则。
参考资料: