从 Fetch 到 Streams —— 以流的角度处理网络请求(下)

charlesc
发布于 2021-3-11 09:35
浏览
0收藏

管道


到这里 ReadableStream 上的方法已经描述的差不多了,最后只剩下 pipeTo() 方法和 pipeThrough() 方法没有提到了。从字面意思上来看,这就是我们之前提到的管道,可以将流直接指向另一个流,最后拿到处理后的数据。Jake Archibald 在他的那篇《2016 — 属于 web streams 的一年》中提出了下面的例子,或许在(当时的)未来可以通过这样的形式以流的形式得到解析后的文本:

var reader = response.body
    .pipeThrough(new TextDecoder()).getReader();
reader.read().then(result => {
    // result.value will be a string
});

 

现在那个未来已经到了,为了不破坏兼容性,TextEncoder 和 TextDecoder 分别扩展出了新的 TextEncoderStream 和 TextDecoderStream,允许我们以流的方式编码或者解码文本。例如下面的例子会在请求中检索 It works! 这段文字,当找到这段文字时返回 true 同时断开请求。此时我们不需要再接收后续的数据,可以减少请求的流量:

 

fetch('/index.html').then((res) => {
    const decoder = new TextDecoderStream('gbk', { ignoreBOM: true });
    const textStream = res.body.pipeThrough(decoder);
    const reader = textStream.getReader();
    const findMatched = () => reader.read().then(({ value, done }) => {
        if (done) {
            return false;
        }
        if (value.indexOf('It works!') >= 0) {
            reader.cancel();
            return true;
        }
        return findMatched();
    });
    return findMatched();
}).then((isMatched) => { ... });

 

或者在未来,我们甚至在流里实现实时转码视频并播放,或者将浏览器还不支持的图片以流的形式实时渲染出来:

const encoder = new VideoEncoder({
    input: 'gif', output: 'h264'
});
const media = new MediaStream();
const video = document.createElement('video');
fetch('/sample.gif').then((res) => {
    response.body.pipeThrough(encoder).pipeTo(media);
    video.srcObject = media;
});

 

从中应该可以看出来这两种方法的区别:pipeTo() 方法应该会接受一个可以写入的流,也就是 WritableStream;而 pipeThrough() 方法应该会接受一个既可写入又可读取的流,也就是 TransformStream。从 Fetch 到 Streams —— 以流的角度处理网络请求(下)-鸿蒙开发者社区接下来我们将介绍这两种流,不过在继续之前,我们先来看看 ReadableStream 在浏览器上的支持程度:

从 Fetch 到 Streams —— 以流的角度处理网络请求(下)-鸿蒙开发者社区

从表中我们注意到,这两个方法支持的比较晚。而原因估计你也能猜得到,当数据从一个可读取的流中流出时,管道的另一端应该是一个可写入的流,问题就在于可写入的流实现的比较晚。

 

WritableStream从 Fetch 到 Streams —— 以流的角度处理网络请求(下)-鸿蒙开发者社区

我们已经从 ReadableStream 中了解到很多关于流的知识了,所以下面我们简单过一下 WritableStream。WritableStream 就是可写入的流,如果说 ReadableStream 是一个管道中流的起点,那么 WritableStream 可以理解为流的终点。下面是一个 WritableStream 实例上的参数和可以使用的方法:

 

WritableStream

 

  • locked
  • abort()
  • getWriter()


可用的方法和参数很少,估计大家从名字就能知道它们是做什么的。其中直接调用 getWriter() 方法会得到一个 WritableStreamDefaultWriter 实例,通过这个实例我们就能向 WritableStream 写入数据。同样的,当我们激活了一个 writer 后,这个流就会被锁定(locked = true)。这个 writer 上有如下属性和方法:

 

WritableStreamDefaultWriter

 

  • closed
  • desiredSize
  • ready
  • abort()
  • close()
  • write()
  • releaseLock()


看起来和 ReadableStreamDefaultReader 没太大区别,多出的 abort() 方法相当于抛出了一个错误,使这个流不能再被写入。另外这里多出了一个 ready 属性,这个属性是一个 Promise,当它被 resolve 时,表明目前流的缓冲区队列不再过载,可以安全地写入。所以如果需要循环向一个流写入数据的话,最好放在 ready 处理。

 

同样的,我们可以自己构造一个 WritableStream,构造时可以定义以下方法和参数:

