js-上传/下载文件

上传

为什么常用FormData对象来上传file

还可以用base64, 见下文。

Content-Type

Content-Type
  • application/x-www-form-urlencoded
    会在url上拼接字符串,如:k=123&c=12241,同时对于中文还会转码。
  • application/json
    直接会在请求体中 添加object对象 如: { a: 123, b: 456 }
  • multipart/form-data
    常用文件传输。
    在network中可以看到添加带数据类型等各类标识的文件类型字符串请求体 告诉服务器端接收对象是一个文件数据流
    image.png

如果采用JSON传输文件,得到的只是一个文件的描述对象,并不是文件本身:


image.png

FormData

FormDataAjax 2.0对象用以将数据编译成键值对,以便于XMLHttpRequest来发送数据。XMLHttpRequest Level 2提供的一个接口对象,可以使用该对象来模拟和处理表单并方便的进行文件上传操作。

image.png

创建FormData对象并赋值

const data = newFormdData()

data.set("name", "小A")
data.set("name1", "小B")

data.append("sex", "男")
data.append("sex", "女")

set()FormData.append(FormData 接口的append() 方法 会添加一个新值到 FormData 对象内的一个已存在的键中,如果键不存在则会添加该键。) 不同之处在于:如果某个 key 已经存在,set() 会直接覆盖所有该 key 对应的值,而 `FormData.append则是在该 key 的最后位置再追加一个值。

FormData取值

data.get("name") // 小A

data.has("name") // true

FormData对文件的处理

// antd的文件上传callback配置
    beforeUpload: (file: Blob) => {
      const data = new FormData()
      data.append("file", file)
    }

FormData与请求

使用axios发送post请求上传文件(multipart/form-data)到后端

Base64

base64只适合处理size小的文件。
base64是长得像字符串的byte类型字段。
base64可以通过application/json直接传输。

blob转base64:

export const blobToBase64 = (blob) => {
    return new Promise((resolve, reject) => {
        const fileReader = new FileReader();
        fileReader.onload = (e) => {
            resolve(e.target.result);
        };
        // readAsDataURL
        fileReader.readAsDataURL(blob);
        fileReader.onerror = () => {
            reject(new Error('blobToBase64 error'));
        };
    });
}

base64转blob:

export function base64ToBlob(str) {
    let bstr = window.atob(str);
    let n = bstr.length;
    let u8Arr = new Uint8Array(n);
    while(n--){
        u8Arr[n] = bstr.charCodeAt(n);
    }
    return new Blob([u8Arr])
}

window.atob()和window.btoa()

window.atob()是用来解码base64字符串。
window.btoa()方法用于创建一个 base-64 编码的字符串。

FileReader

是一个用于读取前端本地文件或者Blob类型数据的对象。

方法

方法 作用 参数 返回值
abort() 中止读取操作 none none
readAsArrayBuffer() 读取file和Blob内容 file/blob result属性中返回ArrayBuffer数据对象的文件内容
eadAsBinaryString()[已被W3废弃] 读取file和Blob内容 file/blob result属性中返回原始二进制数据的文件内容
readAsDataURL() 读取file和Blob内容 file/blob result属性中返回data:URL格式的Base64字符串的文件内容
readAsText() 读取file和Blob内容 file/blob result属性中返回一个字符串的文件内容

事件

FileReader.onabort:该事件是中止读取的时候触发。
FileReader.onerror:该事件是读取发生错误的时候触发。
FileReader.onload:该事件是读取完成的时候触发。
FileReader.onloadstart:该事件是读取操作刚开始的时候触发。
FileReader.onloadend:该事件是读取结束的时候触发(失败和成功的时候都会触发)。
FileReader.onprogress:该事件在读取的时候触发。

FileReader.onload为例,入参数为event,可以取到的数据如下:

          const fileReader = new FileReader();
          fileReader.readAsDataURL(file)
          fileReader.onload = (e) => {
            console.log(e, "e")
          }
image.png

如果是在FileReader.onprogress事件中,可以通过event中的loaded/total计算解析进度。

          const fileReader = new FileReader();
          fileReader.readAsDataURL(file)
          fileReader.onprogress = (e) => {
             console.log(e.loaded/e.total, "progress")
          }
image.png

只读属性

FileReader.error(只读):一个异常,表示在读取文件时发生的错误
FileReader.readyState(只读):表示FileReader状态的数字

状态名 描述
0 EMPTY 未加载任何数据
1 LOADING 数据加载中
2 DONE 数据加载完毕

FileReader.result(只读):读取完文件的内容,该属性在数据读取完成之后才有效,文件内容的格式是由读取的方法所决定。

ArrayBuffer

ArrayBuffer-对象

附:存储单位换算
1 KB = 1024 bytes(字节)
1 Mb = 1024 Kb
1 MB = 1024 KB
1 GB = 1024 MB

一种二进制数组,通过数组(实际上不是数组)的形式直接操作内存。
ArrayBuffer对象下,还包含两种视图:TypedArrayDataView

ArrayBuffer不可直接读取,需要通过他的两种视图进行读取。

TypedArray视图支持的数据类型一共有 9 种(DataView视图支持除Uint8C以外的其他 8 种)。

数据类型 字节长度 含义 对应的 C 语言类型
Int8 1 8 位带符号整数 signed char
Uint8 1 8 位不带符号整数 unsigned char
Uint8C 1 8 位不带符号整数(自动过滤溢出) unsigned char
Int16 2 16 位带符号整数 short
Uint16 2 16 位不带符号整数 unsigned short
Int32 4 32 位带符号整数 int
Uint32 4 32 位不带符号的整数 unsigned int
Float32 4 32 位浮点数 float
Float64 8 64 位浮点数 double

TypedArray包含以下构造函数:

Int8Array:8 位有符号整数,长度 1 个字节。
Uint8Array:8 位无符号整数,长度 1 个字节。
Uint8ClampedArray:8 位无符号整数,长度 1 个字节,溢出处理不同。
Int16Array:16 位有符号整数,长度 2 个字节。
Uint16Array:16 位无符号整数,长度 2 个字节。
Int32Array:32 位有符号整数,长度 4 个字节。
Uint32Array:32 位无符号整数,长度 4 个字节。
Float32Array:32 位浮点数,长度 4 个字节。
Float64Array:64 位浮点数,长度 8 个字节。

DataView实例提供 8 个方法读取内存:

getInt8:读取 1 个字节,返回一个 8 位整数。
getUint8:读取 1 个字节,返回一个无符号的 8 位整数。
getInt16:读取 2 个字节,返回一个 16 位整数。
getUint16:读取 2 个字节,返回一个无符号的 16 位整数。
getInt32:读取 4 个字节,返回一个 32 位整数。
getUint32:读取 4 个字节,返回一个无符号的 32 位整数。
getFloat32:读取 4 个字节,返回一个 32 位浮点数。
getFloat64:读取 8 个字节,返回一个 64 位浮点数。

new Blob()

Blob

const aBlob = new Blob( array, options );
  • array 是一个由ArrayBuffer, ArrayBufferView, Blob, DOMString 等对象构成的 Array ,或者其他类似对象的混合体,它将会被放进 Blob。DOMStrings 会被编码为 UTF-8。
  • options 是一个可选的BlobPropertyBag字典,它可能会指定如下两个属性:
    • type,默认值为 "",它代表了将会被放入到 blob 中的数组内容的 MIME 类型。
    • endings,默认值为"transparent",用于指定包含行结束符\n的字符串如何被写入。 它是以下两个值中的一个:"native",代表行结束符会被更改为适合宿主操作系统文件系统的换行符,或者 "transparent",代表会保持 blob 中保存的结束符不变

使用Blob来存储二进制对象,虽然是二进制原始数据但是类似文件的对象,因此可以像操作文件对象一样操作Blob对象。

Blob与ArrayBuffer的区别是,除了原始字节以外它还提供了mime type作为元数据,Blob和ArrayBuffer之间可以进行转换。

Blob.arrayBuffer()

FileReader.readAsArrayBuffer()类似,但是Blob.arrayBuffer()返回的是一个promise实例,而不是需要通过onload监听。

const bufferPromise = blob.arrayBuffer();

blob.arrayBuffer().then(buffer => /* 处理 ArrayBuffer 数据的代码……*/);

var buffer = await blob.arrayBuffer();

Blob.slice()

Blob.slice() 方法用于创建一个包含源 Blob指定字节范围内的数据的新 Blob 对象。

Blob.slice(<start, end, contentType>)

startend分别表示需要截取的下标(Blob.length),contentType代表截取后想要赋予新的数据片段的类型。

Blob.stream()

读取Blob对象,详见Blob.stream

Blob.text()

text() 方法返回一个 Promise 对象,包含 blob 中的内容,使用 UTF-8 格式编码。

const textPromise = blob.text();

blob.text().then(text => /* 执行的操作…… */);

var text = await blob.text();

分片上传

核心思想是借助Blob.slice()对原始文件进行切片,然后通过http进行并发传输,当所有切片传输完毕后,通知后端进行合并,这里需要对切片进行编号处理,以保证在合并的时候有正确的顺序。

        if(file){
          // 设置分片大小
            const sliceSize = 10 * 1024 * 1024; // 10m
            const blobList = []
          // 对文件分片
            for(let i = 0; i <= Math.floor(file.size / sliceSize); i+=1){
                blobList.push(file.slice(i * sliceSize, ( i + 1 ) * sliceSize < file.size ? ( i + 1 ) * sliceSize : file.size))
            }
          // 创建请求,并并发发送
           const requestList = blobList.map((it, i) => {
                return () => {const formData = new FormData()
                formData.append("file", it)
                formData.append("hash", i.toString())
                testRequest(formData)
            }
           }
           )
          // 等到所有分片数据都发送完毕后,发送一个合并分片的请求
           Promise.all(requestList.map(it => it())).then(() => {
                console.log("发送合并文件请求")
           })
        }

这里更好的做法是把每一份分片生成一个hash值来做唯一标识。
使用spark-md5将文件转换为hash:

npm i spark-md5
    // 获取apk的md5
      var fileReader = new FileReader()
      var spark = new SparkMD5() // 创建md5对象(基于SparkMD5)
      fileReader.readAsBinaryString(myfile) // myfile 对应上传的文件

      // 文件读取完毕之后的处理
      fileReader.onload = (e) => {
        console.log('获取文件的md5')
        spark.appendBinary(e.target.result)
        const md5 = spark.end()
        console.log(md5)

由于读取文件、生成hash这一步骤比较耗时间,可能会造成页面卡死,推荐使用web-worker处理:

// 导入脚本
self.importScripts("/spark-md5.min.js");

// 生成文件 hash
self.onmessage = e => {
  const { fileChunkList } = e.data;
  const spark = new self.SparkMD5.ArrayBuffer();
  let percentage = 0;
  let count = 0;
  const loadNext = index => {
    const reader = new FileReader();
    reader.readAsArrayBuffer(fileChunkList[index].file);
    reader.onload = e => {
      count++;
      spark.append(e.target.result);
      if (count === fileChunkList.length) {
        self.postMessage({
          percentage: 100,
          hash: spark.end()
        });
        self.close();
      } else {
        percentage += 100 / fileChunkList.length;
        self.postMessage({
          percentage
        });
        // calculate recursively
        loadNext(count);
      }
    };
  };
  loadNext(0);
};

最终可以得到该文件的最终hash: spark.end()

断点续传

核心思路:需要服务端/前端记住上次暂停时上传到哪个hash位置了。

改造request

如果要实现断点续传,就需要用到请求的一些特殊能力:

  • 取消未完成请求。
  • 获取请求成功失败情况,取得成功请求和失败请求的队列。
  1. 开始上传时,生成一个上传队列,将上传过程中的请求(或请求hash放入列表中)
  let requestList = []
   const importBackListFn = async (data) => {
        requestList.push({
            data: data.get("data"), 
            hash: data.get("hash"), 
})
        const result = await request()
        return {
          ...result,
          hash: data.get("hash"), 
        }
    }
  1. 上传过程中,将已上传成功的文件分片从队列中删除
importBackListFn().then((res) => {
  if(res){
      requestList = requestList.filter(it => it.hash !== res.hash)
  }
})
  1. 暂停时,将队列中的所有请求都取消,xhr.abort()或者new AbortController(),并清空上传队列
const handleAbort = () => {
  // 原生
    xhr.abort()
  // fetch
    const controller = new AbortController()
    controller.abort()

    requestList = []
}
  1. 重新续传时,通过后端请求取到已上传的队列,与本地文件分片对比,重新生成待上传的队列
// 请求已上传文件列表
getFileListHash().then(res => {
  blobList = blobList.filter(it => res.findIndex(ite => ite.hash === it.hash) !== -1)
})
// 继续上传blobList
参考上面的分片请求方法

分片上传/断点续传如何控制上传进度条

  1. 需要新增一个变量存储已上传成功的文件列表,包含hash和size即可。
const hasuploadFile;
importBackListFn().then((res) => {
  if(res){
      requestList = requestList.filter(it => it.hash !== res.hash)
      hasuploadFile.push(res)
  }
})
  1. 通过hasuploadFile.map(it => it.size).reduce((pre, cur) => pre + cur)/file.size计算百分比即可。
const process = hasuploadFile.map(it => it.size).reduce((pre, cur) => pre + cur)/file.size;

下载

Blob形式

这里的object参数是用于创建URL的File对象、Blob 对象或者 MediaSource 对象,生成的链接就是以blob:开头的一段地址,表示指向的是一个二进制数据。
其中localhost:1234是当前网页的主机名称和端口号,也就是location.host,而且这个Blob URL是可以直接访问的。需要注意的是,即使是同样的二进制数据,每调用一次URL.createObjectURL()方法,就会得到一个不一样的Blob URL。这个URL的存在时间,等同于网页的存在时间,一旦网页刷新或卸载,这个Blob URL就失效。
通过URL.revokeObjectURL(objectURL)可以释放 URL 对象。当你结束使用某个 URL 对象之后,应该通过调用这个方法来让浏览器知道不用在内存中继续保留对这个文件的引用了,允许平台在合适的时机进行垃圾收集。

const objectURL = URL.createObjectURL(object); //blob:http://localhost:1234/abcedfgh-1234-1234-1234-abcdefghijkl

需要在页面上创建a标签元素,并模拟点击

    let arch = window.document.createElement("a")
    if (arch.href) {
        window.URL.revokeObjectURL(arch.href)
    }
    arch.href = objectURL
    arch.download = filename || "juicy"
    arch.click()

base64形式

转成Blob再下载。
或者
base64拼成的链接可以直接通过a标签下载:


image.png
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 175,490评论 5 419
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 74,060评论 2 335
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 124,407评论 0 291
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 47,741评论 0 248
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 56,543评论 3 329
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 43,040评论 1 246
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 34,107评论 3 358
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 32,646评论 0 229
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 36,694评论 1 271
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 32,398评论 2 279
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 33,987评论 1 288
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 30,097评论 3 285
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 35,298评论 3 282
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 27,278评论 0 14
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 28,413评论 1 232
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 38,397评论 2 309
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 38,099评论 2 314