手记

StreamSaver.js入门教程:优雅解决前端下载文件的难题

本文简介

点赞 + 关注 + 收藏 = 学会了


本文介绍一个能让前端优雅下载大文件的工具:StreamSaver.js


StreamSaver.js 可用于实现在Web浏览器中直接将大文件流式传输到用户设备的功能。

传统的下载方式可能导致大文件的加载时间较长或造成内存占用过大的问题,使用 <a> 标签打开新页面下载文件,遇到 .txt 或者 .mp4 之类的文件可能就直接在页面展示了,不会触发下载功能。而 StreamSaver.js 则通过流式下载的方式解决了这些问题。

StreamSaver.js 将大文件拆分成小块,并在下载过程中逐块传输到硬盘,从而降低内存占用和提高下载速度。



环境准备

要学习 StreamSaver.js 首先要准备一份或者多份可下载的文件。

你可以使用网络上的文件资源,但这需要你自己去找。

你也可以在自己的电脑运行个服务,把文件资源丢进去即可。


如果你用脚手架创建项目,比如vue或者react之类的项目,也可以把文件放在静态资源目录里。

比如用 vite 创建一个 Vue 项目,然后在 public 目录下创建一个 test.txt 文件。项目运行起来,在浏览器访问 http://localhost:端口号/public/test.txt 就能查看到这个文件内容。



安装 StreamSaver.js

可以使用 CDN 或者 npm 安装 StreamSaver.js

本文使用 CDN 的方式讲解。


CDN

StreamSaver.js 文件下载到你项目里引入即可。

<script src="../StreamSaver.js"></script>

npm


使用以下命令下载 StreamSaver 到项目里

npm i streamsaver

然后在要使用的地方引入即可。

import streamSaver from "streamsaver"


起步

起步阶段,我们先试试如何下载一个 .txt 文件。

如果我们要下载一些浏览器读不懂的文件,我们可以使用 <a>标签在新窗口打开链接,也可以使用 windows.open('url') 的方式打开新窗口进行下载。

但如果这个文件浏览器是读得懂的,比如 .txt 文件,那浏览器就不会执行下载,而是会直接在页面中把文件内容展示出来。

此时就可以使用 StreamSaver.js 来解决这个问题。

使用 StreamSaver.js 下载文件的大概流程是这样的(为了方便理解,我用一些不专业的术语进行描述):

  1. 创建一个文件,该文件支持写入操作。streamSaver.createWriteStream('文件名.后缀')
  2. 使用 fetch 方法访问文件的url,将内容一点点的放到 StreamSaver 创建的文件里。
  3. 监听文件内容是否读取完整,读取完就执行“保存并关闭文件”的操作。

根据上面的指引编写代码:

<!-- 下载按钮 -->
<button id="download">下载</button>

<!-- 引入StreamSaver.js -->
<script src="../StreamSaver.js"></script>
<script>
  // 监听按钮点击事件,点击就下载文件
  download.onclick = () => {
    // 【步骤1】创建一个文件,该文件支持写入操作
    const fileStream = streamSaver.createWriteStream('test.txt') // 这里传入的是下载后的文件名,这个名字可以自定义
    
    // 【步骤2】使用 fetch 方法访问文件的url,将内容一点点的放到 StreamSaver 创建的文件里
    fetch('http://localhost:9988/public/test.txt')
      .then(res => {
        const readableStream = res.body
        if (window.WritableStream && readableStream.pipeTo) {
          return readableStream.pipeTo(fileStream)
            .then(() => console.log('完成写入'))
        }

        // 【步骤3】监听文件内容是否读取完整,读取完就执行“保存并关闭文件”的操作。
        window.writer = fileStream.getWriter()
        const reader = res.body.getReader()
        const pump = () => reader.read()
          .then(res => res.done
            ? writer.close()
            : writer.write(res.value).then(pump)
          )
        pump()
      })
  }
</script>

大概就是这样子了。


🔔提示

如果遇到提示跨域的问题,可以配置 mitm 指向 mitm.html

mitm.htmlStreamSaver.js仓库 里可以获取到。

可以把 mitm.html 放到你服务器再配置。

streamSaver.mitm = 'https://你的服务器地址/mitm.html'


打包下载 zip

如果想将多个文件打包成zip下载到本地,可以将 StreamSaver.jszip-stream.js 结合在一起使用。

zip-stream.jsStreamSaver.js 的仓库里有。

zip-stream.js/examples 目录里。


使用 npm 安装 streamsaver 也能在 /examples 目录下找到 zip-stream.js,直接引入即可。

import 'node_modules/streamsaver/examples/zip-stream.js'