const stream = new WritableStream({
    start(controller) {
        // 将会在对象创建时立刻执行,并传入一个流控制器
        controller.error(reason)
            // 对流抛出一个错误
    },
    write(chunk, controller) {
        // 将会在一个新的数据片段写入时调用,可以获取到写入的片段
    },
    close(controller) {
        // 将会在流写入完成时调用
    },
    abort(reason) {
        // 将会在流强制关闭时调用,此时流会进入一个错误状态,不能再写入
    }
}, queuingStrategy); // { highWaterMark: 1 }

 

下面的例子中,我们通过循环调用 writer.write() 方法向一个 WritableStream 写入数据:

const stream = new WritableStream({
    write(chunk) {
        return new Promise((resolve) => {
            console.log('got chunk:', chunk);
            // 在这里对数据进行处理
            resolve();
        });
    },
    close() {
        console.log('stream closed');
    },
    abort() {
        console.log('stream aborted');
    }
});
const writer = stream.getWriter();
// 将数据逐一写入 stream
data.forEach((chunk) => {
    // 待前一个数据写入完成后再写入
    writer.ready.then(() => {
        writer.write(chunk);
    });
});
// 在关闭 writer 前先保证所有的数据已经被写入
writer.ready.then(() => {
    writer.close();
});

 

下面是 WritableStream 的浏览器支持情况,可见 WritableStream 在各个浏览器上的的实现时间和 pipeTo() 与 pipeThrough() 方法的实现时间是吻合的,毕竟要有了可写入的流,管道才有存在的意义。

从 Fetch 到 Streams —— 以流的角度处理网络请求(下)-鸿蒙开发者社区

TransformStream


从之前的介绍中我们知道,TransformStream 是一个既可写入又可读取的流,正如它的名字一样,它作为一个中间流起着转换的作用。所以一个 TransformStream 实例只有如下参数:

 

TransformStream

  • readable: ReadableStream
  • writable: WritableStream

 

TransformStream 上没有其他的方法,它只暴露了自身的 ReadableStream 与 WritableStream。我们只需要在数据源流上链式使用 pipeThrough() 方法就能实现流的数据传递,或者使用暴露出来的 readable 和 writable 直接操作数据即可使用它。

 

TransformStream 的处理逻辑主要在流内部实现,下面是构造一个 TransformStream 时可以定义的方法和参数:

const stream = new TransformStream({
    start(controller) {
        // 将会在对象创建时立刻执行,并传入一个流控制器
        controller.desiredSize
            // 填满队列所需字节数
        controller.enqueue(chunk)
            // 向可读取的一端传入数据片段
        controller.error(reason)
            // 同时向可读取与可写入的两侧触发一个错误
        controller.terminate()
            // 关闭可读取的一侧,同时向可写入的一侧触发错误
    },
    transform(chunk, controller) {
        // 将会在一个新的数据片段传入可写入的一侧时调用
    },
    flush(controller) {
        // 当可写入的一端得到的所有的片段完全传入 transform() 方法处理后,在可写入的一端即将关闭时调用
    }
}, queuingStrategy); // { highWaterMark: 1 }

 

有了 ReadableStream 与 WritableStream 作为前置知识,TransformStream 就不需要做太多介绍了。下面的示例代码摘自 MDN,是一段实现 TextEncoderStream 和 TextDecoderStream 的 polyfill,本质上只是对 TextEncoder 和 TextDecoder 进行了一层封装:

const tes = {
    start() { this.encoder = new TextEncoder() },
    transform(chunk, controller) {
        controller.enqueue(this.encoder.encode(chunk))
    }
}
let _jstes_wm = new WeakMap(); /* info holder */
class JSTextEncoderStream extends TransformStream {
    constructor() {
        let t = { ...tes }
        super(t)
        _jstes_wm.set(this, t)
    }
    get encoding() { return _jstes_wm.get(this).encoder.encoding }
}
const tes = {
    start() {
        this.decoder = new TextDecoder(this.encoding, this.options)
    },
    transform(chunk, controller) {
        controller.enqueue(this.decoder.decode(chunk))
    }
}
let _jstds_wm = new WeakMap(); /* info holder */
class JSTextDecoderStream extends TransformStream {
    constructor(encoding = 'utf-8', { ...options } = {}) {
        let t = { ...tds, encoding, options }
        super(t)
        _jstes_wm.set(this, t)
    }
    get encoding() { return _jstds_wm.get(this).decoder.encoding }
    get fatal() { return _jstds_wm.get(this).decoder.fatal }
    get ignoreBOM() { return _jstds_wm.get(this).decoder.ignoreBOM }
}

 

