前置知识点
几个能触发浏览器自动创建文件并下载的 API
ini
代码解读
复制代码
location.href; window.open; iframe.src; a[download].click();
ServiceWorker
作用
用于浏览器代理,可以修改请求和响应,可以离线使用。
事件
发布事件
javascript
代码解读
复制代码
postMessage(message, transfer);
参数名 | 作用 | 类型 |
---|---|---|
message | 消息内容,会使用深复制来传递 | any |
transfer | 可转移对象数组,e.g. [port2] | Array |
订阅事件
onmessage
或者 addEventListener('message')
注册
使用 navigator.serviceWorker.register
来注册。
e.g.
javascript
代码解读
复制代码
navigator.serviceWorker.register("sw.js", { scope: "./" });
拦截
在 serviceWorker
中监听 message
事件,事件回调中使用以下代码
javascript
代码解读
复制代码
event.respondWith(new Response(stream, { headers: responseHeaders }));
MessageChannel
用于在两个不同的浏览器上下文中进行通信。
创建:
javascript
代码解读
复制代码
const channel = new MessageChannel();
对象中有 port1
和 port2
两个 MessagePort,可以通过 addEventListener('message',()=>{})
或者 onmessage
来订阅 message
事件
StreamSaver 的基本使用
使用 StreamSaver 创建一个 WritableStream
,然后将 fetch
返回的 ReadableStream
连接到 WritableStream
javascript
代码解读
复制代码
import streamSaver from "streamsaver"; const downloadStreamSingle = async () => { const res = await fetch(`/rest/download/video`); const fileStream = streamSaver.createWriteStream("video.mp4"); await res.body.pipeTo(fileStream); };
StreamSaver 的实现方案
总体思路
- 前端传入文件名
- 前端打开
iframe/popup
(https
下使用iframe
,http
下使用popup
) iframe/popup
注册ServiceWorker
,并绑定MessageChannel.port
(用于前端和ServiceWorker
之间的通信)iframe/popup
加载完毕后,前端把文件名、随机数发送到iframe/popup
,iframe/popup
再发送到ServiceWorker
ServiceWorker
端根据文件名和随机数,动态生成一个 URL(URL 和ServiceWorker
在同一个域下),并绑定 URL 和MessageChannel.port
、ReadableStream
之间的关联,供下次请求 URL 时取用ServiceWorker
向前端返回 URL- 前端关闭
iframe/popup
,并使用location.href
请求 URL ServiceWorker
接收到 URL 的请求,拦截该请求,返回上次保存的ReadableStream
- 前端将接口返回的
readableStream
, 用pipeTo
连接到创建的writableStream
,实现流式下载
模块
分为 3 部分
- StreamSaver.js:前端部分
- mitm.html:通过
iframe/popup
打开的页面,用于注册ServiceWorker
- sw.js:
ServiceWorker
部分,用于拦截请求,并返回流式响应
画成顺序图,如下:
StreamSaver 的执行过程
StreamSaver.js
当引入了 StreamSaver
后,就会执行以下代码用于初始化全局变量
以下代码用于判断浏览器环境是否支持 ServiceWorker
,如果不支持,后面会降级成非流式保存文件(当整个文件内容返回完毕后,才保存文件)
这段代码用于判断浏览器环境是否支持 TransformStream
,为变量 supportsTransferable
赋值。supportsTransferable
的值会作为对象属性发送给ServiceWorker
。
当需要保存文件时,就调用 createWriteStream
入口函数
函数在初始化了几个变量后,就通过调用 loadTransporter
函数来创建 iframe/popup
为了在前端和 iframe/popup 之间通信,还要创建 MessageChannel
,MessageChannel
有 port1 和 port2,port1 指代浏览器端的 StreamSaver.js
,port2 指代 iframe/popup 打开的 mitm.html
进入 loadTransporter
函数体,发现使用 isSecureContext
来判断使用 iframe
还是 popup
我们顺便进入 makePopup
函数体内
其实就是用 createDocumentFragment
,传入 mitm.html
的 URL 来打开一个 popup
形式的页面,并订阅 message
事件,在回调中向 popup
对象发送 load
事件
我们说回 createWriteStream 函数的主逻辑。接下来根据文件名和随机数,创建一个 pathname 路径,和 supportsTransferable 的值一起,打包到一个对象中,稍后会发送。
接下来会创建 TransformStream
,并发送 transformStream.readable
port1 订阅 message 事件,事件回调的逻辑接下来会讲解。
判断 iframe/popup 是否已加载,如果已加上,就发送 message
。
如果还没加载,就添加 load
事件回调,在回调中向 iframe/popup 发送 message
。
message 事件的参数就是上面创建的 response 对象,包括 transferringReadable
,pathname
和 headers
使用 popup/iframe
的方式打开 mimt.html
,向主页面发送事件
整个 createWriteStream
函数的主要逻辑就到此为止了,接下来就返回 transformStream.writable 流。
如果浏览器不支持 MessageChannel 咋办呢?就返回原生的 WritableStream。
效果就是,等待整个文件的内容都返回了,才保存成文件。
mitm.html
刚才打开 popup 时,加载了 mitm.html
后会自动执行里面script
标签中的 js
下面的代码会向原页面(打开 popup 的页面)发送 message
事件,表示 popup 已加载。
sw.js 注册成 ServiceWorker
,然后绑定 message
回调
StreamSaver.js 在收到 StreamSaver::loadedPopup
事件后,向 popup
对象发送 load
事件
注:popup
变量是创建 popup 时,使用 createDocumentFragment
返回的对象
Popup
对象收到load
事件后,向mitm.html
发送message
事件,参数是 pathname 等
Mitm.html
Mitm.html
收到message
事件后,向 ServiceWorker
发送 message
事件
sw.js
sw.js 收到 message
事件后,做 2 件事:
- 保存
downloadUrl
和stream
, - 向 port1(StreamSaver.js) 发送
message
事件,参数是downloadUrl
如果 transferringReadable
为 true
,就绑定 port
的message
事件,在回调中设置readableStream
port 收到message
事件时,设置 readableStream
的值,供下面返回
StreamSaver.js
port1 收到 downloadUrl 后,就使用 location.href
向 ServiceWorker
所在的域发出请求
downloadUrl
是在 sw.js 生成的,作用是关闭 popup
后再次请求(ServiceWorker
只能拦截注册后接收到的请求,所以需要在前端再次发送请求),触发 ServiceWorker
返回 readableStream
jimmywarting.github.io/StreamSaver…
请求发送到 ServiceWorker
,sw.js 根据 url 找到上次保存的 readableStream
sw.js 返回stream
当响应内容返回到浏览器时,由于使用 location.href
来请求,所以自动触发浏览器的下载动作。会创建一个文件,然后流式下载。
当文件内容返回完毕后,文件就下载完成。
思考几个问题
为什么前端请求 URL,就能自动触发下载文件?
- URL 被
ServiceWorker
拦截,并没有发送到服务端 ServiceWorker
拦截到请求后,返回了ReadableStream
- 由于采用
location.href
来发起请求,所以可以触发浏览器默认的下载行为
为什么要创建 TransformStream?
个人认为,这是为了流式下载,可以一边读,一边写
为什么有了 TransformStream 后,还需要 ServiceWorker?
需要建立一个代理,请求 URL,返回服务端的流
需要通过 location.href
来触发浏览器的下载行为
为什么关闭了 iframe/popup
后,ServiceWorker
还能继续代理请求?
ServiceWorker
注册后,下次请求才能拦截,本次请求不会拦截
ServiceWorker
和页面状态无关,页面关闭不会影响到 ServiceWorker
的状态
能不能直接创建 TransformStream,然后用 pipeTo 连接到 fetch 的 readableStream(不用 ServiceWorker)?
本人还没尝试过这种办法,但是估计不会触发浏览器的下载行为
还有其他流式下载的方案吗?
头脑风暴想到的可能方案是:
使用 location.href 触发浏览器创建文件,拿到这个文件的 stream,然后使用 pipeTo 往 stream 中写入内容
由于还没找到获取浏览器创建的文件流的 API,所以不确定此方案是否可行。
原文链接:https://juejin.cn/post/7416903220591575078