如果使用 CDN 的方式就直接用 <script src='zip-stream.js'></script> 引入即可。


打包zip下载的步骤:

  1. 创建下载后的文件名和文件格式。
  2. 使用 zip-stream 创建一个 ZIP 实例,用来不断接收要下载的文件。
  3. 所有文件下载完成就执行 close() 方法将所有文件真正打包成一个 zip

<button id="download">下载</button>

<script src="../StreamSaver.js"></script>
<script src="zip-stream.js"></script>
<script>

	// 要下载的文件地址列表
	let urls = [
		{
			fileName: 'test.txt',
			url: 'http://localhost:9988/public/test.txt',
		},
		{
			fileName: 'test.csv',
			url: 'http://localhost:9988/public/test.csv',
		}
	]

	download.onclick = () => {
		// 【步骤1】
		const fileStream = streamSaver.createWriteStream('test.zip')

		// 【步骤2】
		const readableZipStream = new ZIP({
			async pull(ctrl) {
				for (let i = 0; i < urls.length; i++) {
					const res = await fetch(urls[i].url)
					const stream = () => res.body
					const name = urls[i].fileName
					ctrl.enqueue({ name, stream }) // 不断接收要下载的文件
				}

				// 【步骤3】
				ctrl.close()
			}
		})

		if (window.WritableStream && readableZipStream.pipeTo) {
			return readableZipStream.pipeTo(fileStream).then(
				() => console.log('下载完了')
			)
		}
	}
	
</script>

点击下载按钮后的效果:

这个例子准备了 .csv.txt 文件。下载时会合并成 .zip,解压后能看到里面的所有文件都是正常能打开的 。



合成文件再下载

在这个例子中,我要将2个 .csv 文件合并成1个再下载。

我准备了两个 .csv 文件(test1.csv 和 test2.csv),它们的内容分别长这个样子。

我要将它们合并成这样子:


在合并文件之前我们首先要清楚这个文件的内容是如何组成的。

在 Excel 中打开 .csv 的每个单元格的内容转换成文本形式的话是用逗号分隔。

如果要合并多个 .csv 文件,只需监听到每个 .csv 下载完成,然后再拼接一个 \n 换行,再下载下一个 .csv 文件即可。


整理一下就是以下几个步骤:

  1. 拿到一组下载地址,把它们转存到一个迭代器里。
  2. 递归执行迭代器,如果迭代器里还有内容,就使用 fetch 请求数据。
  3. 如果迭代器没内容了,使用 writer.close() 关闭文件写入。

该功能写成真正的代码如下所示:

<button id="download" onclick="down()">下载</button>

<script src="../StreamSaver.js"></script>
<script>
  // 编码转换方法
  let encode = TextEncoder.prototype.encode.bind(new TextEncoder)

  // 准备好要下载的链接
  const urls = [
    'http://localhost:9988/public/test1.csv',
    'http://localhost:9988/public/test2.csv'
  ]

  // 迭代器数据
  let urlsIter = null

  // 写入方法放到全局中保存
  let writer = null

  // 下载按钮点击事件
  function down() {
    // 创建一个下载管道,并将下载后的文件命名为 newTest.csv
    const fileStream = streamSaver.createWriteStream('newTest.csv')
    // 创建写入方法
    writer = fileStream.getWriter()
    // 将要下载的链接转换成迭代器
    urlsIter = urls[Symbol.iterator]()

    // 开始执行循环下载
    forDown()
  }

  // 循环下载的方法
  async function forDown() {
    // 获取迭代器最新一条数据
    let urlIter = urlsIter.next()
    // 如果迭代器没数据,执行写入完成操作,并停止递归
    if (urlIter.done) {
      writer.close()
      return
    }

    // 迭代器有内容时执行请求操作
    await fetch(urlIter.value)
      .then(res => {
        // 通过请求文件url获取到的数据
        const readableStream = res.body
        if (window.WritableStream && readableStream.pipeTo) {
          const reader = readableStream.getReader()
          // 讲获取到的每一包写入文件里
          const pump = () => {
            return reader.read()
              .then(readRes => {
                if (readRes.done) { // 当前文件读取完成后执行
                  // 文件读取完成后换行
                  writer.write(encode('\n'))
                  // 执行请求下一个文件
                  forDown()
                } else { // 文件读取过程执行
                  // 一包包写入
                  writer.write(readRes.value)
                    .then(pump)
                }
            })
          }
          pump()
        }
      })
  }
</script>

这个案例稍微复杂一丢丢,建议跟着手敲一遍。



点赞 + 关注 + 收藏 = 学会了
代码仓库

0人推荐
随时随地看视频
慕课网APP