前言
在平时开发中,我们经常会遇到文件上传、图片预览(本地、互联网下载)、文件下载、生成文件并下载等功能开发。这些相关的实现代码网上基本都能搜索到,而且可能还有多种实现方式。如果我们直接 copy,基本上能跑起来并达到我们想要的效果。
然而,我们应该了解为什么是这样实现的。只有这样,出了问题我们能调试,能结合自己的实际场景采用最优的实现方式。
作为一个有追求的 coder,每一行代码我们都应该了如指掌。
本文的内容就是讲解 JS 文件系统中相关对象和 API,并整理了一些前端常见的文件操作实践。
希望通过此文,以后大家开发文件操作相关的功能时,不用去 google 了,自己就可以轻松撸出来(直接copy)。欢迎交流相互学习,并扩展实践示例。
关键词:File、Blob、FileReader、文件下载、图片预览、文件上传、导出文件
JS 文件相关对象
File
我们知道,NodeJS 提供了强大的文件操作模块(fs),允许我们读写机器上的文件系统(有文件权限)。
然而在前端浏览器(JS代码)中,我们是没有能力和 API 可以直接读写机器上文件系统的。
在网页中,有两种操作可以读取机器上的文件:
<Input type='file'>。- Drag 和 Drop 操作 。
读取本地文件
我们看下通过以上的操作,我们拿到的文件是怎样的:
通过 Input 控件上传文件后,文件会储存在 input 节点中的 files 属性里。由于可以多选,files (FileList 类型) 是个类数组,每一项代表一个文件。
我们可以看到,每个文件都是一个 File 对象。
File 对象是 JS 提供的用来获取文件信息和访问其内容的接口。通过 File 对象,我们可以获取到的文件信息有:
- lastModified:文件最后一次修改时间。
- name:文件名(带扩展名)。
- size:文件大小(单位为Byte)。
- type:文件的 MIME Type。 而比较常见的获取上传图片宽高的需求,原生API是无法获取到的,这时候就需要曲线救国了。通过借助 img 标签自然撑开来获取图片的宽高。(或者上传到服务端,服务端返回图片相关信息,包括原始宽高)。
const reader = new FileReader();
reader.onload = function(e) {
const image = new Image();
image.onload = () => {
const width = image.width; // 图片的宽
const height = image.height; // 图片的高
}
img.src = e.target.result;
};
reader.readAsDataURL(file);
创建文件
除了通过文件控件选择本地文件外,还可以使用 File() 构造函数来创建一个文件,得到 File 对象。
new File(content, fileName, [options])
- content[Array]:一个由 ArrayBuffer、ArrayBufferView、Blob、USVString 组成的数组,也就是写入文件的内容。
- fileName[String]:文件名.
- options[Object]:
- lastModified[Number]:文件最后一次修改时间戳。
- type[String]:指定文件 MIME Type。 这里的 content 可以是字符串,也可以是 ArrayBuffer、ArrayBufferView、Blob,这三个对象,后面我们讲到。
其实,我们前端也是可以生成文件,只是我们只能通过浏览器下载的方式下载到用户文件系统中,无法像服务端那样直接操作文件系统写入文件。
从网页上导出文件功能,可以通过 File() 构造函数生成一个文件对象来实现,具体实践请看下文。
Data URLs
Data URLs 是 URI 一种规范,也是一种标识资源的方式。它允许内容的创建者将小文件嵌入在 documents 中,浏览器是能解析这个格式的数据并展示在网页上的。
Data URLs 最常用的场景就是将文件编码为 base 64 格式,并嵌在网页中。比如 <img>、<a> 等标签的资源都是通过资源标识符 URL 来指定的。所以当我们需要将本地文件,或者文件对象作为资源指定给 <img>、<a> 等标签时,需要将文件对象转为 Data URLs(不能直接将 File 对象赋值给 img 标签的 src)。下面会讲到如何将文件编码为 Data URLs。
Data URLs 字符串本身就是数据,使用 base64 编码格式,对数据进行编码,所以将文件转为 Data URLs 后,URLs 字符串大小比原始文件大 30%(base 64格式造成的,有兴趣的同学可以查询相关资料)。
它的语法如下(符合 URI 规范):
data:[<mediatype>][;base64],<data>
// etc
data:image/png;base64,iVBORw0KGgoAAAANSUh....
| 组成 | 含义 |
|---|---|
| data: | 前缀,URI 协议 |
[<mediatype>] | MIME TYPE 数据类型 |
[;base64] | 可选 base64 标识,如果是二进制数据,需要指定该标识 |
<data> | 数据 |
如果是二进制数据,则会使用 base64 编码规则将其编码为可视的 ASCII。
Blob
Blob 是什么
Blob(Binary Large Object)表示一个不可变、原始数据的类文件对象。它可以保存比较大的数据,比如视频文件。
Blob 对象就是代表了原始数据,比如文件对象(File),也是继承自它,然后扩展以支持读取用户系统上的文件。
由于 File 对象继承自 Blob,所以所有接收 Blob 类型的参数,都可以传入 File,这就是 Java 中的多态。
我们可以通俗地理解,blob 对象保存了数据,比如文件数据、字符串等。上面我们提到的 File 对象所代表的文件数据也是通过 Blob 来保存的,File 对象继承自 Blob 对象,并扩展了相关的接口以支持从用户的文件系统选择文件。
使用 Blob
构造 blob 对象
我们可以使用 Blob() 构造函数来生成一个 blob 对象。
var newBlob = new Blob(array, options);
- 第一个参数是一个由 ArrayBuffer、ArrayBufferView、Blob、String等类型对象组成的数组,这也是 blob 对象保存的数据。
- 第二个参数是可选配置,有两个配置项:
- type —— 默认值为 "",它设置了保存到 Blob 数据的 MIME 类型。
- endings —— 默认值为 "transparent",用于设置如何解释换行符 '\n' 。设置 "transparent" 则表示保存原数据格式不变;设置 "native" 则表示更改为适合宿主操作系统文件系统的换行符。
blob 对象属性
- size: 该对象所保存数据的大小,单位为字节
- type: 该对象保存数据的 MIME 类型。
blob 对象方法
- slice([start[, end[, contentType]]]):返回一个新的 Blob 对象,包含了源 Blob 对象中指定范围内的数据。这里的 start 和 end 是指字节大小。
- stream():返回一个能读取 blob 内容的 ReadableStream。
- text():返回一个 Promise 对象且包含 blob 所有内容的 UTF-8 格式的 USVString。
- arrayBuffer():返回一个 Promise 对象且包含 blob 所有内容的二进制格式的 ArrayBuffer
Blob 对象保存的数据是不可变的,只能通过 slice 方法截取该对象保存数据一部分,然后返回一个新 blob 对象,之前的 blob 对象不发生改变。
Blob URLs
上面我们说了 Data URLs,同样,Blob URLs 也是 URI 的一种。为了将文件指定给 <img>、 <a> 等标签,我们将 File 对象编码为 Data URLs 。如果我们要把 Blob 对象指定给 <img>、<a> 等标签,也需要将其转为 URL,Blob URLs 就是 Blob 对象的 URL 表示法。将 Blob 对象转为 Blob URLs 有两种方法:FileReader. readAsDataURL(blob) 和 URL.createObjectURL (blob)。
FileReader 下文详细介绍
Blob URLs 格式如下:
blob:<origin>/<uuid>
URL.createObjectURL (blob) 是同步函数,这时 Blob 对象所代表的文件数据还是保存在用户机器的磁盘中(如果是动态创建的blob对象数据则保存在内存内),浏览器内部会生成一个 Blob URL 字符串,该字符串映射到 Blob 对象,这也是为什么 createObjectURL 是同步的,而 FileReader.readAsDataURL(blob) 是异步的。因此,此类 URL 较短,但可以访问 Blob。生成的 URL 仅在当前文档打开的状态下才有效(经测试,当前浏览器都可以打开,新开一个浏览器则会 404)。
它跟普通 http 的 URL 一样,在 network 中可以看到发起的请求,不过该请求会被浏览器处理掉。
不过,Blob URLs 也有需要注意的地方。 在创建 Blob URLs 后 Blob 会被引用,浏览器不会主动释放它,除非 Blob 被主动清除了。 所以在使用 Blob URLs 时(使用 URL.createObjectURL 创建的),我们要记得调用 URL.revokeObjectURL(url) 方法,从内部映射中删除引用,从而允许 Blob 内垃圾回收,并释放内存。
Data URLs 与 Blob URLs 对比
| Data URLs | Blob URLs | |
|---|---|---|
| 数据内容 | 使用 base64 编码,字符串本身数据,对原来数据没有影响 | 只是一个标识符,指向所代表的 Blob 对象 |
| 文件源 | File 对象 | Blob 对象 |
| 生效范围 | 可以复制,任意地方使用,本身就是数据 | 只是映射,只能在当前浏览器内生效 |
FileReader
File 对象只是 JS 提供的用来获取文件信息和访问其内容的接口。而文件数据本身还是保持在系统的硬盘上的,并没有读取到内存中。如果想要读取文件内容,那么就要借助 FileReader API 了。
FileReader 允许我们以指定的编码格式读取 File 对象或者 Blob 对象代表的文件或数据内容。
let reader = new FileReader(); // 没有参数
提供了四个主要方法,每个方法读取的格式不同:
我们知道 File 对象是继承自 Blob 的,并扩展了一些文件系统相关能力。多态概念,支持 Blob 的地方同时也支持 File。所以 FileReader 是可以读取 File 对象代表的数据的。
- readAsArrayBuffer(Blob):读取 Blob 数据并转为 ArrayBuffer 格式。将 Blob(File 文件) 转为 ArrayBuffer。
- readAsBinaryString(Blob): 读取 Blob 数据并将其转为二进制字符串。以上这两个方法在将文件数据转为二进制格式时会用到。
- readAsText(Blob, [encoding]) :读取数据为指定编码格式的文本。将 Blob 转为字符串。
- readAsDataURL(Blob) :读取数据并将其编码成 base64 编码的URL。如果是 Blob 对象,则转为 Blob URLs。如果是 File 对象,则转为 Data URLs。
除此之外,还有一些获取状态的属性,以及读取进度事件等。更多内容查看:FileReader。
以上四个方法会将磁盘中的文件数据读取到内存中来,所以都是异步,在数据读取完成后则会调用 onload 方法。
function handleFiles(files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const img = document.createElement("img");
const reader = new FileReader();
reader.onload = function(e) { img.src = e.target.result };
reader.readAsDataURL(file);
}
}}
ArrayBuffer
ArrayBuffer 对象代表储存二进制数据的一段内存,它不能直接读写,只能通过视图(TypedArray 视图和 DataView 视图)来读写,视图的作用是以指定格式解读二进制数据。
详细了解请移步:es6.ruanyifeng.com/#docs/array…
ArrayBuffer 关注的是储存数据的内存,可以通过视图去操作对于地址的数据。而 Blob 代表的是一个不可变的数据(以二进制的方式储存),我们是不可以更改其内容的,Blob 关注的其表示的数据,而不是存储的地方。
文件操作实践示例
本地文件上传到服务端
Form 表单
这是最原始的文件上传方式。设置 form 属性 enctype="multipart/form-data"。
<form action="请求地址" method="请求类型" enctype="multipart/form-data">
<input type="file" name="">
<input type="text" name="">
<input type="submit" value="提交">
</form>
这种方式灵活性比较小,一般不使用。
FormData
HTML5 提供了 FormData 对象,让我们可以更加自由和方便地构造表单数据。借助该对象,我们可以很灵活地上传文件了。
var formData = new FormData(form) // form 可选
form 是 <form> 元素,我们可以从已有的表单元素中提取数据并生成一个 FormData 对象。
FormData 对象实例可以直接作为 POST 请求的 body。
同时需要设置 Header 'Content-Type': 'multipart/form-data'。
function upload(file: File) {
const formData = new FormData();
formData.append('file', file);
return axios.post<{}, file.File>('/api/v1/files/create', formData, {
headers: {
'Content-Type': 'multipart/form-data',
},
});
}
本地图片上传预览
我们知道,img 标签的 src 接收一个字符串,无法直接将 File 对象赋值个 src。所以需要对 File 对象进行转换,转换成 src 能处理的格式。
Img 标签的 src 属性
首先我们来重新认识下 src 属性。
我们之前都是直接给照片的地址的,比如绝对路径: http://www/aa.jpg,或者相对路径: /static/bb.png。 在网页上上传文件,我们得到的是一个 File 对象。所以我们是不能直接把该对象赋值给 src 属性的。
我们可以看到,src 属性是一个指定了图片资源的 URL,并且可以使用绝对路径和相对路径,所以他一定是字符串类型。
如果我们把 file 对象直接复制给 src,会发现 JS 会把文件对象转换为 String 类型,调用了对象的 toString 方法。
所以,我们需要把 File 对象转为 src 能解析的 URL。src 支持的 URL 协议。
Img 标签的 src 属性,除了支持默认的 HTTP 协议的 URL,还支持了其他类型的 URL(URI)。比如:
- data URLs
- mediastream URLs
- Blob URLs
- filesystem URLs
扩展 - URL 与 URI
URL ( Uniform Resource Locator ) ,统一资源定位符。URL 是基于 HTTP 协议,用来标识互联网资源的一个短字符串。也是我们所说的网址、链接。
URI(Uniform Resource Identifier),统一资源标识符。它是一个统一的用于标识资源的规范,其语法如下:
URI = scheme:[//authority]path[?query][#fragment]
不同 scheme 规范,就出现了多种 URI,而 URL 是 URI 的一种。
URL 和 URN 都已经是 URI 的一种。只是他们标识资源的方式不同而已。
urn:isbn:9780141036144
urn:ietf:rfc:7230
由于 URN 基本没有使用了,而且 URL 熟知度更高,所以现在说 URI 也是 URL 了。data URIs 都被改名为 data URLs 了。
实现:Data URLs
将 File 转为 Data URLs ?是不是很眼熟?FileReader 派生用场了。
function handleFiles(files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const img = document.createElement("img");
const reader = new FileReader();
reader.onload = function(e) { img.src = e.target.result };
reader.readAsDataURL(file);
}}
这样,我们就实现了本地上传图片预览功能。
实现:Blob URLs
上面我们说到,img src 不仅支持 data URLs,还支持 blob URLs。我们可以使用 URL.createObjectURL 来将 Blob 转为 URL。
function handleFiles(files) {
for (let i = 0; i < files.length; i++) {
const file = files[i];
const img = document.createElement("img");
img.src = URL.createObjectURL(file);
}}
网页上导出、下载文件
网站上导出文件,就是浏览器下载文件。我们需要借助 <a> 标签,该标签上有个 download 属性,如果设置了该属性(该值也是下载文件名,包括后缀名),点击 <a> 标签则会触发浏览器下载,文件地址是 href 的值。
function download(url, fileName) {
// 使用 a 标签实现下载
const a = document.createElement('a');
a.href = url;
a.download = fileName;
a.target = '_blank';
a.click();
}
不同需求场景下,生成不同的 URL,则可以实现不同的需求。
点击下载互联网资源
<a> 标签的 download 下载地址有同源限制。
文件同源
如果是同源文件,可以 url 可以是完整的 HTTP URL,也可以是相对文件地址,比如 ./static/download.png。
// 下载文件地址同源
download('./static/download.png', 'download');
download('http://origin/download.png', 'download');
文件非同源
如果我们要下载其他域名下的文件,是不能直接将 HTTP URL 复制该 href 的,这样不会触发下载,会跳转。我们需要将文件通过 HTTP 请求下载到本地,通过 Blob 对象来保存,然后转为 Blob URLs。
const xhr = new XMLHttpRequest();
xhr.open('GET', downloadUrl, true);
xhr.responseType = 'blob'; // 这里设置为 blob,则 xhr.response 就是 blob 类型了
xhr.onload = () => {
if (xhr.status === 200) {
// 创建 Blob URLs
const url = URL.createObjectURL(xhr.response);
download(url, 'download.txt');
}
};
xhr.send();
下载、导出用户输入内容
使用 <a> 标签实现下载功能时,href 可以是同源的 HTTP URL,也可以是 Data URLs 或者 Blob URLs。
为了导出用户输入的内容(假设内容是字符串),我们只能将内容转为 Data URLs 或者 Blob URLs。这里的实现思路就是,以导出内容为 content 生成 File 对象 或者 Blob 对象,然后通过 FileReader 生成 Data URLs 或者 Blob URLs,或者使用 URL.createObjectURL 生成 Blob URLs。
// 转为 File 对象
const fileName = "foo.txt";
const file = new File(["foo"], fileName, {
type: "text/plain",
});
// 使用 FileReader 将文件转为 Data URLs
const reader = new FileReader();
reader.readAsDataURL(file)
reader.onload = (e) => {
// 使用 a 标签实现下载
download(e.target.result, fileName);
}
/******************* ********************/
// 转为 Blob 对象
const fileName = "foo.txt";
const blob = new Blob(["foo"], { type: "text/plain" });
// 使用 FileReader 将 Blob 对象转为 Blob URLs
const reader = new FileReader();
reader.readAsDataURL(file)
reader.onload = (e) => {
// 使用 a 标签实现下载
download(e.target.result, fileName);
}
// 使用 URL.createObjectURL 将 Blob 对象转为 Blob URLs
download(URL.createObjectURL(blob), fileName);
分片上传
当我们需要上传大文件时,我们可以采用分片上传方式,实现并发上传、失败续传等功能。
当用户从网页上选择一个文件后,我们就得到了可以访问该文件的 File 对象,File 对象是继承自 Blob 的。上面我们提到,blob 对象有个 slice 方法,可以截取一段数据,所以借助该方法,我们可以实现分片上传。
const chunkSize = 40000;
const url = "https://httpbin.org/post";
async function chunkedUpload(file) {
for (let start = 0; start < file.size; start += chunkSize) {
const chunk = file.slice(start, start + chunkSize + 1);
const fd = new FormData();
fd.append("data", chunk);
await fetch(url, { method: "post", body: fd }).then((res) =>
res.text()
);
}
}