文件上传
Published on Mar 19, 2023, with 26 view(s) and 0 comment(s)
Ai 摘要:本文介绍了使用原生JavaScript实现大文件切片上传的技术方案。主要内容包括:1) Content-Type类型解析,重点说明multipart/form-data在文件上传中的应用;2) 文件处理核心API(Blob、File、FileReader等)的功能与用法;3) 实现文件上传的三种方式(基础表单、Base64和切片上传),详细阐述了切片上传的实现流程:文件分片、进度监控、断点续传和服务器合并等关键步骤。文章提供了完整的技术实现思路和代码示例,适用于需要处理大文件上传的Web应用场景。

一. Content-Type(内容类型)

Content-Type(内容类型),一般是指网页中存在的 Content-Type,用于定义网络文件的类型和网页的编码,决定浏览器将以什么形式、什么编码读取这个文件,这就是经常看到一些 PHP 网页点击的结果却是下载一个文件或一张图片的原因。

Content-Type 标头告诉客户端实际返回的内容的内容类型。

语法格式:

Content-Type: text/html; charset=utf-8
Content-Type: multipart/form-data; boundary=something

实例:

Description

常见的媒体格式类型如下:

text/html : HTML格式
text/plain :纯文本格式
text/xml : XML格式
image/gif :gif图片格式
image/jpeg :jpg图片格式
image/png:png图片格式

以application开头的媒体格式类型:

application/xhtml+xml :XHTML格式
application/xml: XML数据格式
application/atom+xml :Atom XML聚合格式
application/json: JSON数据格式
application/pdf:pdf格式
application/msword : Word文档格式
application/octet-stream : 二进制流数据(如常见的文件下载)
application/x-www-form-urlencoded : <form encType=””>中默认的encType,form表单数据被编码为key/value格式发送到服务器(表单默认的提交数据的格式)

另外一种常见的媒体格式是上传文件之时使用的:

multipart/form-data : 需要在表单中进行文件上传时,就需要使用该格式

二. File、Blob、FileReader、ArrayBuffer、Base64

Description

JavaScript 提供了一些 API 来处理文件或原始文件数据,例如:File、Blob、FileReader、ArrayBuffer、base64 等。

1. Blob

引用Blob 对象表示一个不可变、原始数据的类文件对象。它的数据可以按文本或二进制的格式进行读取,也可以转换成 ReadableStream 来用于数据操作。

(1)创建:

可以使用 Blob() 构造函数来创建一个 Blob:

// new Blob(array, options);
const blob = new Blob(["Hello World"], { type: "text/plain" });
console.log(blob);
//Blob
//  size: 11
//  type: "text/plain"
//  [[Prototype]]: Blob

其有两个参数:

  • array:由 ArrayBuffer、ArrayBufferView、Blob、DOMString 等对象构成的,将会被放进 Blob;

  • options:可选的 BlobPropertyBag 字典,它可能会指定如下两个属性

  • - type:默认值为 "",表示将会被放入到 blob 中的数组内容的 MIME 类型。

  • - endings:默认值为"transparent",用于指定包含行结束符 的字符串如何被写入,不常用。

用途:

// index.html
<iframe></iframe>
// index.js
const iframe = document.getElementsByTagName("iframe")[0];
const blob = new Blob(["Hello World"], {type: "text/plain"});
iframe.src = URL.createObjectURL(blob);

Description

(2)Blob分片

除了使用Blob()构造函数来创建blob 对象之外,还可以从 blob 对象中创建blob,也就是将 blob 对象切片。Blob 对象内置了 slice() 方法用来将 blob 对象分片,其语法如下:

const blob = instanceOfBlob.slice([start [, end [, contentType]]]};
const blob = new Blob(["Hello World"], { type: "text/plain" });
const subBlob = blob.slice(0, 5);
console.log(subBlob);//Blob&nbsp;{size: 5, type: ""}
iframe.src = URL.createObjectURL(subBlob);

2. File

文件(File)接口提供有关文件的信息,并允许网页中的 JavaScript 访问其内容。实际上,File 对象是特殊类型的 Blob,且可以用在任意的 Blob 类型的 context 中。Blob 的属性和方法都可以用于 File 对象。

注意:File 对象中只存在于浏览器环境中,在 Node.js 环境中不存在。

在 JavaScript 中,主要有两种方法来获取 File 对象:

  • input元素上选择文件后返回的 FileList 对象;

  • 文件拖放操作生成的 DataTransfer 对象;

