JackAtlas

Command Palette

Search for a command to run...

第十章 流式数据 - 《Progressive Web Apps》读书笔记十
about 7 years ago
前端开发

第十章 流式数据 - 《Progressive Web Apps》读书笔记十

10.1 理解 Web Stream

如果不使用流,在网页上显示一张图片需要:

  1. 通过网络获取图片数据
  2. 处理数据并将其解压为原始像素数据
  3. 将结果数据渲染到页面中

如果使用流,可以一块块地返回下载结果并进行处理,使得屏幕上的渲染更快。还可以并行地获取及处理数据。

10.1.1 Web Stream 有什么优势

  • 知道开始与结束:流知道到它们从哪里开始、在哪里结束,尽管流也可能是无限的。
  • 缓冲:流可以缓冲尚未读取的值。不使用流,这些数据将会丢失。
  • 通过管道连接:可以用管道将流组合成一个异步序列。
  • 内置的错误处理:发生的任何错误都将沿管道进行传播。
  • 可取消:可以取消流并将其传回管道中。

10.1.2 可读流

可读流表示可以从中读取数据的数据源。可读流只允许数据流出,不允许流入。

可读流使用的数据源有两种类型:推送(push)源和读取(pull)源。

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 基础示例

self.addEventListener('fetch', event => {
  event.respondWith(htmlStream()) // 进入fetch事件并用HTML流进行响应
})

function htmlStream() {
  const html = 'html goes here...' // 将要返回的HTML字符串

  const stream = new ReadableStream({ // 创建一个ReadableStream
    start: controller => {
      const encoder = new TextEncoder() // 需要使用TextEncoder将文本转换成字节
      let pos = 0
      let chunkSize = 1

      function push() { // 将结果推送到Web Stream中
        if (pos >= html.length) { // 检查是否超出了HTML的长度,如果超出,则关闭controller
          controller.close()
          return
        }

        controller.enqueue(encoder.encode(html.slice(pos, pos + chunkSize))) // 将下一个HTML块编码并放入队列中

        pos += chunkSize
        setTimeout(push, 50) // 强制延迟500ms,以降低渲染速度
      }

      push() // 开始推送流
    }
  })

  return new Response(stream, { // 返回流的结果作为新的Response对象
    headers: {
      'Content-Type': 'text/html'
    }
  })
}

以上代码为了演示浏览器的流功能,使用 Web Stream 的同时还故意减缓了页面的渲染速度,实际应用中不这样做。

浏览器从刚接收到数据就开始渲染页面,而不是等到所有数据下载完才渲染。

10.3 页面渲染加速

↓ 在 Service Worker 安装期间将资源添加到缓存中

const cacheName = 'latestNews-v1'

self.addEventListener('install', event => {
  self.skipWaiting() // Service Worker 应该尽早开始控制不受前一个 Service Worker 控制的客户端

  event.waitUntil( // 在安装阶段缓存资源
    caches.open(cacheName)
      .then(cache => cache.addAll([
        './js/main.js',
        './images/newspaper.svg',
        './css/site.css',
        './header.html', // 在安装阶段缓存 header.html 和 footer.html
        '/footer.html',
        'offline-page.html'
      ]))
  )
})

self.addEventListener('activate', event => {
  self.clients.claim() // 强制激活当前的 Service Worker
})

↓ 在 Web Stream 中拼装 HTML

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 => { // 进入 fetch 事件
  const url = new URL(event.request.url)

  if (url.pathname.endsWith('/article.html')) { // 是否请求文章的路由
    const articleId = getQueryString('id') // 获取文章的 ID
    const articleUrl = `data-${articleId}` // 建立文章的 URL

    event.respondWith(streamArticle(articleUrl)) // 使用流结果进行响应
  }
})

↓ 在 Web Stream 响应中拼装 HTML

function streamArticle(url) {
  try {
    new ReadableStream({}) // 检查当前浏览器是否支持 Web Stream API
  } catch (e) {
    return new Response('Streams not supported')
  }

  const stream = new ReadableStream({ // 创建 ReadableStream
    start(controller) {
      const startFetch = caches.match('header.html') // 从缓存中获取 header.html
      const bodyData = fetch(`data/${url}.html`) // 使用 Fetch API 来获取页面的主体部分
        .catch(() => new Response('Body fetch failed'))
      const endFetch = caches.match('footer.html') // 从缓存中获取 footer.html

      function pushStream(stream) { // 使用 pushStream 函数来将下一个数据块推送到流中
        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, { // 创建 Response 对象并返回流的结果
    headers: { 'Content-Type': 'text/html' }
  })
}

使用 Service Worker 缓存和流,将页面数据“缝合”在一起,这意味着首次渲染几乎是瞬时的,然后通过网络传输小块内容,这样做是比服务端渲染有优势的。因为内容会通过常规的 HTML 解析器,所以你得到的是流,而且与手动把内容添加到 DOM 中并没有任何行为上的差异。

10.4 Web Stream API 的未来

能够利用浏览器流的好处是可以开始使用 JavaScript 来访问如下内容:

  • Gzip/deflate
  • 音频/视频解码器
  • 图片解码器
  • HTML/XML 的流解析器