到这里我们已经把 Streams API 中所提供的流浏览了一遍,最后是 caniuse 上的浏览器支持数据,可见目前 Streams API 的支持度不算太差,至少主流浏览器都支持了 ReadableStream,读取流已经不是什么问题了,可写入的流使用场景也比较少。不过其实问题不是特别大,我们已经简单知道了流的原理,做一些简单的 polyfill 或者额外写些兼容代码应该也是可以的,毕竟已经有不少第三方实现了。从 Fetch 到 Streams —— 以流的角度处理网络请求(下)-鸿蒙开发者社区在 Service Worker 中使用 Streams API


控制请求的响应速度
首先让我们来模拟体验一下龟速到只有大约 30B/s 的网页看起来是什么样子的:

 

你会注意到页面中的文字是一个个显示出来的(甚至标题栏也是这样的),其实这是借助 Service Worker 的 onfetch 事件配合 Streams API 实现的。熟悉 Service Worker 的同学应该知道 Service Worker 里有一个 onfetch 事件,可以在事件内捕获到页面所有的请求,onfetch 事件的事件对象 FetchEvent 中包含如下参数和方法,排除客户端 id 之类的参数,我们主要关注 request 属性以及事件对象提供的两个方法:

addEventListener('fetch', (fetchEvent) => {
    fetchEvent.clientId
    fetchEvent.preloadResponse
    fetchEvent.replacesClientId
    fetchEvent.resultingClientId
    fetchEvent.request
        // 浏览器原本需要发起请求的 Request 对象
    fetchEvent.respondWith()
        // 阻止浏览器默认的 fetch 请求处理,自己提供一个返回结果的 Promise
    fetchEvent.waitUntil()
        // 延长事件的生命周期,例如在返回数据后再做一些事情
});

 

使用 Service Worker 最常见的例子是借助 onfetch 事件实现中间缓存甚至离线缓存。我们可以调用 caches.open() 打开或者创建一个缓存对象 cache,如果 cache.match(event.request) 有缓存的结果时,可以调用 event.respondWith() 方法直接返回缓存好的数据;如果没有缓存的数据,我们再在 Service Worker 里调用 fetch(event.request) 发出真正的网络请求,请求结束后我们再在 event.waitUntil() 里调用 cache.put(event.request, response.clone()) 缓存响应的副本。由此可见,Service Worker 在这之间充当了一个中间人的角色,可以捕获到页面发起的所有请求,然后根据情况返回缓存的请求,所以可以猜到我们甚至可以改变预期的请求,返回另一个请求的返回值。

 

Streams API 在 Service Worker 中同样可用,所以我们可以在 Service Worker 里监听 onfetch 事件,然后用上我们之前学习到的知识,改变 fetch 请求的返回结果为一个速度很缓慢的流。这里我们让这个流每隔约 30 ms 才吐出 1 个字节,最后就能实现上面视频中的效果:

globalThis.addEventListener('fetch', (event) => {
    event.respondWith((async () => {
        const response = await fetch(event.request);
        const { body } = response;
        const reader = body.getReader();
        const stream = new ReadableStream({
            start(controller) {
                const sleep = time => new Promise(resolve => setTimeout(resolve, time));
                const pushSlowly = () => {
                    reader.read().then(async ({ value, done }) => {
                        if (done) {
                            controller.close();
                            return;
                        }
                        const length = value.length;
                        for (let i = 0; i < length; i++) {
                            await sleep(30);
                            controller.enqueue(value.slice(i, i + 1));
                        }
                        pushSlowly();
                    });
                };
                pushSlowly();
            }
        });
        return new Response(stream, { headers: response.headers });
    })());
});

 

使用 Service Worker 最常见的例子是借助 onfetch 事件实现中间缓存甚至离线缓存。我们可以调用 caches.open() 打开或者创建一个缓存对象 cache,如果 cache.match(event.request) 有缓存的结果时,可以调用 event.respondWith() 方法直接返回缓存好的数据;如果没有缓存的数据,我们再在 Service Worker 里调用 fetch(event.request) 发出真正的网络请求,请求结束后我们再在 event.waitUntil() 里调用 cache.put(event.request, response.clone()) 缓存响应的副本。由此可见,Service Worker 在这之间充当了一个中间人的角色,可以捕获到页面发起的所有请求,然后根据情况返回缓存的请求,所以可以猜到我们甚至可以改变预期的请求,返回另一个请求的返回值。

 