// 第一种方案:input
const fileInput = document.getElementById("fileInput");
fileInput.onchange = (e) => {
    console.log(e.target.files);
}
// 第二种方案:文件拖放
const dropZone = document.getElementById("drop-zone");
dropZone.ondragover = (e) => {
    e.preventDefault();
}
dropZone.ondrop = (e) => {
    e.preventDefault();
    const files = e.dataTransfer.files;
    console.log(files)
}

3. FileReader

FileReader 是一个异步 API,用于读取文件并提取其内容以供进一步使用。FileReader 可以将 Blob 读取为不同的格式。

基本使用

const reader = new FileReader();

这个对象常用属性如下: error:表示在读取文件时发生的错误。 result:文件内容。该属性仅在读取操作完成后才有效,数据的格式取决于使用哪个方法来启动读取操作。 readyState:表示FileReader状态的数字。取值为 0,1,2分别表示还未加载数据,正在加载数据,已经全部加载

FileReader 对象提供了以下方法来加载文件: readAsArrayBuffer():读取指定 Blob 中的内容,完成之后,result 属性中保存的将是被读取文件的 ArrayBuffer 数据对象; FileReader.readAsBinaryString():读取指定 Blob 中的内容,完成之后,result 属性中将包含所读取文件的原始二进制数据; FileReader.readAsDataURL():读取指定 Blob 中的内容,完成之后,result 属性中将包含一个data: URL 格式的 Base64 字符串以表示所读取文件的内容。 FileReader.readAsText():读取指定 Blob 中的内容,完成之后,result 属性中将包含一个字符串以表示所读取的文件内容。

可以看到,上面这些方法都接受一个要读取的 blob 对象作为参数,读取完之后会将读取的结果放入对象的 result 属性中。

事件处理

FileReader 对象常用的事件如下: abort:该事件在读取操作被中断时触发; error:该事件在读取操作发生错误时触发; load:该事件在读取操作完成时触发; progress:该事件在读取 Blob 时触发。当上传大文件时,可以通过 progress 事件来监控文件的读取进度:

当然,这些方法可以加上前置 on 后在HTML元素上使用,比如onload、onerror、onabort、onprogress。除此之外,由于FileReader对象继承自EventTarget,因此还可以使用 addEventListener() 监听上述事件。

const reader = new FileReader();
const fileInput = document.getElementById("fileInput");
fileInput.onchange = (e) => {
    reader.readAsText(e.target.files[0])
}
reader.onload = (e) => {
    console.log(e.target.result);
}

4. ArrayBuffer

ArrayBuffer 对象用来表示通用的、固定长度的原始二进制数据缓冲区。ArrayBuffer 的内容不能直接操作,只能通过 DataView 对象或 TypedArrray 对象来访问。这些对象用于读取和写入缓冲区内容。

ArrayBuffer 本身就是一个黑盒,不能直接读写所存储的数据,需要借助以下视图对象来读写: TypedArray:用来生成内存的视图,通过9个构造函数,可以生成9种数据格式的视图。 DataViews:用来生成内存的视图,可以自定义格式和字节序。

那 ArrayBuffer 与 Blob 有啥区别呢?根据 ArrayBuffer 和 Blob 的特性,Blob 作为一个整体文件,适合用于传输;当需要对二进制数据进行操作时(比如要修改某一段数据时),就可以使用 ArrayBuffer。

Description

5. Object URL

Object URL(MDN定义名称)又称Blob URL(W3C定义名称),是HTML5中的新标准。它是一个用来表示File Object 或Blob Object 的URL。

对于 Blob/File 对象,可以使用 URL构造函数的 createObjectURL() 方法创建将给出的对象的 URL。这个 URL 对象表示指定的 File 对象或 Blob 对象。我们可以在img、script 标签中或者 a 和 link 标签的 href 属性中使用这个 URL。

那这个 API 有什么意义呢?可以将Blob/File对象转化为URL,通过这个URL 就可以实现文件下载或者图片显示等。

6. Base64

Base64 是一种基于64个可打印字符来表示二进制数据的表示方法。Base64 编码普遍应用于需要通过被设计为处理文本数据的媒介上储存和传输二进制数据而需要编码该二进制数据的场景。这样是为了保证数据的完整并且不用在传输过程中修改这些数据。

在 JavaScript 中,有两个函数被分别用来处理解码和编码 base64 字符串:

atob():解码,解码一个 Base64 字符串; btoa():编码,从一个字符串或者二进制数据编码一个 Base64 字符串。

