您现在的位置是:首页 > 编程语言学习 > 后端编程语言 > 文章正文 后端编程语言
JS实现可恢复的文件上传示例详解
2022-12-29 10:46:05
后端编程语言
简介使用fetch方法来上传文件相当容易。连接断开后如何恢复上传?这里没有对此的内建选项,但是我们有实现它的一些方式。对于大文件(如果我们...
使用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。
但是你可以下载这些代码,在本地运行以进行完整演示:
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';
- // 可以改用真实路径,例如
- // 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;
- }
uploader.js
- 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();
- }
- }
- }
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>
结果
正如我们所看到的,现代网络方法在功能上已经与文件管理器非常接近 -- 控制 header,进度指示,发送文件片段等。
我们可以实现可恢复的上传等。
上一篇:uniapp插件uview下表单无法动态校验的问题解决
下一篇:最后一页