Streams API 在 Service Worker 中同样可用,所以我们可以在 Service Worker 里监听 onfetch 事件,然后用上我们之前学习到的知识,改变 fetch 请求的返回结果为一个速度很缓慢的流。这里我们让这个流每隔约 30 ms 才吐出 1 个字节,最后就能实现上面视频中的效果:

globalThis.addEventListener('fetch', (event) => {
    event.respondWith((async () => {
        const response = await fetch(event.request);
        const { body } = response;
        const reader = body.getReader();
        const stream = new ReadableStream({
            start(controller) {
                const sleep = time => new Promise(resolve => setTimeout(resolve, time));
                const pushSlowly = () => {
                    reader.read().then(async ({ value, done }) => {
                        if (done) {
                            controller.close();
                            return;
                        }
                        const length = value.length;
                        for (let i = 0; i < length; i++) {
                            await sleep(30);
                            controller.enqueue(value.slice(i, i + 1));
                        }
                        pushSlowly();
                    });
                };
                pushSlowly();
            }
        });
        return new Response(stream, { headers: response.headers });
    })());
});

 

下载一个前端生成的大文件


看着不是很实用?那么再举一个比较实用的例子吧。如果我们需要让用户在浏览器中下载一个文件,一般都是会指向一个服务器上的链接,然后浏览器发起请求从服务器上下载文件。那么如果我们需要让用户下载一个在客户端生成的文件,比如从 canvas 上生成的图像,应该怎么办呢?其实让客户端主动下载文件已经有现成的库 FileSaver.js 实现了,它的原理可以用下面的代码简述:

const a = document.createElement('a');
const blob = new Blob(chunk, options);
const url = URL.createObjectURL(blob);
a.href = url;
a.download = 'filename';
const event = new MouseEvent('click');
a.dispatchEvent(event);
setTimeout(() => {
    URL.revokeObjectURL(url);
    if (blob.close) blob.close();
}, 1e3);

 

这里利用了 HTML <a> 标签上的 download 属性,当链接存在该属性时,浏览器会将链接的目标视为一个需要下载的文件,链接不会在浏览器中打开,转而会将链接的内容下载到设备的硬盘上。此外在浏览器中还有 Blob 对象,它相当于一个类似文件的二进制数据对象(File 就是继承于它)。我们可以将需要下载的数据(无论是什么类型,字符串、TypedArray 甚至是其他 Blob 对象)传进 Blob 的构造函数里,这样我们就得到了一个 Blob 对象。最后我们再通过 URL.createObjectURL() 方法可以得到一个 blob: 开头的 Blob URL,将它放到有 download 属性的 <a> 链接上,并触发鼠标点击事件,浏览器就能下载对应的数据了。

 

顺带一提,在最新的 Chrome 76+ 和 Firefox 69+ 上,Blob 实例支持了 stream() 方法,它将返回一个 ReadableStream 实例。所以现在我们终于可以直接以流的形式读取文件了——看,只要 ReadableStream 实现了,相关的原生数据流源也会完善,其他的流或许也只是时间问题而已。


不过问题来了,如果需要下载的文件数据量非常大,比如这个数据是通过 XHR/fetch 或者 WebRTC 传输得到的,直接生成 Blob 可能会遇到内存不足的问题。

下面是一个比较极端的糟糕例子,描述了在浏览器客户端打包下载图片的流程。客户端 JavaScript 发起多个请求得到多个文件,然后通过 JSZip 这个库生成了一个巨大的 ArrayBuffer 数据,也就是 zip 文件的数据。接下来就像之前提到的那样,我们基于它构造一个 Blob 对象并用 FileSaver.js 下载了这个图片。如你所想的一样,所有的数据都是存放在内存中的,而在生成 zip 文件时,我们又占用了近乎一样大小的内存空间,最终可能会在浏览器内占用峰值为总文件大小 2-3 倍的内存空间(也就是下图中黄色背景的部分),流程过后可能还需要看浏览器的脸色 GC 回收。

从 Fetch 到 Streams —— 以流的角度处理网络请求(下)-鸿蒙开发者社区

 

 

现在有了 Streams API,我们就有了另一种解决方式。StreamSaver.js 就是这样的一个例子,它借助了 Streams API 和 Service Worker 解决了内存占用过大的问题。阅读它的源码,可以看出它的工作流程类似下面这样:

从 Fetch 到 Streams —— 以流的角度处理网络请求(下)-鸿蒙开发者社区

StreamSaver.js 包含两部分代码,一部分是客户端代码,一部分是 Service Worker 的代码(对于不支持 Service Worker 的情况,作者在 GitHub Pages 上提供了一个运行 Service Worker 的页面供跨域使用)。

 

在初始化时客户端代码会创建一个 TransformStream 并将可写入的一端封装为 writer 暴露给外部使用,在脚本调用 writer.write(chunk) 写入文件片段时,客户端会和 Service Worker 之间建立一个 MessageChannel,并将之前的 TransformStream 中可读取的一端通过 port1.postMessage() 传递给 Service Worker。Service Worker 里监听到通道的 onmessage 事件时会生成一个随机的 URL,并将 URL 和可读取的流存入一个 Map 中,然后将这个 URL 通过 port2.postMessage() 传递给客户端代码。

 

客户端接收到 URL 后会控制浏览器跳转到这个链接,此时 Service Worker 的 onfetch 事件接收到这个请求,将 URL 和之前的 Map 存储的 URL 比对,将对应的流取出来,再加上一些让浏览器认为可以下载的响应头(例如 Content-Disposition)封装成 Response 对象,最后通过 event.respondWith() 返回。这样在当客户端将数据写入 writer 时,经过 Service Worker 的流转,数据可以立刻下载到用户的设备上。这样就不需要分配巨大的内存来存放 Blob,数据块经过流的流转后直接被回收了,降低了内存的占用。

 

所以借助 StreamSaver.js,之前下载图片的流程可以优化如下:JSZip 提供了一个 StreamHelper 的接口来模拟流的实现,所以我们可以调用 generateInternalStream() 方法以小文件块的形式接收数据,每次接收到数据时数据会写入 StreamSaver.js 的 writer,经过 Service Worker 后数据直接被下载。这样就不会再像之前那样在生成 zip 时占用大量的内存空间了,因为 zip 数据在实时生成时被划分成了小块并迅速被处理掉了。从 Fetch 到 Streams —— 以流的角度处理网络请求(下)-鸿蒙开发者社区

总结

 

经过了这么长时间的学习,我们从 Fetch API 的角度出发探索 Streams API,大致了解了以下几点:

 

  • Streams API 允许我们以流的形式实时处理数据,每次只需要处理数据的一小部分
  • 可以使用 pipeTo()、pipeThrough() 方法方便地将多个流连接起来
  • ReadableStream 是可读取的流,WritableStream 是可写入的流,TransformStream 是既可写入又可读取的流
  • Fetch API 的返回值是一个 Response 对象,它的 body 属性是一个 ReadableStream
  • 借助 Streams API 我们可以实现中断 fetch 请求或者计算 fetch 请求的下载速度,甚至可以直接对返回的数据进行修改
  • 我们学习了如何构造一个流,并将其作为 fetch 请求的返回值
    在 Service Worker 里也可以使用 Streams API,使用 onfetch 事件可以监听所有的请求,并对请求进行篡改
  • 顺带了解了如何中断一个 fetch 请求,使用 download 属性下载文件,Blob 对象,MessageChannel 双向通信……


Streams API 提出已经有很长一段时间了,由于浏览器支持的原因再加上使用场景比较狭窄的原因一直没有得到广泛使用,国内的相关资料也比较少。随着浏览器支持逐渐铺开,浏览器原生提供的可读取流和可写入流也会逐渐增加(比如在本文即将写成时才注意到 Blob 对象已经支持 stream() 方法了),能使用上的场景也会越来越多,让我们拭目以待吧。

 

课后习题&答案:

 

1、试试看将上面的 fetch 请求用原生 XMLHttpRequest 实现一遍,看看你还记得多少知识?

const xhr = new XMLHttpRequest();
xhr.open('POST', 'https://example.org/foo');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.responseType = 'json';
xhr.withCredentials = true;
xhr.addEventListener('load', () => {
    const data = xhr.response;
    // ...
});
xhr.send(JSON.stringify({ foo: 'bar' }))

 

在使用 XHR 初始化请求时会有较多的配置项,虽然这些配置项可以发出更复杂的请求,但是或许你也注意到了,发送请求时既有方法的调用,又有参数的赋值,看下来还是不如 Fetch API 那样直接传入一个对象作为请求参数那么简洁的。此外,如果需要兼容比较早的不支持 XHR 2 的浏览器,你可能还需要改成使用 onreadystatechange 事件并手动解析 xhr.responseText。

 