btoa("JavaScript")       // "SmF2YVNjcmlwdA=="
atob("SmF2YVNjcmlwdA==") // "JavaScript"

三. 文件上传

基本文件上传

let formData = new FormData();
formData.append("file", _file);
formData.append("filename", _file.name);

instance.post("/upload_single", formData)

基于bese64文件上传: 需要先把file文件读取为base64

const chagneBASE64 = (file) => {
    return new Promise((resolve) => {
      let fileReader = new FileReader();
      fileReader.readAsDataURL(file);
      fileReader.onload = (ev) => {
        resolve(ev.target.result);
      };
    });
  };

然后上传:

instance.post(
        "/upload_single_base64",
        {
          file: encodeURIComponent(BASE64),
          filename: file.name,
        },
        {
          headers: {
            "Content-Type": "application/x-www-form-urlencoded",
          },
        }

缩略图实现:将图片的路径设置为读取后的base64格式的文件值。生成唯一的hash值:可以选择的文件读取为ArrayBuffer,然后可以使用sparkmd5生成唯一hash值。进度管控实现:主要用到了axios请求时提供的onUploadProgress钩子函数。

instance.post("/upload_single", formData, {
        onUploadProgress(ev) {
          let { loaded, total } = ev;
          upload_progress.style.display = "block";
          upload_progress_value.style.width = `${(loaded / total) * 100}%`;
        },
      });

大文件上传与断点续传: 思路: 1,将要传输的文件分成多个切片,依次传输给服务端。 2,当所有切片传完后通知服务器,合并文件。 3,当上传失败时,再次上传时先获取上传成功的切片,然后排除掉成功的接着上传。或者是服务先进行校验,如果有了,立即通知客户端这个切片已存在。

upload_inp.addEventListener("change", async function () {
    let file = upload_inp.files[0];
    if (!file) return;
    upload_button_select.classList.add("loading");
    upload_progress.style.display = "block";
    // 先从服务器端获取到已经上传的切片
    // 获取文件的hash
    let already = [],
      data;
    let { HASH, suffix } = await changeBuffer(file);
    //获取已经上传的切片信息
    try {
      data = await instance.get("/upload_already", {
        params: {
          HASH,
        },
      });
      if (data.code == 0) {
        already = data.fileList;
      }
    } catch (error) {
      alert("失败了");
    }
    // 上传成功后或失败后清空
    const clear = () => {
      upload_button_select.classList.remove("loading");
      upload_progress.style.display = "none";
      upload_progress_value.style.width = "0%";
    };
    // 把文件进行切片:固定数量,固定大小
    let max = 1024 * 100,
      count = Math.ceil(file.size / max),
      index = 0,
      chunks = [];
    if (count > 100) {
      max = file.size / 100;
      count = 100;
    }
    while (index < count) {
      chunks.push({
        file: file.slice(index * max, (index + 1) * max),
        filename: `${HASH}_${index + 1}.${suffix}`,
      });
      index++;
    }
    index = 0;

    // 上传成功的处理
    const complate = async () => {
      // 管控进度
      index++;
      upload_progress_value.style.width = `${(index / count) * 100}%`;

      // 所有切片成功,发送请求,合并切片
      if (index < count) {
        return;
      }
      upload_progress_value.style.width = `100%`;
      try {
        data = await instance.post(
          "/upload_merge",
          {
            HASH,
            count,
          },
          {
            headers: {
              "Content-Type": "application/x-www-form-urlencoded",
            },
          }
        );
        if (data.code == 0) {
          alert(`恭喜您,文件上传成功,您可以基于 ${data.servicePath} 访问该文件~~`);
          clear();
          return;
        }
        throw data.codeText;
      } catch (error) {
        console.log(error);
        alert("切片合并失败,请您稍后再试~~");
        clear();
      }
    };
    // 把每个切片都上传到服务器上
    chunks.forEach((item) => {
      // 已经上传的无需再次上传
      if (already.length > 0 && already.includes(item.filename)) {
        complate();
        return;
      }
      let fm = new FormData();
      fm.append("file", item.file);
      fm.append("filename", item.filename);
      instance
        .post("/upload_chunk", fm)
        .then((data) => {
          if (data.code == 0) {
            complate();
            return;
          }
          return Promise.reject(data.codeText);
        })
        .catch((err) => {
          alert("当前切片上传失败,");
          clear();
        });
    });

相关文件上传的代码:文件上传