AMP

Shadow Reader 中的流媒体

未分类

我们做了什么?

我们再次让 Shadow Reader 变得更快!

我们创建了 The Shadow Reader (https://amp.cards) 来演示如何在渐进式网络应用 (PWA) 中使用 AMP 页面(阅读我们的 公告帖 以了解更多背景信息)。该网站将 The Guardian 中的真实文章折叠成一种沉浸式新闻阅读体验。它是一个演示,但它也是一个功能齐全的网站,包含将 AMP 嵌入到漂亮的 PWA 中所需的一切。

今年早些时候,我们 增强 了 Shadow Reader,使其遵循 AMP=>AMP/PWA 模式,这使得初始文章加载速度更快,并且使整个应用更加有利于 SEO。现在,我们通过添加 DOM 流式传输使网站变得更快。这意味着浏览器可以在加载时呈现内容 - 它不必等到完全加载!

下面的视频显示了左侧的新流式 Shadow Reader 和右侧的旧版本。为了清楚地看到流式传输带来的差异,我使用了 Charles 代理 来模拟 56kbps 连接。虽然这显然比平均连接速度要差,但 Shadow Reader 已经相当快了! 在 AMP 中,所有 JavaScript 都是异步的。在许多 AMP(包括 The Guardian)中,大部分 HTML 由 <head> 中的 CSS 组成。没有太多 <body> 可以流式传输!因此,很难在良好的连接或甚至在 DevTools 中模拟的 3G 连接中发现差异。

<head> 中加载 HTML 需要一点时间,因此此视频从几秒钟后开始。一旦我们进入 <body>,流式传输就会发挥其魔力,在我的非实验室测试环境中,主文章文本在流式传输的情况下比不流式传输的情况下早 11 秒完全可见。有趣的是,文章的 YouTube 缩略图在流式传输版本中紧随文本之后显示,而在非流式传输版本中,它比流式传输版本晚 67 秒显示。

 

这是如何工作的?

AMP 采用一种称为 Shadow AMP 的形式,它允许 AMP 完全存在于 影子根 中。Shadow AMP 允许您将 AMP 嵌入到另一个网页中。您可以这样创建 Shadow AMP:

const shadowDoc = AMP.attachShadowDoc(container, doc, url);

如果您已阅读Jake Archibald 的帖子,您就会知道可以通过涉及 <iframe> 的技巧将内容流式传输到 DOM 中。您可能不知道的是此技术已用于将 AMP 内容流式传输到影子根中!您只需使用不同的方法创建影子 AMP:

const shadowDoc = AMP.attachShadowDocAsStream(container, url); 

以下是将 AMP 内容流式传输到影子根中的 4 个步骤

  1. 创建流式传输影子 AMP
  2. 使用 fetch()访问您的内容
  3. 将该内容流式传输到影子 AMP 中。
  4. fetch() 告诉您已完成时,关闭编写器。



四个步骤

1. 创建流式传输影子 AMP。见上文。

2. 使用 fetch() 访问您的内容。对 AMP URL 打开 fetch()。fetch() 返回一个解析为响应对象的任务。此对象是一个流。通常,您会使用一种方法将流读完,然后以某种方式对其进行处理。例如,您可以使用 response.text() 或 response.blob(),如下所示

fetch('example.com/amp.html')
.then(response => response.text())
.then(text => console.log(text));

不过,在我们的案例中,我们将使用 response 对象作为流。通过其 body 属性很容易访问它,该属性公开了一个 ReadableStream

fetch('example.com/amp.html')
.then(response => response.body())
.then( // read from the ReadableStream );

ReadableStream 包含一个 getReader() 方法,该方法锁定流并返回一个 ReadableStreamDefaultReader ReadableStreamDefaultReader 公开一个 read() 方法,该方法提供对 HTML 块的访问,如下所示:

// read from the ReadableStream
let reader = response.body.getReader();
let chunk = await reader.read();


3. 将该内容流式传输到 AMP 影子中。
与此同时,shadowDoc 我们创建的内容包含一个 writer 对象,该 writer 包含 writeclose 方法,可用于流式传输。因此,您可以像这样将内容流式传输到 DOM 中:

shadowDoc.writer.write(html);

并像这样关闭它

shadowDoc.writer.close();

因此,一旦我们有了流读取器和 shadowDoc 流媒体播放器,我们重复以下步骤

  • 从流中获取块
  • 解码块并将其写入 DOM

直到流读取器通过布尔值告诉我们它已完成。

执行此操作的一种方法是通过递归。在 Shadow Reader 中,我们使用了 await。以下是我们的流式传输代码要点:

   const shadowDoc = AMP.attachShadowDocAsStream(container, url);
    fetch(url).then(async response => {
      let reader = response.body.getReader();
      let decoder = new TextDecoder();
      while (true) {
        let chunk = await reader.read();
        if (chunk.done) {
          shadowDoc.writer.close();
          break;
        }
        let html = decoder.decode(
          chunk.value || new Uint8Array(),
          {stream: !chunk.done}
        );
        if (html) {
          shadowDoc.writer.write(html);
        }
      }
    });


4. 当
fetch() 告诉您它已完成时,关闭 writer。 看来我们在上面的步骤 3 中已经介绍了这一点。大功告成!

等等,你说——不支持流媒体播放的浏览器怎么办? 幸运的是,在这种情况中,调用 AMP.attachShadowDocAsStream() 会优雅地降级到与 AMP.attachShadowDoc() 相同的功能。

当应用加载时,Shadow Reader 会加载并预渲染前三篇文章。我们没有对这些文章使用流媒体播放,为了安全起见,我们也不会对不支持流媒体播放的浏览器使用流媒体播放。你可以通过查看 Article.js 中的 load()stream() 来方便地比较流媒体播放和非流媒体播放方法。

 

现实很复杂。

没有一两个复杂因素,生活会怎样?

测试。如果你像我一样,你希望看到流媒体播放,逐块进行,方法是在流媒体播放代码中插入一个断点。但这可能不会产生想要的结果。在我写这篇文章时,在 Chrome 中设置了断点,影子根在关闭之前一直是不可见的。如果你使用 DevTools 将你的连接限制为 2G,则可以更轻松地看到流媒体播放。使用发送较小块内容的代理服务器效果会更好。或者使用特别大的 AMP 进行测试,因为许多 AMP 包含很少的 HTML,并且整个内容可能只分成 2 块甚至 1 块。

操作顺序。在流媒体播放之前,Shadow Reader 会显示一个加载旋转器,加载文章,在隐藏的 div 中渲染它,然后移除旋转器并显示文章。使用流媒体播放,我们希望立即显示文章! 这意味着我们必须移动一些动画。这也意味着 AMP 的任何修改都必须立即进行,而不是在它完全加载之后。具体来说:

隐藏 AMP 中不需要的部分。当 Shadow Reader 导入 AMP 时,它需要移除原始文章的菜单、页眉或页脚,因为这些内容已经是 PWA 的一部分,而且同时拥有两个菜单会很愚蠢。Shadow Reader 过去会在 DOM 中创建整篇文章,移除不需要的部分,然后显示经过清理的文章。通过流式传输,我们必须从一开始就绝不显示不需要的元素。我们可以在 HTML 进入时过滤掉不需要的元素,但这很难。相反,我们只需使用 CSS 隐藏这些元素。

通常情况下,这很简单。AMP 会自动将 amp-shadow 类应用于影子根,这使你可以轻松地对位于影子根中的 AMP 进行不同的样式设置。不幸的是,在我们的案例中,我们无法控制 Guardian 文章。我们通过查找包含 <style amp-custom> 的区块,然后将我们的 CSS 直接注入其中来解决此问题。

无论如何,这都是有用的,因为 Shadow Reader 在 AMP 中包含了一个自定义样式表。不幸的是,在流式传输期间,无法指望浏览器在看到 <style> 标记后立即加载并应用 CSS - 它可能会等到流式传输完成后。我们通过注入自定义样式表来解决此问题。完成! 以下是实际的最终流式传输代码

    fetch(this.proxyUrl).then(async response => {
      let reader = response.body.getReader();
      let decoder = new TextDecoder();

      while (true) {
        let chunk = await reader.read();

        if (chunk.done) {
          shadowDoc.writer.close();
          break;
        }

        let html = decoder.decode(
          chunk.value || new Uint8Array(),
          {stream: !chunk.done}
        );

        // check each chunk of HTML to see if it contains <style amp-custom>. If so, add in some extra CSS.
        if (html) {
          html = shadowReader.backend.injectCSS(html);

          // when we've got the body, start the process of animating the card and showing the article,
          // placing the card before the article
          if (html.includes('<body')) {
            html = article.prependCardHtml(html);
            shadowDoc.writer.write(html);
            article.card.animate();
            article.show();

          } else {
            shadowDoc.writer.write(html);
          }
        }
      }
    });


留待读者练习。
在截稿时,我还没有处理一个极端情况:如果 <body 在两个区块之间被分割,则此方法会失败。请提交 PR!

由 Google 的 AMP 开发者倡导者 Ben Morss 发布