⚠️本文为掘金社区首发签约文章,未获受权禁止转载javascript
在 文件上传,搞懂这8种场景就够了(1452个👍) 这篇文章发布以后,阿宝哥收到了挺多掘友的留言,感谢掘友们一直以来的鼓励与支持。其中掘友 @个人烟雨不在江南 和 @rainx 在文章底部分别发了如下留言:html
既然掘友有要求,连标题也帮阿宝哥想好了,那咱们就来整一篇文章,总结一下文件下载的场景。前端
通常在咱们工做中,主要会涉及到 9 种文件下载的场景,每一种场景背后都使用不一样的技术,其中也有不少细节须要咱们额外注意。今天阿宝哥就来带你们总结一下这 9 种场景,让你们可以轻松地应对各类下载场景。阅读本文后,你将会了解如下的内容:html5
在浏览器端处理文件的时候,咱们常常会用到 Blob 。好比图片本地预览、图片压缩、大文件分块上传及文件下载。在浏览器端文件下载的场景中,好比咱们今天要讲到的 a 标签下载、showSaveFilePicker API 下载、Zip 下载 等场景中,都会使用到 Blob ,因此咱们有必要在学习具体应用前,先掌握它的相关知识,这样能够帮助咱们更好地了解示例代码。java
Blob(Binary Large Object)表示二进制类型的大对象。在数据库管理系统中,将二进制数据存储为一个单一个体的集合。Blob 一般是影像、声音或多媒体文件。在 JavaScript 中 Blob 类型的对象表示一个不可变、原始数据的类文件对象。 它的数据能够按文本或二进制的格式进行读取,也能够转换成 ReadableStream 用于数据操做。node
Blob
对象由一个可选的字符串 type
(一般是 MIME 类型)和 blobParts
组成:ios
在 JavaScript 中你能够经过 Blob 的构造函数来建立 Blob 对象,Blob 构造函数的语法以下:git
const aBlob = new Blob(blobParts, options);
复制代码
相关的参数说明以下:github
""
,它表明了将会被放入到 blob 中的数组内容的 MIME 类型。"transparent"
,用于指定包含行结束符 \n
的字符串如何被写入。 它是如下两个值中的一个: "native"
,表明行结束符会被更改成适合宿主操做系统文件系统的换行符,或者 "transparent"
,表明会保持 blob 中保存的结束符不变。Blob URL/Object URL 是一种伪协议,容许 Blob 和 File 对象用做图像、下载二进制数据连接等的 URL 源。在浏览器中,咱们使用 URL.createObjectURL
方法来建立 Blob URL,该方法接收一个 Blob
对象,并为其建立一个惟一的 URL,其形式为 blob:<origin>/<uuid>
,对应的示例以下:web
blob:http://localhost:3000/53acc2b6-f47b-450f-a390-bf0665e04e59
复制代码
浏览器内部为每一个经过 URL.createObjectURL
生成的 URL 存储了一个 URL → Blob 映射。所以,此类 URL 较短,但能够访问 Blob
。生成的 URL 仅在当前文档打开的状态下才有效。它容许引用 <img>
、<a>
中的 Blob
,但若是你访问的 Blob URL 再也不存在,则会从浏览器中收到 404 错误。
上述的 Blob URL 看似很不错,但实际上它也有反作用。 虽然存储了 URL → Blob 的映射,但 Blob 自己仍驻留在内存中,浏览器没法释放它。映射在文档卸载时自动清除,所以 Blob 对象随后被释放。可是,若是应用程序寿命很长,那么 Blob 在短期内将没法被浏览器释放。所以,若是你建立一个 Blob URL,即便再也不须要该 Blob,它也会存在内存中。
针对这个问题,你能够调用 URL.revokeObjectURL(url)
方法,从内部映射中删除引用,从而容许删除 Blob(若是没有其余引用),并释放内存。
如今你已经了解了 Blob 和 Blob URL,若是你还意犹未尽,想深刻理解 Blob 的话,能够阅读 你不知道的 Blob 这篇文章。下面咱们开始介绍客户端文件下载的场景。
随着 Web 技术的不断发展,浏览器的功能也愈来愈强大。这些年出现了不少在线 Web 设计工具,好比在线 PS、在线海报设计器或在线自定义表单设计器等。这些 Web 设计器容许用户在完成设计以后,把生成的文件保存到本地,其中有一部分设计器就是利用浏览器提供的 Web API 来实现客户端文件下载。下面阿宝哥先来介绍客户端下载中,最多见的 a 标签下载 方案。
html
<h3>a 标签下载示例</h3>
<div>
<img src="../images/body.png" />
<img src="../images/eyes.png" />
<img src="../images/mouth.png" />
</div>
<img id="mergedPic" src="http://via.placeholder.com/256" />
<button onclick="merge()">图片合成</button>
<button onclick="download()">图片下载</button>
复制代码
在以上代码中,咱们经过 img
标签引用了如下 3 张素材:
当用户点击 图片合成 按钮时,会将合成的图片显示在 img#mergedPic
容器中。在图片成功合成以后,用户能够经过点击 图片下载 按钮把已合成的图片下载到本地。对应的操做流程以下图所示:
由上图可知,总体的操做流程相对简单。接下来,咱们来看一下 图片合成 和 图片下载 的实现逻辑。
js
图片合成的功能,阿宝哥是直接使用 Github 上 merge-images 这个第三方库来实现。利用该库提供的 mergeImages(images, [options])
方法,咱们能够轻松地实现图片合成的功能。调用该方法后,会返回一个 Promise 对象,当异步操做完成后,合成的图片会以 Data URLs 的格式返回。
const mergePicEle = document.querySelector("#mergedPic");
const images = ["/body.png", "/eyes.png", "/mouth.png"].map(
(path) => "../images" + path
);
let imgDataUrl = null;
async function merge() {
imgDataUrl = await mergeImages(images);
mergePicEle.src = imgDataUrl;
}
复制代码
而图片下载的功能是借助 dataUrlToBlob
和 saveFile
这两个函数来实现。它们分别用于实现 Data URLs => Blob 的转换和文件的保存,具体的代码以下所示:
function dataUrlToBlob(base64, mimeType) {
let bytes = window.atob(base64.split(",")[1]);
let ab = new ArrayBuffer(bytes.length);
let ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type: mimeType });
}
// 保存文件
function saveFile(blob, filename) {
const a = document.createElement("a");
a.download = filename;
a.href = URL.createObjectURL(blob);
a.click();
URL.revokeObjectURL(a.href)
}
复制代码
由于本文的主题是介绍文件下载,因此咱们来重点分析 saveFile
函数。在该函数内部,咱们使用了 HTMLAnchorElement.download 属性,该属性值表示下载文件的名称。若是该名称不是操做系统的有效文件名,浏览器将会对其进行调整。此外,该属性的做用是代表连接的资源将被下载,而不是显示在浏览器中。
须要注意的是,download
属性存在兼容性问题,好比 IE 11 及如下的版本不支持该属性,具体以下图所示:
(图片来源:caniuse.com/download)
当设置好 a 元素的 download
属性以后,咱们会调用 URL.createObjectURL
方法来建立 Object URL,并把返回的 URL 赋值给 a 元素的 href
属性。接着经过调用 a 元素的 click
方法来触发文件的下载操做,最后还会调用一次 URL.revokeObjectURL
方法,从内部映射中删除引用,从而容许删除 Blob(若是没有其余引用),并释放内存。
关于 a 标签下载 的内容就介绍到这,下面咱们来介绍如何使用新的 Web API —— showSaveFilePicker
实现文件下载。
a 标签下载示例:a-tag
showSaveFilePicker API 是 Window
接口中定义的方法,调用该方法后会显示容许用户选择保存路径的文件选择器。该方法的签名以下所示:
let FileSystemFileHandle = Window.showSaveFilePicker(options);
复制代码
showSaveFilePicker 方法支持一个对象类型的可选参数,可包含如下属性:
excludeAcceptAllOption
:布尔类型,默认值为 false
。默认状况下,选择器应包含一个不该用任何文件类型过滤器的选项(由下面的 types
选项启用)。将此选项设置为 true
意味着 types
选项不可用。types
:数组类型,表示容许保存的文件类型列表。数组中的每一项是包含如下属性的配置对象:
description(可选)
:用于描述容许保存文件类型类别。accept
:是一个对象,该对象的 key
是 MIME 类型,值是文件扩展名列表。调用 showSaveFilePicker 方法以后,会返回一个 FileSystemFileHandle 对象。有了该对象,你就能够调用该对象上的方法来操做文件。好比调用该对象上的 createWritable 方法以后,就会返回 FileSystemWritableFileStream 对象,就能够把数据写入到文件中。具体的使用方式以下所示:
async function saveFile(blob, filename) {
try {
const handle = await window.showSaveFilePicker({
suggestedName: filename,
types: [
{
description: "PNG file",
accept: {
"image/png": [".png"],
},
},
{
description: "Jpeg file",
accept: {
"image/jpeg": [".jpeg"],
},
},
],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return handle;
} catch (err) {
console.error(err.name, err.message);
}
}
function download() {
if (!imgDataUrl) {
alert("请先合成图片");
return;
}
const imgBlob = dataUrlToBlob(imgDataUrl, "image/png");
saveFile(imgBlob, "face.png");
}
复制代码
当你使用以上更新后的 saveFile
函数,来保存已合成的图片时,会显示如下保存文件选择器:
由上图可知,相比 a 标签下载 的方式,showSaveFilePicker API 容许你选择文件的下载目录、选择文件的保存格式和更改存储的文件名称。看到这里是否是以为 showSaveFilePicker API 功能挺强大的,不过惋惜的是该 API 目前的兼容性还不是很好,具体以下图所示:
(图片来源:caniuse.com/?search=sho…
其实 showSaveFilePicker 是 File System Access API 中定义的方法,除了 showSaveFilePicker 以外,还有 showOpenFilePicker 和 showDirectoryPicker 等方法。若是你想在实际项目中使用这些 API 的话,能够考虑使用 GoogleChromeLabs 开源的 browser-fs-access 这个库,该库可让你在支持平台上更方便地使用 File System Access API,对于不支持的平台会自动降级使用 <input type="file">
和 <a download>
的方式。
可能你们对 browser-fs-access 这个库会比较陌生,可是若是换成是 FileSaver.js 这个库的话,应该就比较熟悉了。接下来,咱们来介绍如何利用 FileSaver.js 这个库实现客户端文件下载。
showSaveFilePicker API 下载示例:save-file-picker
FileSaver.js 是在客户端保存文件的解决方案,很是适合在客户端上生成文件的 Web 应用程序。它是 HTML5 版本的 saveAs() FileSaver 实现,支持大多数主流的浏览器,其兼容性以下图所示:
(图片来源:github.com/eligrey/Fil…
在引入 FileSaver.js 这个库以后,咱们就可使用它提供的 saveAs
方法来保存文件。该方法对应的签名以下所示:
FileSaver saveAs(
Blob/File/Url,
optional DOMString filename,
optional Object { autoBom }
)
复制代码
saveAs 方法支持 3 个参数,第 1 个参数表示它支持 Blob/File/Url
三种类型,第 2 个参数表示文件名(可选),而第 3 个参数表示配置对象(可选)。若是你须要 FlieSaver.js 自动提供 Unicode 文本编码提示(参考:字节顺序标记),则须要设置 { autoBom: true}
。
了解完 saveAs 方法以后,咱们来举 3 个具体的使用示例:
1. 保存文本
let blob = new Blob(["你们好,我是阿宝哥!"], { type: "text/plain;charset=utf-8" });
saveAs(blob, "hello.txt");
复制代码
2. 保存线上资源
saveAs("https://httpbin.org/image", "image.jpg");
复制代码
若是下载的 URL 地址与当前站点是同域的,则将使用 a[download]
方式下载。不然,会先使用 同步的 HEAD 请求 来判断是否支持 CORS 机制,若支持的话,将进行数据下载并使用 Blob URL 实现文件下载。若是不支持 CORS 机制的话,将会尝试使用 a[download]
方式下载。
标准的 W3C File API Blob 接口并不是在全部浏览器中均可用,对于这个问题,你能够考虑使用 Blob.js 来解决兼容性问题。
(图片来源:caniuse.com/?search=blo…
3. 保存 canvas 画布内容
let canvas = document.getElementById("my-canvas");
canvas.toBlob(function(blob) {
saveAs(blob, "abao.png");
});
复制代码
须要注意的是 canvas.toBlob()
方法并不是在全部浏览器中均可用,对于这个问题,你能够考虑使用 canvas-toBlob.js 来解决兼容性问题。
(图片来源:caniuse.com/?search=toB…
介绍完 saveAs 方法的使用示例以后,咱们来更新前面示例中的 download
方法:
function download() {
if (!imgDataUrl) {
alert("请先合成图片");
return;
}
const imgBlob = dataUrlToBlob(imgDataUrl, "image/png");
saveAs(imgBlob, "face.png");
}
复制代码
很明显,使用 saveAs 方法以后,下载已合成的图片就很简单了。若是你对 FileSaver.js 的工做原理感兴趣的话,能够阅读 聊一聊 15.5K 的 FileSaver,是如何工做的? 这篇文章。前面介绍的场景都是直接下载单个文件,其实咱们也能够在客户端同时下载多个文件,而后把已下载的文件压缩成 Zip 包并下载到本地。
FileSaver 下载示例:file-saver
在 文件上传,搞懂这8种场景就够了 这篇文章中,阿宝哥介绍了如何利用 JSZip 这个库提供的 API,把待上传目录下的全部文件压缩成 ZIP 文件,而后再把生成的 ZIP 文件上传到服务器。一样,利用 JSZip 这个库,咱们能够实如今客户端同时下载多个文件,而后把已下载的文件压缩成 Zip 包,并下载到本地的功能。对应的操做流程以下图所示:
在以上 Gif 图中,阿宝哥演示了把 3 张素材图,打包成 Zip 文件并下载到本地的过程。接下来,咱们来介绍如何使用 JSZip 这个库实现以上的功能。
html
<h3>Zip 下载示例</h3>
<div>
<img src="../images/body.png" />
<img src="../images/eyes.png" />
<img src="../images/mouth.png" />
</div>
<button onclick="download()">打包下载</button>
复制代码
js
const images = ["body.png", "eyes.png", "mouth.png"];
const imageUrls = images.map((name) => "../images/" + name);
async function download() {
let zip = new JSZip();
Promise.all(imageUrls.map(getFileContent)).then((contents) => {
contents.forEach((content, i) => {
zip.file(images[i], content);
});
zip.generateAsync({ type: "blob" }).then(function (blob) {
saveAs(blob, "material.zip");
});
});
}
// 从指定的url上下载文件内容
function getFileContent(fileUrl) {
return new JSZip.external.Promise(function (resolve, reject) {
// 调用jszip-utils库提供的getBinaryContent方法获取文件内容
JSZipUtils.getBinaryContent(fileUrl, function (err, data) {
if (err) {
reject(err);
} else {
resolve(data);
}
});
});
}
复制代码
在以上代码中,当用户点击 打包下载 按钮时,就会调用 download
函数。在该函数内部,会先调用 JSZip
构造函数建立 JSZip
对象,而后使用 Promise.all 函数来确保全部的文件都下载完成后,再调用 file(name, data [,options])
方法,把已下载的文件添加到前面建立的 JSZip
对象中。最后经过 zip.generateAsync
函数来生成 Zip 文件并使用 FileSaver.js 提供的 saveAs
方法保存 Zip 文件。
Zip 下载示例:Zip
在服务端下载的场景中,附件形式下载是一种比较常见的场景。在该场景下,咱们经过设置 Content-Disposition
响应头来指示响应的内容以何种形式展现,是之内联(inline)的形式,仍是以附件(attachment)的形式下载并保存到本地。
Content-Disposition: inline
Content-Disposition: attachment
Content-Disposition: attachment; filename="mouth.png"
复制代码
而在 HTTP 表单的场景下, Content-Disposition
也能够做为 multipart body 中的消息头:
Content-Disposition: form-data
Content-Disposition: form-data; name="fieldName"
Content-Disposition: form-data; name="fieldName"; filename="filename.jpg"
复制代码
第 1 个参数老是固定不变的 form-data
;附加的参数不区分大小写,而且拥有参数值,参数名与参数值用等号(=
)链接,参数值用双引号括起来。参数之间用分号(;
)分隔。
了解完 Content-Disposition
的做用以后,咱们来看一下如何实现以附件形式下载的功能。Koa 是一个简单易用的 Web 框架,它的特色是优雅、简洁、轻量、自由度高。因此咱们选择它来搭建文件服务,并使用 @koa/router 中间件来处理路由:
// attachment/file-server.js
const fs = require("fs");
const path = require("path");
const Koa = require("koa");
const Router = require("@koa/router");
const app = new Koa();
const router = new Router();
const PORT = 3000;
const STATIC_PATH = path.join(__dirname, "./static/");
// http://localhost:3000/file?filename=mouth.png
router.get("/file", async (ctx, next) => {
const { filename } = ctx.query;
const filePath = STATIC_PATH + filename;
const fStats = fs.statSync(filePath);
ctx.set({
"Content-Type": "application/octet-stream",
"Content-Disposition": `attachment; filename=${filename}`,
"Content-Length": fStats.size,
});
ctx.body = fs.createReadStream(filePath);
});
// 注册中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
// ENOENT(无此文件或目录):一般是由文件操做引发的,这代表在给定的路径上没法找到任何文件或目录
ctx.status = error.code === "ENOENT" ? 404 : 500;
ctx.body = error.code === "ENOENT" ? "文件不存在" : "服务器开小差";
}
});
app.use(router.routes()).use(router.allowedMethods());
app.listen(PORT, () => {
console.log(`应用已经启动:http://localhost:${PORT}/`);
});
复制代码
以上的代码被保存在 attachment
目录下的 file-server.js
文件中,该目录下还有一个 static
子目录用于存放静态资源。目前 static
目录下包含如下 3 个 png 文件。
├── file-server.js
└── static
├── body.png
├── eyes.png
└── mouth.png
复制代码
当你运行 node file-server.js
命令成功启动文件服务器以后,就能够经过正确的 URL 地址来下载 static
目录下的文件。好比在浏览器中打开 http://localhost:3000/file?filename=mouth.png
这个地址,你就会开始下载 mouth.png
文件。而若是指定的文件不存在的话,就会返回文件不存在。
Koa 内核很简洁,扩展功能都是经过中间件来实现。好比经常使用的路由、CORS、静态资源处理等功能都是经过中间件实现。所以要想掌握 Koa 这个框架,核心是掌握它的中间件机制。若你想深刻了解 Koa 的话,能够阅读 如何更好地理解中间件和洋葱模型 这篇文章。
在编写 HTML 网页时,对于一些简单图片,一般会选择将图片内容直接内嵌在网页中,从而减小没必要要的网络请求,可是图片数据是二进制数据,该怎么嵌入呢?绝大多数现代浏览器都支持一种名为 Data URLs 的特性,容许使用 Base64 对图片或其余文件的二进制数据进行编码,将其做为文本字符串嵌入网页中。因此文件也能够经过 Base64 的格式进行传输,接下来咱们将介绍如何下载 Base64 格式的图片。
附件形式下载示例:attachment
Base64 是一种基于 64 个可打印字符来表示二进制数据的表示方法。因为 2⁶ = 64 ,因此每 6 个比特为一个单元,对应某个可打印字符。3 个字节有 24 个比特,对应于 4 个 base64 单元,即 3 个字节可由 4 个可打印字符来表示。相应的转换过程以下图所示:
Base64 经常使用在处理文本数据的场合,表示、传输、存储一些二进制数据,包括 MIME 的电子邮件及 XML 的一些复杂数据。 在 MIME 格式的电子邮件中,base64 能够用来将二进制的字节序列数据编码成 ASCII 字符序列构成的文本。使用时,在传输编码方式中指定 base64。使用的字符包括大小写拉丁字母各 26 个、数字 10 个、加号 + 和斜杠 /,共 64 个字符,等号 = 用来做为后缀用途。
Base64 的相关内容就先介绍到这,若是你想进一步了解 Base64 的话,能够阅读 一文读懂base64编码 这篇文章。下面咱们来看一下具体实现代码:
html
在如下 HTML 代码中,咱们经过 select
元素来让用户选择要下载的图片。当用户切换不一样的图片时,img#imgPreview
元素中显示的图片会随之发生变化。
<h3>base64 下载示例</h3>
<img id="imgPreview" src="./static/body.png" />
<select id="picSelect">
<option value="body">body.png</option>
<option value="eyes">eyes.png</option>
<option value="mouth">mouth.png</option>
</select>
<button onclick="download()">下载</button>
复制代码
js
const picSelectEle = document.querySelector("#picSelect");
const imgPreviewEle = document.querySelector("#imgPreview");
picSelectEle.addEventListener("change", (event) => {
imgPreviewEle.src = "./static/" + picSelectEle.value + ".png";
});
const request = axios.create({
baseURL: "http://localhost:3000",
timeout: 60000,
});
async function download() {
const response = await request.get("/file", {
params: {
filename: picSelectEle.value + ".png",
},
});
if (response && response.data && response.data.code === 1) {
const fileData = response.data.data;
const { name, type, content } = fileData;
const imgBlob = base64ToBlob(content, type);
saveAs(imgBlob, name);
}
}
复制代码
在用户选择好须要下载的图片并点击下载按钮时,就会调用以上代码中的 download
函数。在该函数内部,咱们利用 axios 实例的 get
方法发起 HTTP 请求来获取指定的图片。由于返回的是 base64 格式的图片,因此在调用 FileSaver 提供的 saveAs
方法前,咱们须要将 base64 字符串转换成 blob 对象,该转换是经过如下的 base64ToBlob
函数来完成,该函数的具体实现以下所示:
function base64ToBlob(base64, mimeType) {
let bytes = window.atob(base64);
let ab = new ArrayBuffer(bytes.length);
let ia = new Uint8Array(ab);
for (let i = 0; i < bytes.length; i++) {
ia[i] = bytes.charCodeAt(i);
}
return new Blob([ab], { type: mimeType });
}
复制代码
// base64/file-server.js
const fs = require("fs");
const path = require("path");
const mime = require("mime");
const Koa = require("koa");
const cors = require("@koa/cors");
const Router = require("@koa/router");
const app = new Koa();
const router = new Router();
const PORT = 3000;
const STATIC_PATH = path.join(__dirname, "./static/");
router.get("/file", async (ctx, next) => {
const { filename } = ctx.query;
const filePath = STATIC_PATH + filename;
const fileBuffer = fs.readFileSync(filePath);
ctx.body = {
code: 1,
data: {
name: filename,
type: mime.getType(filename),
content: fileBuffer.toString("base64"),
},
};
});
// 注册中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
ctx.body = {
code: 0,
msg: "服务器开小差",
};
}
});
app.use(cors());
app.use(router.routes()).use(router.allowedMethods());
app.listen(PORT, () => {
console.log(`应用已经启动:http://localhost:${PORT}/`);
});
复制代码
在以上代码中,对图片进行 Base64 编码的操做是定义在 /file
路由对应的路由处理器中。当该服务器接收到客户端发起的文件下载请求,好比 GET /file?filename=body.png HTTP/1.1
时,就会从 ctx.query
对象上获取 filename
参数。该参数表示文件的名称,在获取到文件的名称以后,咱们就能够拼接出文件的绝对路径,而后经过 Node.js 平台提供的 fs.readFileSync
方法读取文件的内容,该方法会返回一个 Buffer 对象。在成功读取文件的内容以后,咱们会继续调用 Buffer 对象的 toString
方法对文件内容进行 Base64 编码,最终所下载的图片将以 Base64 格式返回到客户端。
base64 格式下载示例:base64
分块传输编码主要应用于以下场景,即要传输大量的数据,可是在请求在没有被处理完以前响应的长度是没法得到的。例如,当须要用从数据库中查询得到的数据生成一个大的 HTML 表格的时候,或者须要传输大量的图片的时候。
要使用分块传输编码,则须要在响应头配置 Transfer-Encoding
字段,并设置它的值为 chunked
或 gzip, chunked
:
Transfer-Encoding: chunked
Transfer-Encoding: gzip, chunked
复制代码
响应头 Transfer-Encoding
字段的值为 chunked
,表示数据以一系列分块的形式进行发送。须要注意的是 Transfer-Encoding
和 Content-Length
这两个字段是互斥的,也就是说响应报文中这两个字段不能同时出现。下面咱们来看一下分块传输的编码规则:
\r\n
结尾;\r\n
结尾,但数据不包含 \r\n
;0\r\n\r\n
。了解完分块传输的编码规则,咱们来看如何利用分块传输编码实现文件下载。
html5
<h3>chunked 下载示例</h3>
<button onclick="download()">下载</button>
复制代码
js
const chunkedUrl = "http://localhost:3000/file?filename=file.txt";
function download() {
return fetch(chunkedUrl)
.then(processChunkedResponse)
.then(onChunkedResponseComplete)
.catch(onChunkedResponseError);
}
function processChunkedResponse(response) {
let text = "";
let reader = response.body.getReader();
let decoder = new TextDecoder();
return readChunk();
function readChunk() {
return reader.read().then(appendChunks);
}
function appendChunks(result) {
let chunk = decoder.decode(result.value || new Uint8Array(), {
stream: !result.done,
});
console.log("已接收到的数据:", chunk);
console.log("本次已成功接收", chunk.length, "bytes");
text += chunk;
console.log("目前为止共接收", text.length, "bytes\n");
if (result.done) {
return text;
} else {
return readChunk();
}
}
}
function onChunkedResponseComplete(result) {
let blob = new Blob([result], {
type: "text/plain;charset=utf-8",
});
saveAs(blob, "hello.txt");
}
function onChunkedResponseError(err) {
console.error(err);
}
复制代码
当用户点击 下载 按钮时,就会调用以上代码中的 download
函数。在该函数内部,咱们会使用 Fetch API 来执行下载操做。由于服务端的数据是以一系列分块的形式进行发送,因此在浏览器端咱们是经过流的形式进行接收。即经过 response.body
获取可读的 ReadableStream,而后用 ReadableStream.getReader()
建立一个读取器,最后调用 reader.read
方法来读取已返回的分块数据。
由于 file.txt
文件的内容是普通文本,且 result.value
的值是 Uint8Array 类型的数据,因此在处理返回的分块数据时,咱们使用了 TextDecoder 文本解码器。一个解码器只支持一种特定文本编码,例如 utf-8
、iso-8859-2
、koi8
、cp1261
,gbk
等等。
若是收到的分块非 终止块,result.done
的值是 false
,则会继续调用 readChunk
方法来读取分块数据。而当接收到 终止块 以后,表示分块数据已传输完成。此时,result.done
属性就会返回 true
。从而会自动调用 onChunkedResponseComplete
函数,在该函数内部,咱们以解码后的文本做为参数来建立 Blob 对象。以后,继续使用 FileSaver 库提供的 saveAs
方法实现文件下载。
这里咱们用 Wireshark 网络包分析工具,抓了个数据包。具体以下图所示:
从图中咱们能够清楚地看到在 HTTP chunked response 下面包含了 Data chunk(数据块) 和 End of chunked encoding(终止块)。接下来,咱们来看一下服务端的代码。
const fs = require("fs");
const path = require("path");
const Koa = require("koa");
const cors = require("@koa/cors");
const Router = require("@koa/router");
const app = new Koa();
const router = new Router();
const PORT = 3000;
router.get("/file", async (ctx, next) => {
const { filename } = ctx.query;
const filePath = path.join(__dirname, filename);
ctx.set({
"Content-Type": "text/plain;charset=utf-8",
});
ctx.body = fs.createReadStream(filePath);
});
// 注册中间件
app.use(async (ctx, next) => {
try {
await next();
} catch (error) {
// ENOENT(无此文件或目录):一般是由文件操做引发的,这代表在给定的路径上没法找到任何文件或目录
ctx.status = error.code === "ENOENT" ? 404 : 500;
ctx.body = error.code === "ENOENT" ? "文件不存在" : "服务器开小差";
}
});
app.use(cors());
app.use(router.routes()).use(router.allowedMethods());
app.listen(PORT, () => {
console.log(`应用已经启动:http://localhost:${PORT}/`);
});
复制代码
在 /file
路由处理器中,咱们先经过 ctx.query
得到 filename
文件名,接着拼接出该文件的绝对路径,而后经过 Node.js 平台提供的 fs.createReadStream
方法建立可读流。最后把已建立的可读流赋值给 ctx.body
属性,从而向客户端返回图片数据。
如今咱们已经知道能够利用分块传输编码(Transfer-Encoding)实现数据的分块传输,那么有没有办法获取指定范围内的文件数据呢?对于这个问题,咱们能够利用 HTTP 协议的范围请求。接下来,咱们将介绍如何利用 HTTP 范围请求来下载指定范围的数据。
chunked 下载示例:chunked
HTTP 协议范围请求容许服务器只发送 HTTP 消息的一部分到客户端。范围请求在传送大的媒体文件,或者与文件下载的断点续传功能搭配使用时很是有用。若是在响应中存在 Accept-Ranges
首部(而且它的值不为 “none”),那么表示该服务器支持范围请求。
在一个 Range 首部中,能够一次性请求多个部分,服务器会以 multipart 文件的形式将其返回。若是服务器返回的是范围响应,须要使用 206 Partial Content 状态码。假如所请求的范围不合法,那么服务器会返回 416 Range Not Satisfiable 状态码,表示客户端错误。服务器容许忽略 Range 首部,从而返回整个文件,状态码用 200 。
Range 语法:
Range: <unit>=<range-start>-
Range: <unit>=<range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>
Range: <unit>=<range-start>-<range-end>, <range-start>-<range-end>, <range-start>-<range-end>
复制代码
unit
:范围请求所采用的单位,一般是字节(bytes)。<range-start>
:一个整数,表示在特定单位下,范围的起始值。<range-end>
:一个整数,表示在特定单位下,范围的结束值。这个值是可选的,若是不存在,表示此范围一直延伸到文档结束。了解完 Range
语法以后,咱们来看一下实际的使用示例:
# 单一范围
$ curl http://i.imgur.com/z4d4kWk.jpg -i -H "Range: bytes=0-1023"
# 多重范围
$ curl http://www.example.com -i -H "Range: bytes=0-50, 100-150"
复制代码
html
<h3>范围下载示例</h3>
<button onclick="download()">下载</button>
复制代码
js
async function download() {
try {
let rangeContent = await getBinaryContent(
"http://localhost:3000/file.txt",
0, 100, "text"
);
const blob = new Blob([rangeContent], {
type: "text/plain;charset=utf-8",
});
saveAs(blob, "hello.txt");
} catch (error) {
console.error(error);
}
}
function getBinaryContent(url, start, end, responseType = "arraybuffer") {
return new Promise((resolve, reject) => {
try {
let xhr = new XMLHttpRequest();
xhr.open("GET", url, true);
xhr.setRequestHeader("range", `bytes=${start}-${end}`);
xhr.responseType = responseType;
xhr.onload = function () {
resolve(xhr.response);
};
xhr.send();
} catch (err) {
reject(new Error(err));
}
});
}
复制代码
当用户点击 下载 按钮时,就会调用 download
函数。在该函数内部会经过调用 getBinaryContent
函数来发起范围请求。对应的 HTTP 请求报文以下所示:
GET /file.txt HTTP/1.1
Host: localhost:3000
Connection: keep-alive
User-Agent: Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.114 Safari/537.36
Accept: */*
Accept-Encoding: identity
Accept-Language: zh-CN,zh;q=0.9,en;q=0.8,id;q=0.7
Range: bytes=0-100
复制代码
而当服务器接收到该范围请求以后,会返回对应的 HTTP 响应报文:
HTTP/1.1 206 Partial Content
Vary: Origin
Access-Control-Allow-Origin: null
Accept-Ranges: bytes
Last-Modified: Fri, 09 Jul 2021 00:17:00 GMT
Cache-Control: max-age=0
Content-Type: text/plain; charset=utf-8
Date: Sat, 10 Jul 2021 02:19:39 GMT
Connection: keep-alive
Content-Range: bytes 0-100/2590
Content-Length: 101
复制代码
从以上的 HTTP 响应报文中,咱们见到了前面介绍的 206 状态码和 Accept-Ranges 首部。此外,经过 Content-Range
首部,咱们就知道了文件的总大小。在成功获取到范围请求的响应体以后,咱们就可使用返回的内容做为参数,调用 Blob 构造函数建立对应的 Blob 对象,进而使用 FileSaver 库提供的 saveAs 方法来下载文件了。
const Koa = require("koa");
const cors = require("@koa/cors");
const serve = require("koa-static");
const range = require("koa-range");
const PORT = 3000;
const app = new Koa();
// 注册中间件
app.use(cors());
app.use(range);
app.use(serve("."));
app.listen(PORT, () => {
console.log(`应用已经启动:http://localhost:${PORT}/`);
});
复制代码
服务端的代码相对比较简单,范围请求是经过 koa-range 中间件来实现的。因为篇幅有限,阿宝哥就不展开介绍了。感兴趣的小伙伴,能够自行阅读该中间件的源码。其实范围请求还能够应用在大文件下载的场景,若是文件服务器支持范围请求的话,客户端在下载大文件的时候,就能够考虑使用大文件分块下载的方案。
范围下载示例:range
相信有些小伙伴已经了解大文件上传的解决方案,在上传大文件时,为了提升上传的效率,咱们通常会使用 Blob.slice 方法对大文件按照指定的大小进行切割,而后在开启多线程进行分块上传,等全部分块都成功上传后,再通知服务端进行分块合并。
那么对大文件下载来讲,咱们可否采用相似的思想呢?其实在服务端支持 Range
请求首部的条件下,咱们也是能够实现大文件分块下载的功能,具体处理方案以下图所示:
由于在 JavaScript 中如何实现大文件并发下载? 这篇文章中,阿宝哥已经详细介绍了大文件并发下载的方案,因此这里就不展开介绍了。咱们只回顾一下大文件并发下载的完整流程:
其实在大文件分块下载的场景中,咱们使用了 async-pool 这个库来实现并发控制。该库提供了 ES7 和 ES6 两种不一样版本的实现,代码很简洁优雅。若是你想了解 async-pool 是如何实现并发控制的,能够阅读 JavaScript 中如何实现并发控制? 这篇文章。
大文件分块下载示例:big-file
本文阿宝哥详细介绍了文件下载的 9 种场景,但愿阅读完本文后,你对 9 种场景背后使用的技术有必定的了解。其实在传输文件的过程当中,为了提升传输效率,咱们可使用 gzip
、deflate
或 br
等压缩算法对文件进行压缩。因为篇幅有限,阿宝哥就不展开介绍了,若是你感兴趣的话,能够阅读 HTTP 传输大文件的几种方案 这篇文章。
有了文件下载的场景,怎么能缺乏文件上传的场景呢?若是你还没阅读过 文件上传,搞懂这 8 种场景就够了 这篇文章,建议你有空的时候,能够一块儿了解一下。这里再次感谢掘友们一直以来的支持,若是大家还想了解其余方面的内容,欢迎给阿宝哥留言哟。