StreamSaver.js执行过程分析

149 篇文章 0 订阅
149 篇文章 0 订阅

前置知识点

几个能触发浏览器自动创建文件并下载的 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();

对象中有 port1port2 两个 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 的实现方案

总体思路

  1. 前端传入文件名
  2. 前端打开 iframe/popuphttps下使用iframehttp下使用popup
  3. iframe/popup 注册 ServiceWorker,并绑定 MessageChannel.port(用于前端和 ServiceWorker 之间的通信)
  4. iframe/popup 加载完毕后,前端把文件名、随机数发送到 iframe/popup, iframe/popup 再发送到 ServiceWorker
  5. ServiceWorker 端根据文件名和随机数,动态生成一个 URL(URL 和 ServiceWorker 在同一个域下),并绑定 URL 和 MessageChannel.portReadableStream 之间的关联,供下次请求 URL 时取用
  6. ServiceWorker 向前端返回 URL
  7. 前端关闭 iframe/popup,并使用 location.href 请求 URL
  8. ServiceWorker 接收到 URL 的请求,拦截该请求,返回上次保存的 ReadableStream
  9. 前端将接口返回的 readableStream, 用 pipeTo 连接到创建的 writableStream,实现流式下载

模块

分为 3 部分

  • StreamSaver.js:前端部分
  • mitm.html:通过 iframe/popup 打开的页面,用于注册 ServiceWorker
  • sw.jsServiceWorker 部分,用于拦截请求,并返回流式响应

画成顺序图,如下:

StreamSaver 的执行过程

StreamSaver.js

当引入了 StreamSaver 后,就会执行以下代码用于初始化全局变量

以下代码用于判断浏览器环境是否支持 ServiceWorker,如果不支持,后面会降级成非流式保存文件(当整个文件内容返回完毕后,才保存文件)

这段代码用于判断浏览器环境是否支持 TransformStream,为变量 supportsTransferable 赋值。supportsTransferable 的值会作为对象属性发送给ServiceWorker

当需要保存文件时,就调用 createWriteStream 入口函数

函数在初始化了几个变量后,就通过调用 loadTransporter 函数来创建 iframe/popup

为了在前端和 iframe/popup 之间通信,还要创建 MessageChannelMessageChannel 有 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 对象,包括 transferringReadablepathnameheaders

使用 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 件事:

  1. 保存 downloadUrlstream,
  2. 向 port1(StreamSaver.js) 发送message事件,参数是 downloadUrl

如果 transferringReadable 为 true,就绑定 port 的message事件,在回调中设置readableStream

port 收到message事件时,设置 readableStream 的值,供下面返回

StreamSaver.js

port1 收到 downloadUrl 后,就使用 location.hrefServiceWorker 所在的域发出请求

downloadUrl 是在 sw.js 生成的,作用是关闭 popup 后再次请求(ServiceWorker 只能拦截注册后接收到的请求,所以需要在前端再次发送请求),触发 ServiceWorker 返回 readableStream

jimmywarting.github.io/StreamSaver…

请求发送到 ServiceWorker,sw.js 根据 url 找到上次保存的 readableStream

sw.js 返回stream

当响应内容返回到浏览器时,由于使用 location.href 来请求,所以自动触发浏览器的下载动作。会创建一个文件,然后流式下载。

当文件内容返回完毕后,文件就下载完成。

思考几个问题

为什么前端请求 URL,就能自动触发下载文件?

  1. URL 被 ServiceWorker 拦截,并没有发送到服务端
  2. ServiceWorker 拦截到请求后,返回了ReadableStream
  3. 由于采用 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

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值