2、如果我们调用了流的 tee() 方法得到了两个流,但我们只读取了其中一个流,另一个流在之后读取,会发生什么吗?

 

使用 tee() 方法分流出来的两个流之间是相互独立的,所以被读取的流会实时读取到传递的数据,过一段时间读取另一个流,拿到的数据也是完全一样的。不过由于另一个流没有被读取,克隆的数据可能会被浏览器放在一个缓冲区里,即便后续被读取可能也无法被浏览器即时 GC。

const file = document.querySelector('input[type="file"]').files[0];
const stream = file.stream();
const readStream = (stream) => {
    let total = 0;
    const reader = stream.getReader();
    const read = () => reader.read().then(({ value, done }) => {
        if (done) return;
        total += value.length;
        console.log(total);
        read();
    });
    read();
};

const [s1, s2] = stream.tee();
readStream(s1);
readStream(s2);

 

例如在上述代码中选择一个 200MB 的文件,然后直接调用 readStream(stream),在 Chrome 浏览器下没有较大的内存起伏;如果调用 stream.tee() 后得到两个流 s1 和 s2,如果同时对两个流调用 readStream() 方法,在 Chrome 浏览器下同样没有较大的内存起伏,最终输出的文件大小也是一致的;如果只对 s1 调用的话,会发现执行结束后 Chrome 浏览器下内存占用多了约 200MB,此时再对 s2 调用,最终得到的文件大小虽然一致,但是内存并没有及时被 GC 回收,此时浏览器的内存占用还是之前的 200MB。

 

可能你会好奇,之前我们尝试过使用 tee() 方法得到两段流,一个流直接返回另一个流用于输出下载进度,会有这样的资源占用问题吗?会不会出现两个流速度不一致的情况?其实计算下载进度的代码并不会非常耗时,数据计算完成后也不会再有多余的引用,浏览器可以迅速 GC。此外计算的速度是大于网络传输本身的速度的,所以并不会造成瓶颈,可以认为两个流最终的速度是基本一样的。

 

3、如果不调用 controller.error() 抛出错误强制中断流,而是继续之前的流程调用 controller.close() 关闭流,会发生什么事吗?

 

从上面的结果来看,当我们调用 aborter() 方法时,请求被成功中止了。不过如果不调用 controller.error() 这个方法抛出错误的话,由于我们主动关闭了 fetch 请求返回的流,循环调用的 reader.read() 方法会接收到 done = true,然后会调用 controller.close()。这就意味着这个流是被正常关闭的,此时 Promise 链的后续操作不会被中断,而是会收到已经传输的不完整数据。

如果没有做特殊的逻辑处理的话,直接返回不完整的数据可能会导致错误。不过如果能好好利用上的话,或许可以做更多事情——比如断点续传的另一种实现,这就有点像 Firefox 的私有实现 moz-chunked-arraybuffer 了。


4、StreamSaver.js 在不支持 TransformStream 的浏览器下其实是可以正常工作的,这是怎么实现的呢?

 

记得我们之前提到过构造一个 ReadableSteam 然后包装成 Response 对象返回的实现吧?我们最终的目的是需要构造一个流并返回给浏览器,这样传入的数据可以立即被下载,并且没有多余引用而迅速 GC。所以对于不支持 TransformStream 甚至 WritableStream 的浏览器,StreamSaver.js 封装了一个模拟 WritableStream 实现的 polyfill。当 polyfill 得到数据时,会将得到的数据片段通过 MessageChannel 直接传递给 Service Worker。Service Worker 发现这不是一个流,会构造出一个 ReadableStream 实例,并将数据通过 controller.enqueue() 方法传递进流。后续的流程估计你已经猜到了,和当前的后续流程是一样的,同样是生成一个随机 URL 并跳转,然后返回封装了这个流的 Response 对象。

事实上,现在的 Firefox Send 就使用了这样的实现,当用户下载文件时会发出请求,Service Worker 接收到下载请求后会建立真实的 fetch 请求连接服务器,将返回的数据实时解密后直接下载到用户的设备上。这样的直观效果是,浏览器直接下载了文件,文件会显示在浏览器的下载列表中,同时页面上还会有下载进度:

从 Fetch 到 Streams —— 以流的角度处理网络请求(下)-鸿蒙开发者社区

分类
已于2021-3-11 09:35:38修改
收藏
回复
举报
回复
    相关推荐