JS实现可恢复的文件上传示例详解

目录
  • 正文
  • 不太实用的进度事件
  • 算法
    • server.js
    • uploader.js
    • index.html

正文

使用 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,进度指示,发送文件片段等。

我们可以实现可恢复的上传等。

以上就是JS实现可恢复的文件上传示例详解的详细内容,更多关于JS可恢复文件上传的资料请关注我们其它相关文章!

(0)

相关推荐

  • js前端上传文件缩略图技巧示例详解

    目录 引言 文件对象简介 Blob File FileReader FormData 文件对象之间的关系 缩略图的实现 总结 引言 通常情况下,前端提交给服务器的数据格式为JSON格式,但很多时候用户想上传自己的头像.视频等,这些非文本数据的时候,就不能直接以JSON格式上传到后端了. 当我们要获取用户上传的文件,可以使用input表单项,将type属性值设置为“file”. <form action=""> <input type="file"

  • 原生JS实现文件上传

    本文实例为大家分享了JS实现文件上传的具体代码,供大家参考,具体内容如下 一.目的: 实现上传图片功能 二.效果: 三.思路: 用input标签自带的上传,先隐藏掉,给上传按钮添加点击事件,绑定input的点击事件 四.代码: //html <input ref="img-upload-input" class="img-upload-input" type="file" accept=".png, .jpg" @cha

  • nodejs 实现简单的文件上传功能(示例详解)

    首先需要大家看一下目录结构,然后开始一点开始我们的小demo. 文件上传总计分为三种方式: 1.通过flash,activeX等第三方插件实现文件上传功能. 2.通过html的form标签实现文件上传功能,优点:浏览器兼容好. 3.通过xhr level2的异步请求,可以百度formData对象. 这里使用2做个练习. node插件请看下package.json文件 { "name": "upload", "version": "0.1

  • JSP实现文件上传功能

    本文实例为大家分享了JSP实现文件上传功能的具体代码,供大家参考,具体内容如下 一.准备部分 需要阿帕奇的fileupload.jar与io.jar包共同完成.构建完成路径后可继续. 资源地址 二.页面部分 <form action="UploadServlet" method="post" enctype="multipart/form-data"> 学号:<input type="text" name=&

  • JavaScript获取上传文件相关信息示例详解

    目录 前题场景 处理方式 图片文件 音频文件 判断处理 分析总结 前题场景 在开发过程中,文件上传是再熟悉不过的场景了,但是根据实际使用情况对上传文件的限制又各有不同.需要对本地上传文件进行相应的限制处理,防止不符合规则或者要求的文件上传到云存储中,从而造成云盘资源空间浪费. 与此同时,也在给客户端使用文件信息之前做了一次数据过滤处理,减少客户端对文件资源的处理和校验. 处理方式 因为客户端使用后台管理上传的图片文件和音频文件时,为了优化展示效果和加载的速度,所以在后台管理系统上传处希望依据图片

  • JavaScript进阶之前端文件上传和下载示例详解

    目录 文件下载 1.通过a标签点击直接下载 2.open或location.href 3.Blob和Base64 文件上传 文件上传思路 File文件 上传单个文件-客户端 上传文件-服务端 多文件上传-客户端 大文件上传-客户端 大文件上传-服务端 文件下载 1.通过a标签点击直接下载 <a href="https:xxx.xlsx" rel="external nofollow" download="test">下载文件</

  • java处理csv文件上传示例详解

    前言:示例只是做了一个最最基础的上传csv的示例,如果要引用到代码中去,还需要根据自己的业务自行添加一些逻辑处理. ReadCsvUtil工具类 package com.hanfengyeqiao.gjb.utils; import java.io.*; import java.util.*; /** * csv工具类 */ public class ReadCsvUtil { private static final String FIX="\uFEFF"; /** * 获取csv文

  • JavaScript 使用Ckeditor+Ckfinder文件上传案例详解

    目录 一.准备工作 二.解压 三.开始集成 一.准备工作 Ckeditor_4.5.7_full + Ckfinder_java_2.6.0 二.解压 1.解压ckeditor,和平常文件解压相同,正常解压即可 2.解压ckfinder,解压完成后进入ckfinder文件夹下,发现有CKFinderJava-2.6.0.war文件,继续解压. 3.注意看红框部分 三.开始集成 1.准备工作完成,将图1中的ckeditor,及图3中的ckfinder文件夹拷贝到我们自己的项目的WebContent

  • 基于Struts文件上传(FormFile)详解

    Struts中FormFile用于文件进行上传 1.在jsp文件中进行定义 <form action="/StrutsFileUpAndDown/register.do" method="post" enctype="multipart/form-data"> 名字:<input type="text" name="name" /> 头像:<input type="f

  • python中Django文件上传方法详解

    Django上传文件最简单最官方的方法 1.配置media路径 在settings.py中添加如下代码: MEDIA_ROOT = os.path.join(BASE_DIR, 'media') 2.定义数据表 import os from django.db import models from django.utils.timezone import now as timezone_now def upload_to(instance, filename):     now = timezo

  • js 实现 input type="file" 文件上传示例代码

    在开发中,文件上传必不可少,<input type="file" /> 是常用的上传标签,但是它长得又丑.浏览的字样不能换,我们一般会用让,<input type="file" />隐藏,点其他的标签(图片等)来时实现选择文件上传功能. 看代码: 复制代码 代码如下: <!DOCTYPE html> <html xmlns="http://www.w3.org/1999/xhtml"> <he

  • JS+Struts2多文件上传实例详解

    本文实例为大家分享了JS Struts2多文件上传的具体代码,供大家参考,具体内容如下 1.JSP页面: JS控制增加删除多个上传文件框,代码如下: <%@ page language="java" pageEncoding="UTF-8"%> <%@ taglib prefix="s" uri="/struts-tags"%> <!DOCTYPE html PUBLIC "-//W3C

  • PHP文件上传实例详解!!!

    首先来看下上传部分的表单代码:   复制代码 代码如下: <form method="post" action="upload.php" enctype="multipart/form-data">        <table border=0 cellspacing=0 cellpadding=0 align=center width="100%">         <tr>       

  • jquery组件WebUploader文件上传用法详解

    WebUploader是由Baidu WebFE(FEX)团队开发的一个简单的以HTML5为主,FLASH为辅的现代文件上传组件,下文来为各位演示一下关于jquery WebUploader文件上传组件的用法. 使用WebUploader还可以批量上传文件.支持缩略图等等众多参数选项可设置,以及多个事件方法可调用,你可以随心所欲的定制你要的上传组件. 接下来我以图片上传实例,给大家讲解如何使用WebUploader. HTML 我们首先将css和相关js文件加载. <link rel="s

  • springmvc+kindeditor文件上传实例详解

    本文实例为大家分享了springmvc+kindeditor文件上传的具体代码,供大家参考,具体内容如下 下载kindeditor 压缩包里面的jar放到tomcat的lib文件夹下,kindeditor文件放工程里,不用的可以删掉 jsp页面 <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%> <%@ ta

  • VUE学习之Element-ui文件上传实例详解

    目录 引言 单文件上传 和表单一起上传 需求:需同时给后端传文件FormData和其他所需参数 数据的编码方式 补充:代理的使用 什么是代理?为什么要用代理 代理的使用 process.env.NODE_ENV 总结 引言 对于文件上传,在开发主要涉及到以下两个方面: 单个文件上传和表单一起实现上传(这种情况一般都是文件上传之后,后端返回保存在服务器的文件名,最后和我们的表单一起上传) 单文件上传 element-ui中的el-upload组件默认发送post请求,在使用upload组件自动携带

随机推荐