《Progressive Web Apps》读书笔记十
第十章 流式数据
10.1 理解 Web Stream
如果不使用流,在网页上显示一张图片需要:
- 通过网络获取图片数据
- 处理数据并将其解压为原始像素数据
- 将结果数据渲染到页面中
如果使用流,可以一块块地返回下载结果并进行处理,使得屏幕上的渲染更快。还可以并行地获取及处理数据。
10.1.1 Web Stream 有什么优势
- 知道开始与结束:流知道到它们从哪里开始、在哪里结束,尽管流也可能是无限的。
- 缓冲:流可以缓冲尚未读取的值。不使用流,这些数据将会丢失。
- 通过管道连接:可以用管道将流组合成一个异步序列。
- 内置的错误处理:发生的任何错误都将沿管道进行传播。
- 可取消:可以取消流并将其传回管道中。
10.1.2 可读流
可读流表示可以从中读取数据的数据源。可读流只允许数据流出,不允许流入。
可读流使用的数据源有两种类型:推送(push)源和读取(pull)源。
1 2 3 4 5
| var stream = new ReadableStream({ start(controller) {}, pull(controller) {}, cancel(controller) {} }, queuingStrategy)
|
start(controller)
方法,会立即调用它并用它来设置任何基础数据源,只有当这个 Promise 成功完成后才会调用 pull(controller)
。
pull(controller)
方法,当流的缓冲区未满时,会调用该方法,而且会重复调用,直到缓冲区满为止。只有当前一个 pull(controller)
成功完成后,才会调用下一个 pull(controller)
。
cancel(controller)
方法,当消费者表示他们不再对流感兴趣时,会调用该方法来取消任何基础数据源。
queuingStrategy
是一个对象,决定了流如何根据内部队列的状态来发出过载信号。
10.2 基础示例
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35
| self.addEventListener('fetch', event => { event.respondWith(htmlStream()) })
function htmlStream() { const html = 'html goes here...'
const stream = new ReadableStream({ start: controller => { const encoder = new TextEncoder() let pos = 0 let chunkSize = 1
function push() { if (pos >= html.length) { controller.close() return }
controller.enqueue(encoder.encode(html.slice(pos, pos + chunkSize)))
pos += chunkSize setTimeout(push, 50) }
push() } })
return new Response(stream, { headers: { 'Content-Type': 'text/html' } }) }
|
以上代码为了演示浏览器的流功能,使用 Web Stream 的同时还故意减缓了页面的渲染速度,实际应用中不这样做。
浏览器从刚接收到数据就开始渲染页面,而不是等到所有数据下载完才渲染。
10.3 页面渲染加速
↓ 在 Service Worker 安装期间将资源添加到缓存中
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21
| const cacheName = 'latestNews-v1'
self.addEventListener('install', event => { self.skipWaiting()
event.waitUntil( caches.open(cacheName) .then(cache => cache.addAll([ './js/main.js', './images/newspaper.svg', './css/site.css', './header.html', '/footer.html', 'offline-page.html' ])) ) })
self.addEventListener('activate', event => { self.clients.claim() })
|
↓ 在 Web Stream 中拼装 HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| function getQueryString(field, url = window.location.href) { const reg = new RegExp('[?&]' + field + '=([^&#]*)', 'i') const result = reg.exec(url) return result ? result[1] : null }
self.addEventListener('fetch', event => { const url = new URL(event.request.url)
if (url.pathname.endsWith('/article.html')) { const articleId = getQueryString('id') const articleUrl = `data-${articleId}`
event.respondWith(streamArticle(articleUrl)) } })
|
↓ 在 Web Stream 响应中拼装 HTML
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41
| function streamArticle(url) { try { new ReadableStream({}) } catch (e) { return new Response('Streams not supported') }
const stream = new ReadableStream({ start(controller) { const startFetch = caches.match('header.html') const bodyData = fetch(`data/${url}.html`) .catch(() => new Response('Body fetch failed')) const endFetch = caches.match('footer.html')
function pushStream(stream) { const reader = stream.getReader() function read() { return reader.read().then(result => { if (result.done) return controller.enqueue(result.value) return read() }) }
return read() }
startFetch .then(response => pushStream(response.body)) .then(() => bodyData) .then(response => pushStream(response.body)) .then(() => endFetch) .then(response => pushStream(response.body)) .then(() => controller.close()) } })
return new Response(stream, { headers: { 'Content-Type': 'text/html' } }) }
|
使用 Service Worker 缓存和流,将页面数据“缝合”在一起,这意味着首次渲染几乎是瞬时的,然后通过网络传输小块内容,这样做是比服务端渲染有优势的。因为内容会通过常规的 HTML 解析器,所以你得到的是流,而且与手动把内容添加到 DOM 中并没有任何行为上的差异。
10.4 Web Stream API 的未来
能够利用浏览器流的好处是可以开始使用 JavaScript 来访问如下内容:
- Gzip/deflate
- 音频/视频解码器
- 图片解码器
- HTML/XML 的流解析器