React-渐进式-Web-应用-三-

77 阅读1小时+

React 渐进式 Web 应用(三)

原文:zh.annas-archive.org/md5/7B97DB5D1B53E3A28B301BFF1811634D

译者:飞龙

协议:CC BY-NC-SA 4.0

第九章:使用清单使我们的应用程序可安装

我们现在开始走向渐进式 Web 应用程序领域。从现在开始,我们的唯一重点将是将我们现有的应用程序变得更快、更时尚和更用户友好。

渐进式 Web 应用程序的一个重要优势是弥合了 Web 应用程序(在浏览器中查看)和本地应用程序(作为独立应用程序启动)之间的差距。接下来的几章,特别是将专注于使我们的 Web 应用程序更像本地应用程序,而不失去 Web 应用程序的所有优势。

Web 应用程序相对于本地应用程序的第一个主要优势是没有安装障碍。如果你创建一个本地应用程序,你需要说服用户在甚至使用你的应用程序之前,投入宝贵的存储空间和带宽。他们必须愿意忍受下载和安装过程。然后他们必须保留它,即使他们并不经常使用它。

Web 应用程序没有这样的障碍。你几乎可以立即使用它们,而且最复杂的 Web 应用程序具有可以与本地应用程序媲美的功能。它们的缺点是什么?嗯,用户必须先导航到他们的浏览器,然后再导航到网页才能使用它。他们没有漂亮整洁的应用程序存在的提醒,从他们手机的主屏幕上盯着他们。

什么是双赢的最佳选择?它将是一个允许用户在安装到他们的设备之前先试用的应用程序,但一旦安装后,它会像本地应用程序一样运行,并在设备的主屏幕上显示图标。

我们如何实现这一点?我们可以通过一个 Web 应用程序清单来实现。

在本章中,我们将涵盖以下内容:

  • 什么是 Web 应用程序清单?

  • 如何使我们的应用程序可以在 Android 上安装

  • 如何使我们的应用程序可以在 iOS 上安装

  • 使用 Web 应用程序安装横幅

什么是应用程序清单?

在第二章,使用 Webpack 入门,当我们设置我们的 Webpack 构建配置时,我们确保我们的构建过程生成了一个资产清单,文件名为asset-manifest.json

这个文件包含了我们的应用程序使用的 JavaScript 文件列表。如果我们愿意,我们可以配置它来列出我们使用的 CSS 和图像文件。

这个资产清单让我们了解了清单的用途--描述应用程序的某个部分。我们的 Web 应用清单类似,但简单地描述了我们的应用程序从更高层面上的全部内容,以一种类似于应用商店对本地应用的描述的方式。

这就是它的外观,随着我们构建文件,我们将更深入地了解,但 Web 应用清单的真正魔力在于它的功能。

在某些浏览器上(本章后面会详细介绍),如果您的 Web 应用包括一个合适的 Web 应用清单,用户可以选择将网页保存到主屏幕上,它会像一个常规应用程序一样出现,并带有自己的启动图标。当他们点击图标时,它将以闪屏启动,并且(尽管是从浏览器运行)以全屏模式运行,因此看起来和感觉像一个常规应用程序。

浏览器支持

这就是 Web 应用清单的缺点--它是一种新技术。因此,很少有浏览器实际支持它。截至目前,只有较新版本的安卓 Webview 和 Chrome for Android 具有完全支持。

我预测支持很快会到来,适用于所有新版浏览器,但目前我们该怎么办呢?

简而言之,有办法在旧版浏览器上激活类似的功能。在本章中,我们将介绍如何使用 Web 应用清单(适用于新版浏览器的用户,并为未来做准备)以及 iOS 设备的polyfill

如果您有兴趣覆盖其他设备,可以使用 polyfills,比如ManUpgithub.com/boyofgreen/manUp.js/)。这些 polyfills 的作用是将不同设备的各种解决方法编译成一个清单文件。

然而,本书是关于 Web 应用的未来,所以我们将向您展示一切您需要为 Web 应用清单的世界做准备。

使我们的应用可安装-安卓

谷歌是 PWA 的最大支持者之一,因此他们的 Chrome 浏览器和安卓操作系统对 Web 应用清单最为友好。

让我们通过创建一个清单的过程,以使其与最新版本的 Chrome 兼容。在本章后面,我们将以更手动的方式进行相同的过程,以支持 iOS。

清单属性

让我们开始吧!在您的public/文件夹中,创建一个名为manifest.json的文件,然后添加一个空对象。以下每个都将是该对象的键值对。我们将快速浏览一下每个可用属性:

  • name:您的应用程序名称。简单!:
"name": "Chatastrophe",
  • short_name:您的应用程序名称的可读版本。这是在全名无法完全显示时使用,比如在用户的主屏幕上。如果您的应用程序名称是“为什么 PWA 对每个人都很棒”,您可以将其缩短为“PWAs R Great”或其他内容:
“short_name”: “Chatastrophe”,
  • icons:用户设备使用的图标列表。我们将只使用我们当前的徽标,这恰好是图标所需的最大尺寸。

Google 推荐以下一组图标:

  • 128x128 作为基本图标大小

  • 152x152 适用于 Apple 设备

  • 144x144 适用于 Microsoft 设备

  • 192x192 适用于 Chrome

  • 256x256、384x384 和 512x512 适用于不同的设备尺寸

最后两个包含在资产包中。我们需要我们的设计师为我们的生产版本创建其余部分,但目前还不需要:

"icons": [
  {
    "src":"/assets/icon.png",
    "sizes": "192x192",
    "type": "image/png"
  },
  { 
    "src": "/assets/icon-256.png", 
    "sizes": "256x256", 
    "type": "image/png" 
  }, 
  { 
    "src": "/assets/icon-384.png", 
    "sizes": "384x384", 
    "type": "image/png" 
  }, 
  { 
    "src": "/assets/icon-512.png", 
    "sizes": "512x512", 
    "type": "image/png" 
  }
],
  • start_url:启动 URL 用于分析目的,以便您可以看到有多少用户通过安装的 PWA 访问您的 Web 应用程序。这是可选的,但不会有害。
"start_url": "/?utm_source=homescreen",
  • background_color:背景颜色用于启动我们的应用程序时显示的闪屏的颜色。在这里,我们将其设置为一个漂亮的橙红色:
"background_color": "#e05a47",
  • theme_color:这类似于background_color,但在您的应用程序处于活动状态时,它会为 Android 上的工具栏设置样式。一个不错的点缀:
"theme_color": "#e05a47",
  • display:正如我们之前所说,PWA 可以像本机应用程序一样启动,即浏览器栏被隐藏;这就是这个属性的作用。如果您认为让用户能够看到地址栏更好,可以将其设置为“browser”:
"display": "standalone"

其他属性

还有一些属性需要您了解我们的应用程序:

  • related_applications:您可以提供与您的 Web 应用程序相关的本机应用程序的列表,并附带下载的 URL;将其与prefer_related_applications配对使用。

  • prefer_related_applications:一个默认值为 false 的布尔值。如果为 true,则用户将收到有关相关应用程序的通知。

  • scope:一个字符串,比如/app。如果用户导航到范围之外的页面,应用程序将返回到浏览器中常规网页的外观。

  • description:您的应用程序的描述;不是强制性的。

  • dir:类型的方向。

  • langshort_name的语言。与dir配对使用,可用于确保从右到左的语言正确显示。

链接我们的清单

就是这样!最后,您的manifest.json应该是这样的:

{
  "name": "Chatastrophe",
  "short_name": "Chatastrophe",
  "icons": [
    {
      "src":"/assets/icon.png",
      "sizes": "192x192",
      "type": "image/png"
    },
    { 
      "src": "/assets/icon-256.png", 
      "sizes": "256x256", 
      "type": "image/png" 
    }, 
    { 
      "src": "/assets/icon-384.png", 
      "sizes": "384x384", 
      "type": "image/png" 
    }, 
    { 
      "src": "/assets/icon-512.png", 
      "sizes": "512x512", 
      "type": "image/png" 
    }
  ],
  "start_url": "/?utm_source=homescreen",
  "background_color": "#e05a47",
  "theme_color": "#e05a47",
  "display": "standalone"
}

然后,您可以像这样从您的index.html中链接它:

<link rel="manifest" href="/manifest.json">

确保您也将其复制到您的build文件夹中。

如果一切顺利,并且您使用的是最新版本的 Chrome,您可以通过转到 Chrome Dev Tools 中的“应用程序”选项卡来检查是否正常工作。确保首先重新启动服务器。您应该会看到以下内容:

现在来测试一下吧!让我们再次运行我们的部署过程,使用**yarn deploy**。完成后,转到您的 Android 设备上的应用程序。为了触发 Web 应用程序安装横幅,您需要访问该站点两次,每次访问之间间隔五分钟:

如果您没有看到安装横幅,您也可以通过转到选项下拉菜单并选择“添加到主屏幕”来安装它。

一旦您点击“添加到主屏幕”,您应该会看到它出现:

然后,当我们启动时,我们会得到一个漂亮的启动画面:

这很可爱。

这就是为 Android 制作可安装的 PWA 的要点。这是一个非常简洁流畅的过程,这要感谢 Google 对 PWA 的倡导,但我们的许多用户无疑会使用 iPhone,因此我们也必须确保我们也支持他们。

使我们的应用可安装- iOS

截至撰写本文时,苹果尚未支持渐进式 Web 应用程序。关于这一点有许多理论(他们的盈利能力强大的 App Store 生态系统,与谷歌的竞争,缺乏控制),但这意味着使我们的应用可安装的过程要更加手动化。

让我们明确一点-截至目前,PWA 的最佳体验将是针对使用最新版本 Chrome 的 Android 设备用户。

然而,PWA 也是关于渐进式增强的,这是我们将在后面的章节中更深入地介绍的概念。渐进式增强意味着我们为每个用户在其设备上提供最佳的体验;如果他们可以支持所有新的功能,那很好,否则,我们会尽力利用他们正在使用的工具。

因此,让我们来看看如何使我们的 UX 对于想要将我们的应用保存到主屏幕的 iPhone 用户来说是愉快的。

我们将使用大量的<meta>标签来告诉浏览器我们的应用是可安装的。让我们从图标开始:

<link rel="apple-touch-icon" href="/assets/icon.png">

将以下内容添加到public/index.html(在本节的其余部分中,将所有的meta标签分组放在link标签之上)。这定义了用户主屏幕上的图标。

接下来,我们为页面添加一个标题,这将作为主屏幕上应用程序的名称。在您的link标签之后添加这个:

<title>Chatastrophe</title>

然后,我们需要让 iOS 知道这是一个 Web 应用程序。您可以使用以下meta标签来实现:

<meta name="apple-mobile-web-app-capable" content="yes">

就像我们在 Android 部分中使用theme_color一样,我们希望样式化状态栏的外观。默认值是黑色,看起来像这样:

另一个选项是 black-translucent,它并不是非常黑,主要是半透明的:

使用以下内容添加:

<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">

我们要做的最后一件事是设计启动画面;在应用程序启动时出现的内容。

在 iOS 上进行此操作有点手动--您需要提供一个静态图像。

为了完全支持,您需要为每个 iOS 屏幕尺寸提供单独的启动图像,从 iPad 到最小的 iPhone。如果您想看到多个启动图像和图标的绝佳示例,请查看gist 链接。这里包括了该 gist 中的启动图像链接:

    <!-- iPad retina portrait startup image -->
    <link href="https://placehold.it/1536x2008"
          media="(device-width: 768px) and (device-height: 1024px)
                 and (-webkit-device-pixel-ratio: 2)
                 and (orientation: portrait)"
          rel="apple-touch-startup-image">

    <!-- iPad retina landscape startup image -->
    <link href="https://placehold.it/1496x2048"
          media="(device-width: 768px) and (device-height: 1024px)
                 and (-webkit-device-pixel-ratio: 2)
                 and (orientation: landscape)"
          rel="apple-touch-startup-image">

    <!-- iPad non-retina portrait startup image -->
    <link href="https://placehold.it/768x1004"
          media="(device-width: 768px) and (device-height: 1024px)
                 and (-webkit-device-pixel-ratio: 1)
                 and (orientation: portrait)"
          rel="apple-touch-startup-image">

    <!-- iPad non-retina landscape startup image -->
    <link href="https://placehold.it/748x1024"
          media="(device-width: 768px) and (device-height: 1024px)
                 and (-webkit-device-pixel-ratio: 1)
                 and (orientation: landscape)"
          rel="apple-touch-startup-image">

    <!-- iPhone 6 Plus portrait startup image -->
    <link href="https://placehold.it/1242x2148"
          media="(device-width: 414px) and (device-height: 736px)
                 and (-webkit-device-pixel-ratio: 3)
                 and (orientation: portrait)"
          rel="apple-touch-startup-image">

    <!-- iPhone 6 Plus landscape startup image -->
    <link href="https://placehold.it/1182x2208"
          media="(device-width: 414px) and (device-height: 736px)
                 and (-webkit-device-pixel-ratio: 3)
                 and (orientation: landscape)"
          rel="apple-touch-startup-image">

    <!-- iPhone 6 startup image -->
    <link href="https://placehold.it/750x1294"
          media="(device-width: 375px) and (device-height: 667px)
                 and (-webkit-device-pixel-ratio: 2)"
          rel="apple-touch-startup-image">

    <!-- iPhone 5 startup image -->
    <link href="https://placehold.it/640x1096"
          media="(device-width: 320px) and (device-height: 568px)
                 and (-webkit-device-pixel-ratio: 2)"
          rel="apple-touch-startup-image">

    <!-- iPhone < 5 retina startup image -->
    <link href="https://placehold.it/640x920"
          media="(device-width: 320px) and (device-height: 480px)
                 and (-webkit-device-pixel-ratio: 2)"
          rel="apple-touch-startup-image">

    <!-- iPhone < 5 non-retina startup image -->
    <link href="https://placehold.it/320x460"
          media="(device-width: 320px) and (device-height: 480px)
                 and (-webkit-device-pixel-ratio: 1)"
          rel="apple-touch-startup-image">

您可能注意到这些链接不包括任何 iPhone 6 Plus 之后的 iPhone。在撰写本文时,iOS 9 对启动图像的支持有问题,iOS 10 则不支持。虽然这不会影响您的应用程序的用户体验(启动画面本来也只能看一秒钟),但这表明了苹果对 PWA 的支持不完全。希望这在不久的将来会发生改变。

总的来说,将您的应用程序制作成 iOS 可安装的 Web 应用程序并不像manifest.json那样花哨或直观,但相当简单。使用**yarn deploy**重新部署您的应用程序,然后在 iPhone 上的 Safari 中打开网页。然后,点击分享并添加到主屏幕:

它应该会出现在您的主屏幕上,就像普通的应用程序一样,并且在启动时会出现如下:

这非常漂亮。

最终的index.html应该是这样的:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta name="viewport" content="width=device-width, initial-scale=1">
    <meta charset="utf-8">
    <meta name="apple-mobile-web-app-capable" content="yes">
    <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
    <link rel="shortcut icon" href="assets/favicon.ico" type="image/x-icon">
    <link rel="manifest" href="/manifest.json">
    <link rel="apple-touch-icon" href="/assets/icon.png">
    <title>Chatastrophe</title>
  </head>
  <body>
    <div id="root"></div>
    <script src="/secrets.js"></script>
    <script src="https://www.gstatic.com/firebasejs/4.3.0/firebase.js"></script>
    <script>
      // Initialize Firebase
      var config = {
        apiKey: window.apiKey,
        authDomain: "chatastrophe-draft.firebaseapp.com",
        databaseURL: "https://chatastrophe-draft.firebaseio.com",
        projectId: "chatastrophe-draft",
        storageBucket: "chatastrophe-draft.appspot.com",
        messagingSenderId: window.messagingSenderId
      };
      window.firebase = firebase;
      firebase.initializeApp(config);
    </script>
  </body>
</html>

应用安装横幅和您

能够添加到主屏幕是一个很棒的功能,但是我们的用户如何知道我们的应用程序是可安装的,特别是如果他们从未听说过 PWA 呢?

进入Web App Install Banner。 以前,应用安装横幅是一种方便的方式来宣传您的原生应用程序-请参阅 Flipboard 的以下示例:

然而,现在,谷歌正在带头推动 PWA 安装横幅,提示用户添加到主屏幕。 请参阅 Chrome Dev Summit 网站的以下示例:

该横幅具有使用户意识到您的网站是 PWA 的优势,并且对于那些不熟悉可安装的 Web 应用程序的用户,提供了进入 PWA 世界的入口点。

当您点击上一个屏幕截图中的“添加”时,您的主屏幕上会显示如下内容:

然而,就像本节中的所有内容一样,这是一项新技术。 目前,仅在安卓上的 Chrome 和 Opera for Android 上存在牢固的支持。 此外,两个浏览器上安装横幅将出现的具体标准也是牢固的:

  • 该应用程序必须具有 Web 应用程序清单

  • 该应用程序必须通过 HTTPS 提供

  • 该应用程序必须使用服务工作者

  • 该应用程序必须被访问两次,访问之间至少间隔五分钟

我们已经涵盖了前三个条件(Firebase 应用程序会自动通过 HTTPS 部署)。 最后一个标准是尽量减少用户的烦恼。

延迟应用程序安装横幅

以下部分仅适用于您拥有安卓设备进行测试,并且安装了最新版本的 Chrome 或 Opera for Android。 您还需要为您的安卓设备设置远程调试,按照以下指南进行操作:developers.google.com/web/tools/chrome-devtools/remote-debugging/

我们之前提到的 PWA 的优势之一是用户在决定是否安装之前有机会与您的应用程序进行交互。 如果 Web 应用程序安装横幅显示得太早(在用户与您的应用程序进行积极交互之前),可能会干扰该过程。

在本节中,我们将通过延迟 Web 应用程序安装横幅事件来解决这个问题,直到用户与我们的应用程序进行积极交互。

我们将向我们的App.js添加一个事件侦听器,以便在横幅显示事件准备好触发时进行监听。 然后,我们将拦截该事件,并在用户发送消息时保存它。

监听事件

Chrome 在显示 Web 应用程序安装横幅之前直接发出beforeinstallprompt事件。这就是我们要监听的事件。像我们的其他 Firebase 事件监听器一样,让我们将其添加到我们的App.jscomponentDidMount中。

我们将创建一个名为listenForInstallBanner的方法,然后从componentDidMount中调用该方法:

componentDidMount() {
  firebase.auth().onAuthStateChanged(user => {
    if (user) {
      this.setState({ user });
    } else {
      this.props.history.push('/login');
    }
  });
  firebase
    .database()
    .ref('/messages')
    .on('value', snapshot => {
      this.onMessage(snapshot);
      if (!this.state.messagesLoaded) {
        this.setState({ messagesLoaded: true });
      }
    });
  this.listenForInstallBanner();
}
listenForInstallBanner = () => {

};

listenForInstallBanner中,我们将做两件事:

  1. 为事件注册一个监听器。

  2. 当该事件触发时,取消它并将其存储以便以后使用。

将其存储以便以后我们可以在任何时候触发它,也就是当用户发送他们的第一条消息时。

代码如下:

listenForInstallBanner = () => {
  window.addEventListener('beforeinstallprompt', (e) => {
    console.log('beforeinstallprompt Event fired');
    e.preventDefault();
    // Stash the event so it can be triggered later.
    this.deferredPrompt = e;
  });
};

我们将在App实例上存储我们的deferredPrompt,以便以后可以获取它。我们将在handleSubmitMessage方法中执行这个操作:

handleSubmitMessage = msg => {
  const data = {
    msg,
    author: this.state.user.email,
    user_id: this.state.user.uid,
    timestamp: Date.now()
  };
  firebase
    .database()
    .ref('messages/')
    .push(data);
  if (this.deferredPrompt) {
 this.deferredPrompt.prompt();
 this.deferredPrompt.userChoice.then(choice => {
 console.log(choice);
 });
 this.deferredPrompt = null;
 }
};

在我们提交消息后,我们触发我们保存的事件。然后,我们记录用户的选择(无论他们是否实际安装了应用程序,我们也可以将其发送到将来选择使用的任何分析工具)。最后,我们删除事件。

好的,让我们测试一下!

将您的 Android 设备连接到计算机上,并在 DevTools 上打开远程调试。我们首先必须部署我们的应用程序,所以点击yarn deploy并等待它完成。然后,在您的设备上打开应用程序并输入一条消息;您应该会看到应用程序安装横幅弹出。

如果没有出现,请检查您的代码,或转到 DevTools 的应用程序选项卡,然后单击“添加到主屏幕”按钮。这应该会触发beforeinstallprompt事件。

总结

Web 应用程序安装横幅仍然是一项新技术,标准仍在不断变化中。有关最新信息,请参阅 Google 关于 Web 应用程序安装横幅的页面-developers.google.com/web/fundamentals/engage-and-retain/app-install-banners/。也就是说,我希望本章对横幅的可能性和当前技术状态有所帮助。

现在我们已经使我们的应用程序更大更好,是时候精简并专注于性能了。下一章见!

第十章:应用外壳

我们上一章讨论了添加主屏幕安装和推送通知,这两者都旨在通过添加功能来改善用户体验,但正如我们在书的开头描述的用户故事一样,这个应用最重要的特性之一是包容性;它是一个面向所有人的聊天应用。

从 Web 应用的角度来看,我们可以更好地重新表述为“任何连接,任何速度”。Web 应用性能的最大障碍是网络请求:在慢速连接下加载数据需要多长时间。

开发人员可能会忽视性能,仅仅因为我们通常在城市中心的空调建筑内快速连接上测试我们的网站。然而,对于像 Chatastrophe 这样的全球应用,我们必须考虑在不发达国家的用户、农村地区的用户以及只有我们十分之一网络速度的用户。我们如何让应用为他们工作?

本节重点讨论性能;具体来说,它是关于优化我们的应用,使其在最恶劣的条件下也能表现良好。如果我们做得好,我们将拥有一个强大的用户体验,适用于任何速度(或缺乏速度)。

在本章中,我们将涵盖以下内容:

  • 渐进增强是什么

  • 性能的 RAIL 模型

  • 使用 Chrome DevTools 来衡量性能

  • 将我们的应用外壳从 React 中移出

什么是渐进增强?

渐进增强是一个简单的想法,但影响深远。它源于提供出色用户体验的愿望,同时又需要性能。如果我们所有的用户都有完美、超快的连接,我们可以构建一个令人难以置信的应用。然而,如果我们所有的用户都有慢速连接,我们必须满足于更简化的体验。

渐进增强说为什么不两者兼得?为什么不两者都有?

我们的受众包括快速连接和慢速连接。我们应该为两者提供服务,并适当地为每个人提供服务,这意味着为最佳连接提供最佳体验,为较差的连接提供更简化(但仍然很棒)的体验,以及介于两者之间的一切。

简而言之,渐进增强意味着随着用户的连接改善,我们的应用会逐渐变得更好,但它始终是有用的和可用的。因此,我们的应用是一种适应连接的应用*。*

您可以想象这正是现代网页加载的方式。首先,我们加载 HTML——内容的基本、丑陋的骨架。然后,我们添加 CSS 使其变得漂亮。最后,我们加载 JavaScript,其中包含使其生动的所有好东西。换句话说,随着网站的加载,我们的应用程序会逐渐变得更好。

渐进增强范式敦促我们重新组织网站的内容,以便重要的内容尽快加载,然后再加载其他功能。因此,如果您使用的是超快速的连接,您会立即得到所有内容;否则,您只会得到使用应用程序所需的内容,其他内容稍后再加载。

因此,在本章中,我们将优化我们的应用程序,尽快启动。我们还将介绍许多工具,您可以使用这些工具来关注性能,并不断增强性能,但是我们如何衡量性能呢?我们可以使用哪些指标来确保我们提供了一个快速的应用程序?RAIL 模型应运而生。

RAIL 模型

RAIL 是谷歌所称的“以用户为中心的性能模型”。这是一组衡量我们应用性能的指南。我们应该尽量避免偏离这些建议。

我们将使用 RAIL 的原则来加快我们的应用程序,并确保它对所有用户都表现良好。您可以在developers.google.com/web/fundamentals/performance/rail上阅读谷歌关于 RAIL 的完整文档。

RAIL 概述了应用程序生命周期中的四个特定时期。它们如下:

  • 响应

  • 动画

  • 空闲

  • 加载

就我个人而言,我认为以相反的顺序来思考它们会更容易(因为这更符合它们的实际顺序),但那样会拼成 LIAR,所以我们可以理解为什么谷歌会回避这一点。无论如何,在这里我们将以这种方式来介绍它们。

加载

首先,您的应用程序加载(让光明降临!)。

RAIL 表示,最佳加载时间为一秒(或更短)。这并不意味着您的整个应用程序在一秒内加载完成;而是意味着用户在一秒内看到内容。他们会对当前任务(加载页面)有一定的感知,而不是盯着一片空白的白屏。正如我们将看到的,这并不容易做到!

空闲

一旦您的应用程序加载完成,它就是空闲的(在操作之间也会是空闲的),直到用户执行操作。

RAIL 认为,与其让你的应用程序闲置不用(懒惰!),我们应该利用这段时间继续加载应用程序的部分。

我们将在下一章中更详细地看到这一点,但如果我们的初始加载只是我们应用程序的基本版本,我们会在空闲时间加载其他内容(渐进增强!)。

动画

动画对我们的目的来说不太相关,但我们将在这里简要介绍一下。基本上,如果动画不以 60 帧每秒的速度执行,用户会注意到动画的延迟。这将对感知性能(用户对应用程序速度的感受)产生负面影响。

请注意,RAIL 还将滚动和触摸手势定义为动画,因此即使你没有动画,如果你的滚动有延迟,你就会有问题。

响应

最终(希望非常快!),用户执行一个操作。通常,这意味着点击按钮、输入或使用手势。一旦他们这样做,你有 100 毫秒的时间来提供一个响应,以确认他们的行动;否则,用户会注意到并感到沮丧,也许会重试该操作,从而在后续造成更多问题(我们都经历过这种情况——疯狂地双击和三击)。

请注意,如果需要进行一些计算或网络请求,某些操作将需要更长的时间来完成。你不需要在 100 毫秒内完成操作,但你必须提供一些响应;否则,正如Meggin Kearney所说,“行动和反应之间的连接就断了。用户会注意到。”

时间轴

正如前面的模型所示,我们的应用程序必须在一定的时间限制内运行。这里有一个方便的参考:

  • 16 毫秒:任何动画/滚动的每帧时间。

  • 100 毫秒:对用户操作的响应。

  • 1000 毫秒以上:在网页上显示内容。

  • 1000 毫秒以上:用户失去焦点。

  • 10,000 毫秒以上:用户可能会放弃页面。

如果你的应用程序按照这些规范执行,你就处于一个良好的状态(这些并不容易做到,正如我们将看到的)。

使用时间轴进行测量

在这一部分,我们将看看如何使用 Chrome DevTools 来分析我们应用程序的性能,这是我们将使用的一些工具中的第一个,用来跟踪我们的应用程序加载和响应的方式。

一旦我们了解了它的性能,我们可以根据 RAIL 原则进行改进。

开发工具当然是一直在不断发展的,所以它们的外观可能会与给定的截图有所不同。然而,核心功能应该保持不变,因此,重要的是要密切关注工作原理。

在 Chrome 中打开部署的 Firebase 应用程序,并打开 DevTools 到性能标签(我建议通过右上角的下拉菜单将工具拖出到单独的窗口中,因为有很多内容要查看);然后,刷新页面。页面加载完成后,您应该看到类似以下内容:

这里有很多内容,让我们来分解一下。我们将从摘要标签开始,底部的圆形图表。

摘要标签

中间的数字是我们的应用程序完全加载所花费的时间。您的数字应该与我的类似,根据您的互联网速度会有一些变化。

到目前为止,这里最大的数字是脚本,几乎达到了 1000 毫秒。由于我们的应用程序使用 JavaScript 很多,这是有道理的。我们立刻就能看到我们大部分的优化应该集中在尽快启动我们的脚本上。

另一个重要的数字是空闲时间的数量(几乎与脚本时间一样多)。我们马上就会看到为什么会有这么多空闲时间,但请记住,RAIL 模型建议利用这段时间开始预加载尚未加载的应用程序部分。目前,我们一开始就加载了所有内容,然后启动所有内容,然后坐在那里一会儿。只加载我们需要的内容(从而减少脚本时间),然后在后台加载其余内容(从而减少空闲时间)将更有意义。

网络请求

我们现在将转到网络请求,因为这将有助于解释性能概况的其余部分。

在这里,您可以看到确切加载了什么数据以及何时加载。一开始,我们看到了很多设置文件:Firebase 应用和messaging库,我们的bundle.js,以及页面的实际文档。

稍后,两个重要的调用是为了用户:登录和加载用户详细信息。我们加载的最后一件事是清单。

这个顺序是有道理的。我们需要加载 Firebase 库和我们的 JavaScript 来启动我们的应用程序。一旦我们这样做,我们就开始登录过程。

接下来发生的事情是,一旦用户登录,我们就会收到来自 Firebase 的消息和数据。正如您所注意到的,这在图表上并没有显示出来,因为它是通过 WebSockets 实时完成的,所以它并不是一个网络请求。然而,它将影响到其余的性能概况,所以请记住这一点。

瀑布

在这里,我们可以详细了解 Chrome 在渲染过程中实际在做什么。

瀑布工具是详细和复杂的,所以我们只能对其进行表面浏览。然而,我们可以从中得出两个见解。首先,我们可以看到所有的空闲时间可视化。大部分是在开始时,这在我们首次加载文档时有些不可避免,但在中间有一个很大的空白,我们可以尝试填补它。

其次,您可以看到应用程序在右侧瀑布图中接收来自 Firebase 的消息。如果您将鼠标悬停在每个块上,实际上可以追踪 Firebase 接收消息并将其状态设置为消息数组的过程。

因此,虽然我们无法在网络请求中看到消息加载,但我们可以在 JavaScript 执行中看到响应。

屏幕截图

这是我最喜欢的性能工具部分,因为它生动地说明了您的应用程序是如何加载的。

正如我们之前所建立的,用户应该在加载您的应用程序后的 1000 毫秒内看到内容。在这里,我们可以看到应用程序上的内容首先出现大约在 400 毫秒左右,所以我们看起来不错,但随着我们的应用程序增长(和我们的脚本负担增加),情况可能会改变,所以现在是尽可能优化的时候了。

PageSpeed Insights

性能工具非常棒,因为它们让我们深入了解应用程序加载的细节。我们将使用它们来跟踪我们应用程序的性能,但是,如果我们想要更具体、更详细的建议,我们可以转向 Google 提供的PageSpeed Insights工具。

转到 PageSpeed Insights(developers.google.com/speed/pagespeed/insights/)并输入您部署的应用程序的 URL。几秒钟后,您将收到关于 Chatastrophe 可以改进的建议:

正如你所看到的,我们的移动性能急需帮助。大部分见解都集中在我们的阻塞渲染 JavaScript 和 CSS 上。我鼓励你阅读关于这些问题的描述,并尝试自行解决它们。在下一节中,我们将致力于根据谷歌的规范改进我们的应用程序,使用另一个渐进式 Web 应用程序的秘密武器——应用外壳模式。

应用外壳模式

我们应用程序的核心是消息列表和聊天框,用户在其中阅读和编写消息。

这个核心功能依赖于 JavaScript 来工作。我们无法绕过这样一个事实,即在用户通过 Firebase 进行身份验证并加载消息数组之前,我们无法显示消息,但是围绕这两个部分的一切大多是静态内容。在每个视图中都是相同的,并且不依赖于 JavaScript 来工作:

我们可以将这称为应用外壳——围绕功能性、由 JavaScript 驱动的核心的框架。

由于这个框架不依赖 JavaScript 来运行,实际上我们不需要等待 React 加载和启动所有 JavaScript,然后再显示它——这正是目前正在发生的事情。

现在,我们的外壳是我们的 React 代码的一部分,因此,在调用ReactDOM.render并在屏幕上显示之前,我们所有的 JavaScript 都必须加载。

然而,对于我们的应用程序,以及许多应用程序来说,UI 中有一个相当大的部分基本上只是 HTML 和 CSS。此外,如果我们的目标是减少感知加载时间(用户认为加载应用程序需要多长时间)并尽快将内容显示在屏幕上,最好将我们的外壳保持为纯粹的 HTML 和 CSS,即将其与 JavaScript 分离,这样我们就不必等待 React。

回到我们的性能工具,你可以看到加载的第一件事是文档,或者我们的index.html

如果我们可以将我们的外壳放在index.html中,它将比目前快得多,因为它不必等待捆绑包加载。

然而,在开始之前,让我们进行基准测试,看看我们目前的情况以及这将带来多大的改进。

使用你部署的应用程序,打开我们的性能工具并刷新应用程序(在 DevTools 打开时使用 Empty Cache & Hard Reload 选项,以确保没有意外的缓存发生-按住并按下重新加载按钮来访问它)。然后,看一下那个图像条,看看内容何时首次出现:

运行测试三次,以确保,并取平均值。对我来说,平均需要 600 毫秒。这是我们要超越的基准。

将 shell HTML 从 React 中移出

让我们首先定义我们想要移动到我们的index.html中的内容。

在下面的图像中,除了消息和聊天框线之外的所有内容都是我们的应用程序 shell:

这就是我们想要从 React 中移出并转换为纯 HTML 的内容,但在继续之前让我们澄清一些事情。

我们的目标是创建一个快速加载的应用程序部分的版本,这些部分不需要立即使用 JavaScript,但最终,我们的一些 shell 将需要 JavaScript。我们需要在页眉中放置我们的注销按钮,这将需要 JavaScript 来运行(尽管只有在用户经过身份验证后才需要)。

因此,当我们谈论将这些内容从 React 中移出时,我们实际上要做的是有一个纯 HTML 和 CSS 版本的 shell,然后,当 React 初始化时,我们将用 React 版本替换它。

这种方法给了我们最好的两种世界:一个快速加载基础版本,一旦 JS 准备好,我们就会替换掉它。如果这听起来很熟悉,你也可以称之为逐步增强我们的应用程序。

那么,我们如何管理这个替换呢?嗯,让我们从打开我们的index.html开始,看看我们的应用程序是如何初始化的:

关键是我们的div#root。正如我们在index.js中看到的那样,那是我们注入 React 内容的地方:

现在,我们将我们的 React 内容嵌入到一个空的div中,但让我们尝试一些东西;在里面添加一个<h1>

<div id="root">
  <h1>Hello</h1>
</div>

然后,重新加载你的应用程序:

<h1>出现直到我们的 React 准备好,此时它被替换,所以我们可以在div#root内添加内容,当 React 准备好时,它将被简单地覆盖;这就是我们的关键。

让我们逐步移动内容,从我们的App.js开始,逐渐向下工作:

我们这里唯一需要的 HTML(或 JSX,目前)是容器。让我们将它复制到div#root中:

<div id="root">
  <div id="container">
  </div>
</div>

然后,在ChatContainer(或LoginContainer,或UserContainer)内部,我们看到有一个div.inner-container,也可以移动过去:

<div id="root">
  <div id="container">
    <div class="inner-container">
    </div>
  </div>
</div>

注意从className(对于 JSX)到class(对于 HTML)的更改。

然后,我们移动Header本身:

<div id="root">
  <div id="container">
     <div class="inner-container">
       <div id="Header">
         <img src="/assets/icon.png" alt="logo" />
         <h1>Chatastrophe</h1>
       </div>
     </div>
  </div>
</div>

重新加载您的应用程序,您将看到我们的 HTML 的一个非常丑陋的版本在 React 加载之前出现:

这里发生了什么?嗯,我们的 CSS 是在我们的App.js中加载的,在我们的导入语句中,因此直到我们的 React 准备好之前它都不会准备好。下一步将是将相关的 CSS 移动到我们的index.html中。

将 CSS 移出 React

目前,我们的应用程序没有太多的 CSS,所以理论上,我们可以只是在index.html<link>整个样式表,而不是在App.js中导入它,但随着我们的应用程序和 CSS 的增长,这将不是最佳选择。

我们最好的选择是内联相关的 CSS。我们首先在<head>下方的<title>标签右侧添加一个<style>标签。

然后,打开src/app.css,并剪切(而不是复制)/* Start initial styles *//* End Initial styles */注释内的 CSS。

将其放在样式标签内并重新加载应用程序:

应用程序看起来完全一样!这是个好消息;在这个阶段,可能不会有明显的加载时间差异。然而,让我们部署然后再次运行我们的性能工具:

正如您所看到的,外壳(带有空白内部)出现在加载指示器出现之前(这表明 React 应用程序已经启动)。这是用户通常会花在空白屏幕上的时间。

移动加载指示器

让我们再向前迈进一小步,还将加载指示器添加到我们的应用程序外壳中,以让用户了解发生了什么。

复制ChatContainer中的 JSX 并将其添加到我们的index.html。然后,重新加载页面:

<div id="root">
  <div id="container">
    <div class="inner-container">
      <div id="Header">
        <img src="/assets/icon.png" alt="logo" />
        <h1>Chatastrophe</h1>
      </div>
      <div id="loading-container">
        <img src="/assets/icon.png" alt="logo" id="loader"/>
      </div>
    </div>
  </div>
</div>

现在,用户可以清楚地感觉到应用程序正在加载,并且会更宽容地对待我们应用程序的加载时间(尽管我们仍然会尽力减少它)。

这是从本章中获得的基本原则:渐进式 Web 应用程序要求我们尽可能多地改善用户体验。有时,我们无法做任何关于加载时间的事情(归根结底,我们的 JavaScript 总是需要一些时间来启动--一旦它启动,它就提供了很好的用户体验),但我们至少可以让用户感受到进展。

良好的网页设计是关于共情。渐进式 Web 应用程序是关于对每个人都持有共情,无论他们从什么条件下访问您的应用程序。

总结

在本章中,我们涵盖了性能工具和概念的基本知识,从 RAIL 到 DevTools,再到 PageSpeed Insights。我们还使用了应用程序外壳模式进行了重大的性能改进。在接下来的章节中,我们将继续完善我们应用的性能。

我们下一章将解决最大的性能障碍——我们庞大的 JavaScript 文件。我们将学习如何使用 React Router 的魔力将其拆分成较小的块,并且如何在应用程序的空闲时间加载这些块。让我们开始吧!

第十一章:使用 Webpack 对 JavaScript 进行分块以优化性能

正如我们在上一章中讨论的那样,将 React 应用程序转换为渐进式 Web 应用程序的最大问题是 React;更具体地说,它是构建现代 JavaScript 应用程序时固有的大量 JavaScript。解析和运行该 JavaScript 是 Chatastrophe 性能的最大瓶颈。

在上一章中,我们采取了一些措施来改善应用程序的感知启动时间,方法是将内容从 JavaScript 移出并放入我们的index.html中。虽然这是一种非常有效的向用户尽快显示内容的方法,但您会注意到,我们并没有做任何实际改变我们的 JavaScript 大小,或者减少初始化所有 React 功能所需的时间。

现在是时候采取行动了。在本章中,我们将探讨如何将我们的 JavaScript 捆绑分割以实现更快的加载。我们还将介绍渐进式 Web 应用程序理论的一个新部分--PRPL 模式。

在本章中,我们将涵盖以下主题:

  • 什么是 PRPL 模式?

  • 什么是代码拆分,我们如何实现它?

  • 创建我们自己的高阶组件

  • 按路由拆分代码

  • 延迟加载其他路由

PRPL 模式

在上一章中,我们介绍了一些执行应用程序的基本原则。您希望用户尽可能少地等待,这意味着尽快加载必要的内容,并将其余的应用程序加载推迟到处理器的“空闲”时间。

这两个概念构成 RAIL 指标的'I'和'L'。我们通过应用外壳的概念迈出了改善'L'的一步。现在,我们将把一些'L'(初始加载)移到'I'(应用程序的空闲时间),但在我们这样做之前,让我们介绍另一个缩写。

PRPL代表推送渲染预缓存延迟加载;这是一个理想应用程序应该如何从服务器获取所需内容的逐步过程。

然而,在我们深入讨论之前,我想警告读者,PRPL 模式在撰写时相对较新,并且随着渐进式 Web 应用程序进入主流,可能会迅速发展。就像我们在本书中讨论的许多概念一样,它依赖于实验性技术,仅适用于某些浏览器。这是尖端的东西。

这就是Addy Osmani的说法:

对于大多数现实世界的项目来说,以其最纯粹、最完整的形式实现 PRPL 愿景实际上还为时过早,但采用这种思维方式或从各个角度开始追求这一愿景绝对不为时过早。 (developers.google.com/web/fundamentals/performance/prpl-pattern/)

让我们依次解释每个字母代表的意思,以及它对我们和我们的应用程序意味着什么。

推送

Addy Osmani将 PRPL 的 PUSH 定义如下:

“推送初始 URL 路由的关键资源。”

基本上,这意味着你的首要任务是尽快加载渲染初始路由所需的内容。听起来很熟悉吗?这正是我们在应用程序外壳中遵循的原则。

推送的一个温和定义可以是“在任何其他内容之前,首先加载关键内容。”这个定义与应用程序外壳模式完全吻合,但这并不完全是Osmani的意思。

以下部分是对服务器推送技术的理论介绍。由于我们无法控制我们的服务器(又名 Firebase),我们不会实施这种方法,但了解对于未来与自己的服务器通信的 PWA 是很有好处的。

如果你看一下我们的index.html,你会发现它引用了几个资产。它请求faviconicon.pngsecrets.js。在 Webpack 构建后,它还会请求我们的主 JavaScript bundle.js

网站通常的工作方式是这样的:浏览器请求index.html。一旦得到文件,它会遍历并请求服务器上列出的所有依赖项,每个都作为单独的请求。

这里的核心低效性在于index.html已经包含了关于它的依赖项的所有信息。换句话说,当它响应index.html时,服务器已经“知道”浏览器接下来会请求什么,那么为什么不预期这些请求并发送所有这些依赖项呢?

进入 HTTP 2.0 服务器推送。这项技术允许服务器对单个请求创建多个响应。浏览器请求index.html,然后得到index.html + bundle.js + icon.png,依此类推。

正如Ilya Grigorik所说,服务器推送“使内联过时”(www.igvita.com/2013/06/12/innovating-with-http-2.0-server-push/)。我们不再需要内联我们的 CSS 来节省对服务器的请求;我们可以编写我们的服务器以在单次请求中发送我们初始路由所需的一切。这是令人兴奋的事情;有关更多信息(以及快速教程),请查看上述链接。

渲染

在(理想情况下)将所有必要的资源推送到客户端之后,我们渲染我们的初始路由。同样,由于应用程序外壳模式的快速渲染,我们已经涵盖了这一点。

预缓存

一旦我们渲染了初始路由,我们仍然需要其他路由所需的资源。预缓存意味着一旦加载了这些资源,它们将直接进入缓存,如果再次请求,我们将从缓存中加载它们。

随着我们进入缓存世界,我们将在下一章中更详细地介绍这一点。

延迟加载

这就是本章的重点所在。

我们希望首先加载我们初始路由所需的资源,以尽快完成初始渲染。这意味着不会加载其他路由所需的资源。

在实际操作中,这意味着我们希望首先加载LoginContainer(如果用户尚未登录),并推迟加载UserContainer

然而,一旦渲染了初始路由并且用户可以看到登录屏幕,我们希望为未来做好准备。如果他们随后切换到UserContainer,我们希望尽快显示它。这意味着一旦加载了初始路由,我们就会在后台加载UserContainer资源。

这个过程被称为延迟加载-加载不需要立即使用的资源,但将来可能需要。

我们用来做到这一点的工具就是代码拆分。

什么是代码拆分?

代码拆分是将我们的 JavaScript 文件分割成有意义的块,以提高性能,但为什么我们需要它呢?

嗯,当用户首次访问我们的应用程序时,我们只需要当前所在路由的 JavaScript。

这意味着当它们在/login时,我们只需要LoginContainer.js及其依赖项。我们不需要UserContainer.js,所以我们希望立即加载LoginContainer.js并延迟加载UserContainer.js。然而,我们当前的 Webpack 设置创建了一个单一的bundle.js文件。我们所有的 JavaScript 都被绑在一起,必须一起加载。代码拆分是解决这个问题的一种方法。我们不再是一个单一的庞大的 JavaScript 文件,而是得到了多个 JavaScript 文件,每个路由一个。

因此,我们将得到一个用于/login,一个用于/user/:id,一个用于/的捆绑包。此外,我们还将得到另一个包含所有依赖项的main捆绑包。

无论用户首先访问哪个路由,他们都会得到该路由的捆绑包和主要捆绑包。与此同时,我们将在后台加载其他两个路由的捆绑包。

代码拆分不一定要基于路由进行,但对于我们的应用程序来说是最合理的。此外,使用 Webpack 和 React Router 进行这种方式的代码拆分相对来说是比较简单的。

事实上,只要您提供一些基本的设置,Webpack 就会自动处理这个问题。让我们开始吧!

Webpack 配置

我们之前讨论过的策略是这样的:我们希望根据路由将我们的bundle.js拆分成单独的块。

这一部分的目的是做两件事:一是为 JavaScript 的块设置命名约定,二是为条件导入添加支持(稍后会详细介绍)。

打开webpack.config.prod.js,让我们进行第一步(这仅适用于PRODUCTION构建,因此只修改我们的生产 Webpack 配置;我们不需要在开发中进行代码拆分)。

就目前而言,我们的输出配置如下:

output: {
   path: __dirname + "/build",
   filename: "bundle.js",
   publicPath: './'
},

我们在build文件夹中创建一个名为bundle.js的单个 JavaScript 文件。

让我们将整个部分改为以下内容:

output: {
   path: __dirname + "/build",
   filename: 'static/js/[name].[hash:8].js',
   chunkFilename: 'static/js/[name].[hash:8].chunk.js',
   publicPath: './'
},

这里发生了什么?

首先,我们将我们的 JavaScript 输出移动到build/static/js,仅仅是为了组织目的。

接下来,我们在我们的命名中使用了两个变量:namehashname变量是由 Webpack 自动生成的,使用了我们的块的编号约定。我们马上就会看到这一点。

然后,我们使用一个hash变量。每次 Webpack 构建时,它都会生成一个新的哈希--一串随机字母和数字。我们使用这些来命名我们的文件,这样每次构建都会有不同的文件名。这在下一章中将很重要,因为这意味着我们的用户永远不会遇到应用程序已更新但缓存仍然保留旧文件的问题。由于新文件将具有新名称,它们将被下载,而不是缓存中的任何内容。

接下来,我们将在我们的代码拆分文件(每个路由的文件)后添加一个.chunk。这并非必需,但如果您想对块进行任何特殊缓存,建议这样做。

一旦我们的代码拆分完成,所有提到的内容将更加清晰,所以让我们尽快完成吧!然而,在继续之前,我们需要在我们的 Webpack 配置中再添加一件事。

Babel 阶段 1

正如我们在 Webpack 章节中解释的那样,Babel 是我们用来允许我们使用尖端 JavaScript 功能,然后将其转译为浏览器将理解的 JavaScript 版本的工具。

在本章中,我们将使用另一个尖端功能:条件导入。然而,在开始之前,我们需要更改我们的 Babel 配置。

JavaScript 语言不断发展。负责更新它的委员会称为 TC39,他们根据 TC39 流程开发更新。它的工作方式如下:

  • 建议一个新的 JavaScript 功能,此时它被称为“阶段 0”

  • 为其工作创建一个提案(“阶段 1”)

  • 创建一个实现(“阶段 2”)

  • 它被打磨以包含(“阶段 3”)

  • 它被添加到语言中

在任何时候,每个阶段都有多个功能。问题在于 JavaScript 开发人员很不耐烦,每当他们听说一个新功能时,即使它处于第 3 阶段、第 2 阶段甚至第 0 阶段,他们也想开始使用它。

Babel 提供了一种方法来做到这一点,即其stage预设。您可以为每个阶段安装一个预设,并获得当前处于该阶段的所有功能。

我们感兴趣的功能(条件导入)目前处于第 2 阶段。为了使用它,我们需要安装适当的 babel 预设:

yarn add --dev babel-preset-stage-2

然后,在两个 Webpack 配置中,将其添加到 module | loaders | JavaScript 测试 | query | presets 下:

module: {
  loaders: [
  {
  test: /\.js$/,
  exclude: /node_modules/,
  loader: 'babel-loader',
  query: {
         presets: ['es2015','react','stage-2'],
         plugins: ['react-hot-loader/babel', 'transform-class-properties']
       }
  },

记得将其添加到webpack.config.jswebpack.config.prod.js中。我们在生产和开发中都需要它。

条件导入

搞定了这些,现在是时候问一下条件导入是什么了。

目前,我们在每个 JavaScript 文件的顶部导入所有的依赖项,如下所示:

import React, { Component } from 'react';

我们始终需要 React,所以这个导入是有意义的。它是静态的,因为它永远不会改变,但前面的意思是 React 是这个文件的依赖项,它将始终需要被加载。

目前,在App.js中,我们对每个容器都是这样做的:

import LoginContainer from './LoginContainer';
import ChatContainer from './ChatContainer';
import UserContainer from './UserContainer';

这样做意味着这些容器是App.js的依赖,所以 Webpack 将始终将它们捆绑在一起;我们无法将它们分开。

相反,我们希望在需要时有条件地导入它们。

这样做的机制有点复杂,但本质上看起来是这样的:

If (path === ‘/login’)
  import('./LoginContainer')
} else if (path === ‘/user/:id’)
  import(‘./UserContainer)
} else {
  import(‘./ChatContainer)
}

那么,我们该如何实现呢?

高阶组件

我们在第五章中讨论了高阶组件,使用 React 进行路由,讨论了来自 React Router 的withRouter;现在,我们将构建一个,但首先,让我们快速复习一下。

高阶组件在 React 中是一个非常有用的模式。如果你学会了如何使用它们,你将打开一系列可能性,使得大型代码库易于维护和可重用,但它们并不像常规组件那样直观,所以让我们确保我们充分涵盖它们。

在最基本的层面上,高阶组件是一个返回组件的函数。

想象一下我们有一个button组件:

function Button(props) {
 return <button color={props.color}>Hello</button>
}

如果你更熟悉class语法,也可以用这种方式来写:

class Button extends Component {
 render() {
   return <button color={this.props.color}>Hello</button>
 }
}

我们使用一个颜色属性来控制文本的颜色。假设我们在整个应用程序中都使用这个按钮。通常情况下,我们发现自己将文本设置为红色--大约 50%的时间。

我们可以简单地继续将color=”red”属性传递给我们的按钮。在这个假设的例子中,这将是更好的选择,但在更复杂的用例中,我们也可以制作一个高阶组件(正如我们将看到的)。

让我们创建一个名为RedColouredComponent的函数:

function colorRed(Component) {
  return class RedColoredComppnent extends Component {
    render () {
      return <Component color="red" />
    }
  }
}

该函数接受一个组件作为参数。它所做的就是返回一个组件类,然后返回该组件并应用color=”red”属性。

然后,我们可以在另一个文件中渲染我们的按钮,如下所示:

import Button from './Button';
import RedColouredComponent from './RedColouredComponent';

const RedButton = RedColouredComponent(Button);

function App() {
 return (
   <div>
     <RedButton />
   </div>
 )
}

然后,我们可以将任何组件传递给RedColouredComponent,从而创建一个红色版本。

这样做打开了新的组合世界--通过高阶组件的组合创建组件。

这毕竟是 React 的本质——用可重用的代码片段组合 UI。高阶组件是保持我们的应用程序清晰和可维护的好方法,但是足够的人为例子,现在让我们自己来做吧!

AsyncComponent

本节的目标是创建一个帮助我们进行代码拆分的高阶组件。

这个组件只有在渲染时才会加载它的依赖项,或者当我们明确告诉它要加载它时。这意味着,如果我们传递给它LoginContainer.js,它只会在用户导航到/login时加载该文件,或者我们告诉它加载它时。

换句话说,这个组件将完全控制我们的 JavaScript 文件何时加载,并打开了懒加载的世界。然而,这也意味着每当渲染一个路由时,相关文件将自动加载。

如果这听起来抽象,让我们看看它的实际应用。

在您的components/目录中创建一个名为AsyncComponent.js的新文件,并添加基本的骨架,如下所示:

import React, { Component } from 'react'

export default function asyncComponent(getComponent) {

}

asyncComponent是一个以导入语句作为参数的函数,我们称之为getComponent。我们知道,作为一个高阶组件,它将返回一个component类:

export default function asyncComponent(getComponent) {
 return class AsyncComponent extends Component {
   render() {
     return (

     )
   }
 }
}

AsyncComponent的关键将是componentWillMount生命周期方法。这是AsyncComponent将知道何时去获取依赖文件的时候。这样,组件在需要之前等待,然后加载任何文件。

然而,当我们得到组件后,我们该怎么办呢?简单,将其存储在状态中:

  componentWillMount() {
     if (!this.state.Component) {
       getComponent().then(Component => {
         this.setState({ Component });
       });
     }
   }

如果我们还没有加载组件,就去导入它(我们假设getComponent返回一个Promise)。一旦导入完成,将状态设置为导入的组件,这意味着我们的render应该是这样的:

  render() {
     const { Component } = this.state;
     if (Component) {
       return <Component {...this.props} />;
     }
     return null;
   }

所有这些对你来说应该很熟悉,除了return语句中的{...this.props}。这是 JavaScript 的展开运算符。这是一个复杂的小东西(更多信息请参见developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Spread_operator),但在这种情况下,它基本上意味着将this.props对象的所有键和值复制到Componentprops上。

通过这种方式,我们可以将 props 传递给asyncComponent返回的组件,并将它们传递给Component渲染。应用于AsyncComponent的每个 prop 都将应用于其render函数中的Component

供参考的完整组件如下:

import React, { Component } from 'react';

export default function asyncComponent(getComponent) {
 return class AsyncComponent extends Component {
   state = { Component: null };

   componentWillMount() {
     if (!this.state.Component) {
       getComponent().then(Component => {
         this.setState({ Component });
       });
     }
   }

   render() {
     const { Component } = this.state;
     if (Component) {
       return <Component {...this.props} />;
     }
     return null;
   }
 };
}

路由拆分

让我们回到App.js,把它全部整合起来。

首先,我们将消除 App 对这三个容器的依赖。用AsyncComponent的导入替换这些导入,使文件顶部看起来像这样:

import React, { Component } from 'react';
import { Route, withRouter } from 'react-router-dom';
import AsyncComponent from './AsyncComponent';
import NotificationResource from '../resources/NotificationResource';
import './app.css';

接下来,我们将定义三个load()函数,每个容器一个。这些是我们将传递给asyncComponent的函数。它们必须返回一个 promise:

const loadLogin = () => {
 return import('./LoginContainer').then(module => module.default);
};

const loadChat = () => {
 return import('./ChatContainer').then(module => module.default);
};

const loadUser = () => {
 return import('./UserContainer').then(module => module.default);
};

看,条件导入的魔力。当调用这些函数时,将导入三个 JavaScript 文件。然后我们从每个文件中获取默认导出,并用它来resolve() Promise

这意味着我们可以在App.js中重新定义我们的组件,如下所示,在前面的函数声明之后(这些函数声明在文件顶部的导入语句之后):

const LoginContainer = AsyncComponent(loadLogin);
const UserContainer = AsyncComponent(loadUser);
const ChatContainer = AsyncComponent(loadChat);

不需要其他更改!您可以保持应用程序的render语句完全相同。现在,当我们提到ChatContainer时,它指的是loadChat…周围的AsyncComponent包装器,它在需要时会获取ChatContainer.js

让我们看看它是否有效。运行yarn build,并查看输出:

我们有四个 JavaScript 文件而不是一个。我们有我们的main.js文件,其中包含App.js加上我们必需的node_modules。然后,我们有三个块,每个容器一个。

还要查看文件大小,您会发现我们并没有通过这种代码拆分获得太多好处,主文件减少了几千字节。然而,随着我们的应用程序增长,每个路由变得更加复杂,代码拆分的好处也会随之增加。这有多简单?

懒加载

懒加载是我们 PRPL 拼图的最后一块,它是利用应用程序的空闲时间来加载其余的 JavaScript 的过程。

如果您**yarn deploy**我们的应用程序并导航到 DevTools 中的网络选项卡,您将看到类似以下的内容:

我们加载我们的主文件,然后加载与当前 URL 相关的任何块,然后停止。

我们在应用程序的空闲时间内没有加载其他路由!我们需要一种方式来触发加载过程,即在初始路由渲染完成后,即App挂载后。

我想你知道这将会发生什么。在AppcomponentDidMount方法中,我们只需要调用我们的三个加载方法:

componentDidMount() {
    this.notifications = new NotificationResource(
      firebase.messaging(),
      firebase.database()
    );
    firebase.auth().onAuthStateChanged(user => {
      if (user) {
        this.setState({ user });
        this.listenForMessages();
        this.notifications.changeUser(user);
      } else {
        this.props.history.push('/login');
      }
    });
    this.listenForMessages();
    this.listenForInstallBanner();
 loadChat();
 loadLogin();
 loadUser();
  }

现在,每当我们完成渲染当前路由时,我们也会准备好其他路由。

如果您再次打开 DevTools 的性能选项卡,您将看到网络请求中反映出这一点:

在左边,底部的黄色块是我们加载的main.js文件。这意味着我们的应用程序可以开始初始化。在右边,三个黄色块对应我们的三个路由块。我们首先加载需要的块,然后很快加载其他两个块。

我们现在更多地利用了应用程序的空闲时间,分散了初始化应用程序的工作。

总结

在本章中,我们涵盖了很多内容,大步迈向了更高性能的应用程序。我们按路由拆分了我们的 JavaScript,并简化了加载过程,以便加载我们需要的内容,并将其推迟到空闲时间。

然而,所有这些实际上只是为下一节铺平了道路。我们需要我们的应用程序在所有网络条件下都能正常运行,甚至在没有任何网络的情况下。我们如何使我们的应用程序在离线状态下工作?

接下来,我们将深入研究缓存的世界,并进一步改善我们应用程序在任何网络条件下的性能,甚至在没有网络的情况下。

第十二章:准备好进行缓存

我们在应用程序的性能方面取得了巨大进展。我们的 JavaScript 现在根据应用程序的路由拆分成更小的块,并且在我们的应用程序有空闲时间时延迟加载不太重要的部分。我们还引入了渐进增强,尽快向用户展示内容,并学习了如何根据 RAIL 指标分析我们应用程序的性能。

然而,我们的 Web 应用程序仍然存在一个核心的低效问题。如果我们的用户离开我们的页面去其他地方(我知道,他们怎么敢),然后返回,我们又要重复同样的过程:下载index.html,下载不同的 JavaScript 包,下载图片等等。

我们要求用户每次访问页面时都下载完全相同的文件,一遍又一遍,而他们的设备有足够的内存来为我们存储这些文件。为什么我们不把它们保存到用户的设备上,然后根据需要检索呢?

欢迎来到缓存。在本章中,我们将涵盖以下内容:

  • 什么是缓存?

  • 缓存 API

  • 在我们的服务工作者中使用缓存 API

  • 测试我们的缓存

什么是缓存?

缓存是减少网络请求或计算的行为。后端缓存可能包括保存严格计算的结果(比如生成统计数据),这样当客户端第二次请求时,我们就不必再次进行计算。客户端缓存通常包括保存网络请求的响应,这样我们就不必再次发起请求。

正如我们之前所说,服务工作者是位于我们应用程序和网络之间的代码片段。这意味着它们非常适合缓存,因为它们可以拦截网络请求并用所请求的文件进行响应,从缓存中获取文件,而不是从服务器获取;节省了时间。

从更广泛的角度来看,你可以将缓存视为不必重复做同样的事情,使用内存来存储结果。

使用渐进式 Web 应用程序进行缓存的好处在于,由于缓存存储在设备内存中,无论网络连接如何,它都是可用的。这意味着无论设备是否连接,都可以访问缓存中存储的所有内容。突然间,我们的网站可以离线访问了。

对于在 Wi-Fi 区域之间切换的移动用户来说,便利因素可能是巨大的,使他们能够快速查看朋友的消息或一组方向(任何曾经没有漫游计划旅行的人都会有这种感觉)。这也不仅仅是纯离线用户的优势;对于网络时断时续或质量低劣的用户来说,能够在网络断断续续时继续工作而不丧失功能性是一个巨大的胜利。

因此,一举两得,我们可以提高我们的应用程序性能,使其对所有用户都可离线使用。然而,在我们开始在 Chatastrophe 中实施缓存之前(希望不会出现灾难),让我们先看一个关于缓存重要性的故事。

缓存的重要性

2013 年,美国政府推出了healthcare.gov/,这是一个供公民注册平价医疗法案(也称为奥巴马医改)的网站。从一开始,该网站就饱受严重的技术问题困扰。对于成千上万的人来说,它根本无法加载。

公平地说,该网站承受着巨大的压力,在运营的第一个月就有大约 2,000 万次访问(来源-www.bbc.com/news/world-us-canada-24613022),但这种压力是可以预料的。

如果你正在为数百万人注册医疗保健的网站(所有人同时开始),性能可能会是你首要考虑的问题,但最终,healthcare.gov/未能交付。

作为对危机的回应(这威胁到了 ACA 的信誉),政府成立了一个团队来解决问题,有点像复仇者联盟,但是软件开发人员(所以根本不是复仇者联盟)。

考虑到该网站的目标,工程师们震惊地发现healthcare.gov/没有实施基本的缓存。没有。因此,每当用户访问该网站时,服务器都必须处理网络请求并生成回复的信息。

这种缺乏缓存产生了复合效应。第一波用户堵塞了管道,所以第二波用户看到了加载屏幕。作为回应,他们刷新屏幕,发出了越来越多的网络请求,依此类推。

一旦 Devengers 实施了缓存,他们将响应时间缩短了四分之三。从那时起,该网站甚至能够处理高峰时段的流量。

Chatastrophe 可能还没有处理healthcare.gov/级别的流量(但是……),但缓存总是一个好主意。

缓存 API

我们将使用Web 缓存 API进行缓存。

请注意,Mozilla 开发者网络将缓存 API 定义为实验性技术,截至 2017 年 8 月,它仅得到 Chrome、Firefox 和最新版本的 Opera 的支持。

API 规范有一些我们需要讨论的怪癖。首先,你可以在缓存中存储多个缓存对象。这样,我们就能够存储我们的缓存的多个版本,以我们喜欢的任何字符串命名。

也就是说,浏览器对于每个站点可以存储的数据有限制。如果缓存太满,它可能会简单地删除来自该站点的所有数据,因此我们最好存储最少量的数据。

然而,还有一个额外的困难。除非明确删除,否则缓存中的项目永远不会过期,因此如果我们不断尝试将新的缓存对象放入我们的缓存中,最终它会变得太满并删除所有内容。管理、更新和删除缓存对象完全取决于我们。换句话说,我们必须清理自己的混乱。

方法

我们将使用五种方法与缓存 API 交互:openaddAllmatchkeysdelete。在接下来的内容中,Caches将指的是缓存 API 本身,而Cache指的是特定的缓存对象,以区分在单个缓存上调用的方法与 API 本身:

  • Caches.open()接受一个缓存对象名称(也称为缓存键)作为参数(可以是任何字符串),并创建一个新的缓存对象,或者打开同名的现有缓存对象。它返回一个Promise,并将缓存对象作为参数解析,然后我们可以使用它。

  • Cache.addAll()接受一个 URL 数组。然后它将从服务器获取这些 URL,并将结果文件存储在当前的缓存对象中。它的小伙伴是Cache.add,它可以用单个 URL 做同样的事情。

  • Caches.match()接受一个网络请求作为参数(我们将在接下来看到如何获取它)。它在缓存中查找与 URL 匹配的文件,并返回一个解析为该文件的Promise。然后我们可以返回该文件,从而取代向服务器发出请求的需要。它的大哥是Caches.matchAll()

  • Caches.keys()返回所有现有缓存对象的名称。然后我们可以通过将它们的键传递给Caches.delete()来删除过时的缓存对象。

缓存 API 中的最后一个方法,我们这里不会使用,但可能会感兴趣的是Caches.put。这个方法接受一个网络请求并获取它,然后将结果保存到缓存中。如果你想缓存每个请求而不必提前定义 URL,这将非常有用。

资产清单

我们的构建过程会自动生成一个asset-manifest.json文件,其中列出了我们应用程序包含的每个 JavaScript 文件。它看起来像这样:

{
  "main.js": "static/js/main.8d0d0660.js",
  "static/js/0.8d0d0660.chunk.js": "static/js/0.8d0d0660.chunk.js",
  "static/js/1.8d0d0660.chunk.js": "static/js/1.8d0d0660.chunk.js",
  "static/js/2.8d0d0660.chunk.js": "static/js/2.8d0d0660.chunk.js"
}

换句话说,我们有一个我们想要缓存的每个 JS 文件的列表。更重要的是,资产清单会使用每个文件的新哈希更新,因此我们不必担心保持其最新。

因此,我们可以使用资产清单中的 URL 以及Cache.addAll()方法一次性缓存所有我们的 JavaScript 资产。但是,我们还需要手动将我们的静态资产(图像)添加到缓存中,但是为了这样做,我们将不得不利用我们的服务工作者生命周期方法并进行一些基本设置。

设置我们的缓存

在本节中,我们将通过我们的三个主要服务工作者生命周期事件,并在每个事件中单独与我们的缓存进行交互。最终,我们将自动缓存所有静态文件。

不过,要警告一下——在开发中使用缓存,充其量是可以容忍的,最坏的情况下是令人恼火的。我们对着屏幕大喊:“为什么你不更新?”直到我们意识到我们的缓存一直在提供旧代码;这种情况发生在我们每个人身上。在本节中,我们将采取措施避免缓存我们的开发文件,并躲避这个问题,但是在未来,请记住奇怪的错误可能是由缓存引起的。

在计算机科学中只有两件难事:缓存失效和命名事物。- Phil Karlton

另一个方法:

在计算机科学中有两个难题:缓存失效、命名事物和 off-by-1 错误。- Leon Bambrick

安装事件

当我们的服务工作者安装时,我们希望立即设置我们的缓存,并开始缓存相关的资产。因此,我们的安装事件的逐步指南如下:

  1. 打开相关的缓存。

  2. 获取我们的资产清单。

  3. 解析 JSON。

  4. 将相关的 URL 添加到我们的缓存中,以及我们的静态资产。

让我们打开firebase-messaging-sw.js并开始工作!

如果你仍然有console.log事件监听器用于安装,很好!删除console.log;否则,设置如下:

self.addEventListener('install', function() {

});

就在这个函数的上面,我们还会将我们的缓存对象名称分配给一个变量:

const CACHE_NAME = ‘v1’;

这个名称可以是任何东西,但我们希望每次部署时都提高版本,以确保旧的缓存无效,并且每个人都能获得尽可能新鲜的代码。

现在,让我们按照清单来运行。

打开缓存

在我们开始正题之前,我们需要谈谈可扩展事件。

一旦我们的服务工作线程被激活和安装,它可能会立即进入“等待”模式--等待必须响应的事件发生。然而,我们不希望它在我们正在打开缓存的过程中进入等待模式,这是一个异步操作。因此,我们需要一种告诉我们的服务工作线程的方法,“嘿,直到缓存被填充,不要认为自己完全安装了。”

我们通过event.waitUntil()来实现这一点。这个方法延长了事件的生命周期(在这里是安装事件),直到其中的所有 Promise 都被解决。

它看起来如下所示:

self.addEventListener('install', event => {
 event.waitUntil(
   // Promise goes here
 );
});

现在我们可以打开我们的缓存。我们的缓存 API 在全局变量 caches 中可用,所以我们可以直接调用caches.open()

const CACHE_NAME = 'v1';
self.addEventListener('install', event => {
 event.waitUntil(
   caches.open(CACHE_NAME)
     .then(cache => {
     });
 );
});

由于当前不存在名称为'v1'的缓存对象,我们将自动创建一个。一旦获得了该缓存对象,我们就可以进行第二步。

获取资产清单

获取资产清单听起来就像它听起来的那样:

self.addEventListener('install', event => {
 event.waitUntil(
   caches.open(CACHE_NAME)
     .then(cache => {
       fetch('asset-manifest.json')
         .then(response => {
           if (response.ok) {

           }
         })
     });
 );
});

请注意,在开发中我们不应该有 asset-manifest;在继续之前,我们需要确保请求响应是正常的,以免抛出错误。

解析 JSON

我们的asset-manifest.json返回了一些 JSON,相当令人惊讶。让我们解析一下:

self.addEventListener('install', event => {
 event.waitUntil(
   caches.open(CACHE_NAME)
     .then(cache => {
       fetch('asset-manifest.json')
         .then(response => {
           if (response.ok) {
             response.json().then(manifest => {

             });
           }
         })
     });
 );
});

现在我们有一个 manifest 变量,它是一个普通的 JavaScript 对象,与asset-manifest.json的内容相匹配。

将相关的 URL 添加到缓存

由于我们有一个 JavaScript 对象来访问 URL,我们可以挑选我们想要缓存的内容,但在这种情况下,我们想要一切,所以让我们遍历对象并获得一个 URL 数组:

response.json().then(manifest => {
  const urls = Object.keys(manifest).map(key => manifest[key]);
})

我们还想缓存index.html和我们的图标,所以让我们推入//assets/icon.png

response.json().then(manifest => {
  const urls = Object.keys(manifest).map(key => manifest[key]);
  urls.push(‘/’);
  urls.push('/assets/icon.png');
})

现在,我们可以使用cache.addAll()将所有这些 URL 添加到缓存中。请注意,我们指的是我们打开的特定缓存对象,而不是一般的 caches 变量:


self.addEventListener('install', event => {
  event.waitUntil(
    caches.open(CACHE_NAME).then(cache => {
      fetch('asset-manifest.json').then(response => {
        if (response.ok) {
          response.json().then(manifest => {
            const urls = Object.keys(manifest).map(key => manifest[key]);
            urls.push('/');
            urls.push('/assets/icon.png');
            cache.addAll(urls);
          });
        }
      });
    })
  );
});

完成!我们已经进行了缓存,但目前还不值得多少,因为我们还没有办法从缓存中检索项目。让我们接着做。

获取事件

当我们的应用程序从服务器请求文件时,我们希望在服务工作线程内拦截该请求,并用缓存的文件进行响应(如果存在)。

我们可以通过监听 fetch 事件来实现这一点,如下所示:

self.addEventListener('fetch', event => {

});

作为参数传递的事件有两个有趣的属性。第一个是event.request,它是目标 URL。我们将使用它来查看我们的缓存中是否有该项,但事件还有一个名为respondWith的方法,基本上意味着“停止这个网络请求的进行,并用以下内容回应它。”

这里是不直观的部分--我们实质上是在调用event.respondWith后立即取消了这个 fetch 事件。这意味着如果我们的缓存中没有该项,我们必须开始另一个 fetch 请求(幸运的是,这不会触发另一个事件监听器;这里没有递归)。这是需要记住的一点。

因此,让我们调用event.respondWith,然后使用caches.match来查看我们是否有与 URL 匹配的文件:

self.addEventListener('fetch', event => {
 event.respondWith(
   caches.match(event.request).then(response => {

   });
 );
});

在这种情况下,响应要么是问题文件,要么是空。如果是文件,我们就返回它;否则,我们发起另一个 fetch 请求并返回其结果。以下是一行版本:

self.addEventListener('fetch', event => {
  event.respondWith(
    caches.match(event.request).then(response => {
      return response || fetch(event.request);
    })
  );
});

就是这样!现在我们资产清单中的文件的所有 fetch 请求都将首先进入缓存,只有在该文件不在缓存中时才会进行实际的网络请求。

激活事件

激活事件是我们三个 service worker 事件中发生的第一个,所以可能看起来奇怪我们最后才谈论它,但这是有原因的。

激活事件是我们进行缓存清理时发生的。我们确保清除任何过期的缓存对象,以便我们的浏览器缓存不会变得太混乱并被终止。

为此,我们基本上删除任何名称与CACHE_NAME的当前值不匹配的缓存对象。

“但是,Scott,”你说,“如果我们的 service worker 没有正确更新,并且仍然包含旧的CACHE_NAME怎么办?”这是一个有效的观点。然而,正如所说的,我们的 service worker 应该在它与上一个 service worker 之间有字节大小的差异时自动更新,所以这不应该成为一个问题。

这次我们的过程不那么密集,但我们仍然来分解一下:

  1. 获取缓存名称列表。

  2. 循环遍历它们。

  3. 删除任何键不匹配CACHE_NAME的缓存。

一个快速提醒--如果你想将你的 CSS 保存在一个单独的缓存中,你可以拥有多个缓存。这样做没有真正的好处,但你可能喜欢有组织的东西。一个可行的方法是创建一个CACHE_NAMES对象,如下所示:

const VERSION = ‘v1’
const CACHE_NAMES = {
 css: `css-${VERSION}`,
 js: `js-${VERSION}`
};

然后,在随后的步骤中,我们将不得不迭代该对象;只是要记住的一些事情。

好的,让我们开始工作。

获取缓存名称列表

同样,我们必须在完成此异步代码时使用event.waitUntil()。这意味着我们最终将不得不返回一个Promiseevent.waitUntil(),这将影响我们编写代码的方式。

首先,我们通过调用cache.keys()来获取缓存键的列表,这会返回一个 promise:

self.addEventListener('activate', event => {
 event.waitUntil(
   cache.keys().then(keyList => {

   })
 );
});

循环遍历它们

我们需要遍历每个键,并调用caches.delete(),如果它不匹配我们的CACHE_NAME。由于我们可能有多个要删除的缓存,并且多次调用caches.delete(),它本身返回一个Promise,我们将在keyList上映射,并使用Promise.all()返回一组Promise

它看起来是这样的:

self.addEventListener('activate', event => {
 event.waitUntil(
   caches.keys().then(keyList => {
     Promise.all(keyList.map(key => {

     }));
   })
 );
});

删除任何键不匹配CACHE_NAME的缓存。

一个简单的if语句,然后调用caches.delete(),我们就完成了:

self.addEventListener('activate', event => {
 event.waitUntil(
   caches.keys().then(keyList => {
     Promise.all(
       keyList.map(key => {
         if (key !== CACHE_NAME) {
           return caches.delete(key);
         }
       })
     );
   })
 );
});

现在我们的缓存将恰好是我们想要的大小(仅在缓存对象上),并且每次我们的服务工作者激活时都会被检查。

因此,我们的缓存保持更新的机制是固有的。每次更新 JavaScript 时,我们都应该更新服务工作者中的版本。这会导致我们的服务工作者更新,从而重新激活,触发对先前缓存的检查和失效;一个美丽的系统。

测试我们的缓存

使用**yarn start快速在本地运行您的应用程序,以检查是否有任何明显的错误(拼写错误等),如果一切正常,请启动yarn deploy**。

打开您的实时应用程序和 Chrome DevTools。在应用程序|服务工作者下关闭更新后重新加载,刷新一次,然后转到网络选项卡。您应该会看到类似以下的内容:

如果这不起作用,请尝试取消注册应用程序|服务工作者下的任何服务工作者,然后重新加载两次。

关键点是(来自服务工作者)在我们的 JavaScript 文件旁边。我们的静态资产是由我们的服务工作者缓存提供的,如果您滚动到网络选项卡的顶部,您将看到这样的情况:

文档本身是由服务工作者提供的,这意味着我们可以在任何网络条件下运行我们的应用程序,甚至是离线的;让我们试试。点击网络选项卡顶部的离线复选框,然后点击重新加载。

如果一切顺利,我们的应用程序的加载时间不应该有任何区别,即使我们没有网络连接!我们的应用程序仍然可以加载,我们的聊天消息也是如此。

消息加载是 Firebase 数据库的一个好处,不是我们的功劳,但是从缓存中加载文档,这才是真正的成就!

当然,我们的用户体验并没有很好地为离线访问做准备。我们应该有一种方式来通知用户他们当前处于离线状态,也许可以通过某种对话框,但我们将其作为一个目标。

总结

我们实现了渐进式梦想——一个可以在任何网络条件下工作的应用程序,包括完全没有网络的情况。缓存是一个复杂的主题,所以为自己的成就鼓掌吧。

然而,在我们过于兴奋并将我们的原型提交给 Chatastrophe 董事会之前,让我们确保我们做对了事情。我们需要一种方式来在我们的项目上盖上一个橡皮图章,上面写着“批准!这是一个渐进式网络应用!”。

幸运的是,一个名为 Google 的小型初创公司已经给了我们一个可以做到这一点的工具。

接下来是对我们完成的渐进式网络应用进行审计,也就是胜利之旅。

第十三章:审核我们的应用程序

审核是确认我们的渐进式 Web 应用程序是否真正符合 PWA 标准的一种方式。这种审核是我们检查工作并确保我们的应用在 PWA 功能方面尽可能好的重要最后一步。

如前所述,渐进式 Web 应用程序的最大支持者是谷歌。他们的 Chrome 浏览器和 Android 操作系统不仅是所有 PWA 友好的,而且谷歌还非常努力地教育开发人员如何以及为什么构建 PWA。当您进入 PWA 的世界时(超出本书范围),您可能经常会查阅他们的文档。

然而,谷歌提供了另一种引领渐进式网络前进的方式。为了确保您的网页或应用程序的质量,他们发布了一套工具来衡量您的网站是否符合一组标准。他们用来做到这一点的主要工具称为 Lighthouse。

以下是本章将涵盖的内容:

  • Lighthouse 是什么?

  • 它遵循哪些标准?

  • DevTools 中的审核标签是什么?

  • 运行我们的第一次审核

  • 评估读数

  • 使用 Lighthouse CLI

Lighthouse 是什么?

简而言之,Lighthouse是一个工具,运行您的网站并告诉您基于一组特定标准它到底有多渐进式。

它通过尝试在各种条件下加载页面(包括 3G 网络和离线),并评估页面的响应方式来实现。它还检查一些 PWA 的常规功能,例如启动画面和服务工作者。

标准

以下标准本质上是 Lighthouse 在查看您的应用程序时遵循的一份清单。每个“测试”都是一个简单的是/否。如果您通过所有测试,您将获得 100 分。这就是我们想要的!

以下是 2017 年 8 月的标准列表:

  • 注册服务工作者:服务工作者是使您的应用能够使用许多渐进式 Web 应用程序功能的技术,例如离线、添加到主屏幕和推送通知。

  • 离线时响应 200:如果您正在构建渐进式 Web 应用程序,请考虑使用服务工作者,以便您的应用程序可以离线工作。

  • 当 JavaScript 不可用时包含一些内容:即使只是警告用户 JavaScript 是必需的,您的应用程序也应在 JavaScript 被禁用时显示一些内容。

  • 配置自定义启动画面:您的应用将构建一个默认的启动画面,但满足这些要求可以保证一个高质量的启动画面,让用户从点击主屏幕图标到应用的首次绘制有一个流畅的过渡。

  • 使用 HTTPS:所有网站都应该使用 HTTPS 进行保护,即使不处理敏感数据的网站也是如此。HTTPS 可以防止入侵者篡改或被动监听您的应用与用户之间的通信,并且是 HTTP/2 和许多新的网络平台 API 的先决条件。

  • 将 HTTP 流量重定向到 HTTPS:如果您已经设置了 HTTPS,请确保将所有 HTTP 流量重定向到 HTTPS。

  • 3G 网络下的页面加载速度足够快:如果交互时间短于 10 秒,即满足 PWA 基准检查表中的定义(来源--developers.google.com/web/progressive-web-apps/checklist),则满足此标准。需要进行网络限速(具体来说,预期的 RTT 延迟>=150 RTT)。

  • 用户可以被提示安装 Web 应用:虽然用户可以手动将您的网站添加到其主屏幕,但如果满足各种要求并且用户对您的网站有适度的参与度,提示(也称为应用安装横幅)将主动提示用户安装应用。

  • 地址栏与品牌颜色匹配:浏览器地址栏可以进行主题设置以匹配您的网站。当用户浏览网站时,theme-color元标签将升级地址栏,一旦添加到主屏幕后,清单主题颜色将在整个网站上应用相同的主题。

  • 具有带有宽度或初始缩放的标签:添加viewport元标签以优化您的应用在移动屏幕上的显示。

  • 内容在视口中正确调整大小:如果您的应用内容的宽度与视口的宽度不匹配,您的应用可能没有针对移动屏幕进行优化。

审核标签

直到 Chrome 60 发布之前,Lighthouse 只能作为 Chrome 扩展程序或命令行工具的测试版版本。然而,现在它在 Chrome DevTools 中有了自己的位置,在新的审核标签中。

在审核标签中,除了 Lighthouse PWA 审核之外,还包括一系列其他基准测试,包括性能和网络最佳实践。我们将专注于 PWA 测试和性能测试,但也可以随意运行其他测试。

审计选项卡的另一个有用功能是能够保存先前的审计,以便在改进应用程序时获得应用程序的历史记录。

好了,说够了。让我们继续进行我们的第一次审计!

我们的第一次审计

打开您的 DevTools,导航到审计选项卡,然后单击运行审计。

应该需要几秒钟,然后给您一个关于我们网站外观的简要摘要,鼓掌。我们的渐进式 Web 应用程序有多好呢?:

一点也不糟糕。事实上,在 PWA 类别中没有比这更好的了。给自己一个鼓励,也许是一个成功的高五。让我们评估读数,然后决定是否要继续前进或者争取在所有类别中达到 100%。

请注意,由于 Lighthouse 正在积极开发中,您的分数可能与上述不符合新的标准。在这种情况下,我鼓励您查看 Lighthouse 所抱怨的内容,并看看是否可以解决问题以达到“100”分。

评估读数

如果您的结果与前面的不符,有两种可能性:

  • Chrome 添加了我们的应用程序无法满足的新测试。正如我们多次提到的,PWA 是一种不断发展的技术,所以这是完全可能的。

  • 您在书中错过了一些步骤;最好的人也会发生这种情况。

无论哪种情况,我都鼓励您进行调查并尝试解决根本问题。谷歌为每个测试标准提供了文档,这是一个很好的起点。

在我们的情况下,我们唯一没有通过的测试是性能。让我们看看我们没有通过的原因:

正如我们在这里看到的,我们的第一个有意义的绘制大约需要三秒钟。请注意,我们的应用程序外壳不被视为有意义的绘制,尽管它确实改善了页面的感知性能。Chrome 足够聪明,知道只有当我们的“登录”表单或“聊天”容器出现时,我们才真正在屏幕上有有意义的内容--用户实际可以使用的东西。

尽管如此,显示有意义的内容需要超过三秒的原因是,我们需要等待我们的 JavaScript 加载,启动,然后加载我们的用户当前是否已登录,然后加载聊天消息或重定向到登录。这是很多后续步骤。

这是一个可以解决的问题吗?也许可以。我们可以设置一些方式,在 React 加载之前找出用户是否已登录(换句话说,将一些 JavaScript 移出我们的主应用程序)。我们可以将chat容器和login表单都移出 React,以确保它们可以在库加载之前呈现,然后想出一些方法在 React 初始化后替换它们(挑战在于替换输入而不擦除用户已开始输入的任何内容)。

所有提到的挑战都属于优化关键渲染路径的范畴。对于任何想深入了解性能优化的人,我鼓励你去尝试一下。然而,从商业角度来看,这对于一点收益来说是很多(可能有错误)优化。根据先前的基准测试,我们的用户已经在大约 400 毫秒内接收到内容,并且完整的应用程序在三秒多一点的时间内加载完成。请记住,由于缓存,大多数用户在随后的访问中将获得更快的加载时间。

我们较低的性能得分实际上展示了使用诸如 React 之类的庞大 JavaScript 库构建高性能应用程序的成本效益。对于那些对更轻量级替代方案感兴趣的人,在下一章节中查看关于 Preact 的部分,这可能是解决前述问题的一个可能方案。

使用 Lighthouse CLI

从审计选项卡运行测试非常简单易行,但我们如何确保在将应用程序推送到线上之前保持应用程序的质量呢?

答案是将 Lighthouse 纳入我们的部署流程,并使用它自动评估我们的构建。这类似于在我们执行yarn deploy时运行测试套件。幸运的是,谷歌为此目的提供了 Lighthouse CLI。

让我们使用以下命令进行安装:

yarn add --dev lighthouse

在这里,我们的目标是在执行yarn deploy时在我们的应用程序上运行 Lighthouse。为此,我们必须制作一个自定义部署脚本。

如果打开我们的package.json,你会在scripts下看到以下内容:

 "scripts": {
   "build": "node_modules/.bin/webpack --config webpack.config.prod.js",
   "start": "node_modules/.bin/webpack-dev-server",
   "deploy": "npm run build && firebase deploy"
 },

让我们将其更改为以下内容:

 "scripts": {
   "build": "node_modules/.bin/webpack --config webpack.config.prod.js",
   "start": "node_modules/.bin/webpack-dev-server",
   "deploy": "npm run build && node scripts/assess.js && firebase deploy"
 },

我们将使用 node 来运行一个用 JavaScript 编写的自定义构建脚本。在你的目录根目录下创建scripts/文件夹,以及assess.js文件。

我们的流程将如下:

  1. 在本地提供我们的build文件夹,以便在浏览器中运行。

  2. 使用 Lighthouse 评估提供的页面。

  3. 在控制台记录结果。

让我们添加我们需要用来提供我们的build文件夹的包:

yarn add --dev serve

请注意,鉴于我们永远不会在生产中使用它们,我们将这个和lighthouse保存为dev依赖项。

服务我们的构建文件夹

在我们的新scripts/assess.js中,要求serve包:

const serve = require('serve');

我们只想要在端口 5000 上serve我们新编译的build文件夹,看起来是这样的:

const server = serve('./build', {
 port: 5000
});

我们可以随时通过运行server.stop()来停止服务器。我们会在显示分数后这样做。

使用 Lighthouse 来评估提供的页面

现在,让我们在assess.js的顶部要求另外两个工具:

const lighthouse = require('lighthouse');
const chromeLauncher = require('lighthouse/chrome-launcher');

chromeLauncher将允许我们打开 Chrome 到目标页面,然后运行 Lighthouse。让我们创建一个名为launchChromeAndRunLighthouse的函数来做到这一点:

function launchChromeAndRunLighthouse(url, flags= {}, config = null) {

}

我们可以选择传入一些标志和配置,这里我们不会使用(标志可以用来在过程展开时打开日志记录)。

在函数内部,我们将启动 Chrome,设置 Lighthouse 运行的端口,然后运行它。最后,我们将停止 Chrome:

function launchChromeAndRunLighthouse(url, flags = {}, config = null) {
 return chromeLauncher.launch().then(chrome => {
   flags.port = chrome.port;
   return lighthouse(url, flags, config).then(results =>
     chrome.kill().then(() => results));
 });
}

顺便说一句,这个函数直接来自 Lighthouse CLI 文档。

好了,现在是最后一步了。我们将使用我们选择的 URL 运行我们的函数(将其放在文件底部,在serve命令下方):

launchChromeAndRunLighthouse('http://localhost:5000', {}).then(results => {
  server.stop();
});

一旦我们有了结果,我们就停止服务器,但我们需要正确显示我们的结果。

记录结果

结果变量以对象的形式出现。它提供了每个类别的详细分数,但我们只关心有问题的地方。在我们的函数调用之前,让我们添加一个分数截止线:

const CUTOFF = 90
launchChromeAndRunLighthouse('http://localhost:5000', {}).then(results => {

我们将使用这个来说“只显示得分低于 90/100 的结果”。

登出结果的过程并不是很令人兴奋,所以我们不会在这里深入讨论。以下是完整的文件:

const serve = require('serve');
const lighthouse = require('lighthouse');
const chromeLauncher = require('lighthouse/chrome-launcher');

function launchChromeAndRunLighthouse(url, flags = {}, config = null) {
 return chromeLauncher.launch().then(chrome => {
   flags.port = chrome.port;
   return lighthouse(url, flags, config).then(results =>
     chrome.kill().then(() => results));
 });
}

const server = serve('./build', {
 port: 5000
})

const CUTOFF = 90

launchChromeAndRunLighthouse('http://localhost:5000', {}).then(results => {
 score = results.score
 const catResults = results.reportCategories.map(cat => {
   if (cat.score < CUTOFF) {
     cat.audits.forEach(audit => {
       if (audit.score < CUTOFF) {
         const result = audit.result
         if (result.score) {
           console.warn(result.description + ': ' + result.score)
         } else {
           console.warn(result.description)
         }
         if (results.displayValue) {
           console.log('Value: ' + result.displayValue)
         }
         console.log(result.helpText)
         console.log(' ')
       }
     })
   }
   return cat
 })
 catResults.forEach(cat => {
   console.log(cat.name, cat.score)
 })
 server.stop()
});

如果您从终端运行node scripts/assess.js,您应该会看到一个问题区域的列表,以及每个类别的最终得分。通过运行yarn deploy将所有内容汇总在一起,您将在 Firebase 部署之前看到这些分数。

现在我们有了一个简单而干净的方法来随时了解我们应用程序的状态,而不必自己启动网站来测试它。

总结

完成!我们对我们的应用进行了全面审查,它在每个类别都表现出色。我们有一个可用的渐进式 Web 应用程序。在本章中,我们了解了 Lighthouse 是什么,以及为什么验证我们的 PWA 很重要。我们还将其作为部署过程的一部分,以确保我们的应用程序继续符合质量标准。现在我们可以认为我们的应用在各个方面都已经完成。

接下来,我们将讨论后续步骤以及增加对 PWA 知识的有用资源,但首先,关于将我们的应用提交给你的朋友和 Chatastrophe 委员会。

第十四章:结论和下一步

“……这就是应用程序根据谷歌的评分。正如您所看到的,它符合渐进式 Web 应用的每个标准,这将与我们的全球业务目标很好地契合——”

“是的,是的,”你的朋友挥了挥手。“很酷。很棒。干得好。但枢纽呢?”

“什么?”你问道。

“你没收到备忘录吗?我一个月前就给你公司邮箱发了一封备忘录。”

“我不知道我有公司邮箱。”

“哦。”你的朋友皱起了眉头。“我以为你对技术很在行。”

“但我不知道——”

“没关系。我可以总结一下。公司已经转变了。聊天很棒,但如果我们再进一步呢?如果我们把它变成一个社交网络呢?想象一下——Facebook 的可分享性,Netflix 的视频流和 Uber 的顺风车,所有这些都在一个区块链上……”

当你走向门口时,你的朋友继续说话。

下一步

我们已经涵盖了将 React 应用程序转变为 PWA 所需的每一步,但是,像往常一样,还有更多要学习的。

本章分为四个部分。首先,我们将列出一些有用的资源,以继续您的 PWA 之旅。然后,我们将介绍一些重要的库,这些库将帮助自动化 PWA 开发的某些方面,或将您的应用程序提升到更高的水平。第三,我将列出一些我最喜欢的关于开发渐进式 Web 应用的文章。最后,我们将看一下一些可能的扩展目标,以便您在接受挑战后扩展和改进 Chatastrophe。

以下许多资源都是通过两个优秀的存储库发现的:awesome-pwa (github.com/hemanth/awesome-pwa) 由 GitHub 用户Hemanth创建,以及awesome-progressive-web-apps (github.com/TalAter/awesome-progressive-web-apps) 由TalAter创建。

我们将看一下以下内容:

  • 扩展您知识的学习资源

  • 成功 PWA 的案例研究

  • 可以从中获得灵感的示例应用程序

  • 关于 PWA 崛起的必读文章

  • 您可以使用的工具来使未来 PWA 的构建更容易

  • Chatastrophe 的扩展目标

学习资源

学习资源如下:

  • 渐进式 Web 应用文档:谷歌关于渐进式 Web 应用的官方文档。这应该是您的第一站,以了解概念或阅读最佳实践。它还提供了关于 PWA 的好处的摘要,并链接到诸如 Lighthouse 之类的工具。

developers.google.com/web/progressive-web-apps/

  • 你的第一个渐进式 Web 应用:一个逐步教程,教你如何构建你的第一个渐进式 Web 应用,或者在你的情况下,你的第二个。如果你想看看没有 React 的情况下构建 PWA 是什么样子,可以看看这个教程。这是非常详细的,涵盖了每个概念。

developers.google.com/web/fundamentals/getting-started/codelabs/your-first-pwapp

  • 离线 Web 应用:由 Google 创建并由 Udacity 托管的免费课程,关于离线优先的 Web 应用。内容分为三个部分:为什么优先离线、Service Workers 和缓存。一些部分,比如 service worker 部分,可能会是复习,但这门课程还深入探讨了 IndexedDB 用于本地存储。

www.udacity.com/course/offline-web-applications--ud899

  • Service Worker 入门:Google 对 Service Workers 的介绍。很多代码看起来会很熟悉,因为它们在本书的 service worker 部分中出现过,但它仍然是一个方便的资源。Matt Gaunt 做了很好的工作,解释了基础知识。

developers.google.com/web/fundamentals/getting-started/primers/service-workers

  • Service Worker 101:关于 service workers 的更加生动的指南,这个可爱的资源包含一系列图表,带你了解 service worker 的生命周期等内容。如果你对 service workers 不确定,可以打印出来贴在你的桌子上。

github.com/delapuente/service-workers-101

  • 开始使用渐进式 Web 应用:Chrome 开发团队的 Addy Osmani 的一篇博客文章(我们将在这个资源部分经常看到他)。这是一个很好的高层次介绍 PWA 的好处,并介绍了一些起步的模板。

addyosmani.com/blog/getting-started-with-progressive-web-apps/

  • 使用 Push API:Mozilla 开发者网络关于 Push API 的指南。如果你想在你的 PWA 中使用推送通知,而不依赖于 Firebase Cloud Notifications,就从这里开始。

developer.mozilla.org/en-US/docs/Web/API/Push_API/Using_the_Push_API

  • 使用缓存 API:Mozilla 开发者网络对缓存 API 的指南。在这里没有太多新东西,我们在缓存章节中没有涵盖到的,但鉴于缓存 API 的“实验性”状态,回头参考一下是很好的。这项技术可以从目前的状态发展,所以把它作为一个参考。

developer.mozilla.org/en-US/docs/Web/API/Cache

  • 通过应用安装横幅增加用户参与度:应用安装横幅的如何和为什么。一个详尽的常见问题解答了你可能有的任何问题。还有一个关于推迟提示的很棒的教程,你可以用它来巩固我们在第九章中涵盖的概念,使用清单使我们的应用可安装

developers.google.com/web/updates/2015/03/increasing-engagement-with-app-install-banners-in-chrome-for-android?hl=en

  • Web 基础-性能:谷歌关于构建高性能 Web 应用的资源。值得注意的是,谷歌对性能有一个特定的哲学,属于 PWA 模型,但不一定是更好性能的唯一途径。也就是说,对于任何对速度感兴趣的人来说,这是一个很棒的(有时过于技术性的)资源。

developers.google.com/web/fundamentals/performance/

  • 引入 RAIL:面向用户的性能模型:这篇文章以“性能建议不胜枚举,是吗?”开篇。这是真实的话,尽管Paul IrishPaul Lewis的建议比大多数更好。这篇文章特别关注为什么我们应该遵循这个指标来介绍 RAIL。答案?用户应该放在第一位。

www.smashingmagazine.com/2015/10/rail-user-centric-model-performance/

  • 渐进式 Web 应用通讯简报:我的免费通讯,让你了解渐进式 Web 应用的世界,包括教程、文章、有趣的项目等。如果你想要联系我,只需点击下一期的“回复”。我会很乐意收到你的来信。

pwa-newsletter.com/

  • 网站性能优化:另一个由谷歌和 Udacity 合作的课程,这次是关于优化性能的课程。它介绍了 DevTools 并深入探讨了关键渲染路径等概念。这门课程应该需要大约一周的时间来完成。

www.udacity.com/course/website-performance-optimization--ud884

  • 浏览器渲染优化:这里还有一个!这门课程的副标题是“构建 60 FPS Web 应用”,这是一个值得追求的目标(正如我们的 RAIL 指标建议的那样)。它可以被认为是前面课程的更深入版本。在完成这门课程后,你可以称自己为 Web 性能专家。

www.udacity.com/course/browser-rendering-optimization--ud860

  • 使用 React 构建渐进式 Web 应用Addy Osmani再次出现。在这里,他带领我们使用 React 构建 PWA。请注意,这个教程更像是一个概述,而不是一个逐步指南,但在我写这本书时,这对我来说是一个非常宝贵的资源。他还提供了许多链接到其他文章和资源,以进一步扩展你的知识。

medium.com/@addyosmani/progressive-web-apps-with-react-js-part-i-introduction-50679aef2b12

  • Service Worker Cookbook:关于 service workers 的一切你想知道的东西。说真的,这是一个了不起的资源,会让你很快成为专家。如果你对这项新技术感到兴奋并想深入了解,这是一个很好的机会。

serviceworke.rs/

  • 将你的网站改造成 PWA:大多数公司不会立即从头开始构建 PWA。相反,他们会希望将 PWA 功能添加到他们现有的网站或应用中。这是一个很好的入门指南,并附有大量的截图。

www.sitepoint.com/retrofit-your-website-as-a-progressive-web-app/

案例研究

你需要说服老板尝试渐进式 Web 应用吗?看看以下大公司采用 PWA 的案例研究(Chatastrophe Inc.因破产而被移出列表)。

构建 Google I/O 2016 渐进式 Web 应用

Google I/O 2016 应用程序(昵称 IOWA)是使用 Firebase 和 Polymer 构建的。这就是他们的做法。这是一个更加技术性的指南,介绍了几个高级概念;这是一个了解下一级 PWA 的好方法。

developers.google.com/web/showcase/2016/iowa2016

AliExpress 案例研究

AliExpress 是俄罗斯访问量最大的电子商务网站。通过转换为 PWA,他们将新用户的转化率提高了 104%。他们还将在网站上花费的时间增加了 74%。这些都是很大的数字,为 PWA 提供了一个有力的商业案例。

developers.google.com/web/showcase/2016/aliexpress

eXtra Electronics 案例研究

这对于业务改进来说怎么样--销售额增加了 100%。这就是 eXtra Electronics 通过网络推送通知到达的用户所取得的成就。事实上,网络推送通知现在是 eXtra 最大的留存渠道,超过了电子邮件。更加努力!

developers.google.com/web/showcase/2016/extra

Jumia 案例研究

又一个关于网络推送通知的好消息。Jumia 的转化率增加了 9 倍。他们过去会发送电子邮件提醒顾客购物车中剩下的物品,但开启率很低。现在引入了通知。

developers.google.com/web/showcase/2016/jumia

Konga 案例研究

你的用户关心他们的数据限制;不要让他们受苦。Konga 将他们的原生应用与 PWA 进行比较,将数据使用量减少了 92%。最终,用户完成第一笔交易所需的数据减少了 84%。考虑到入门的障碍降低了。

developers.google.com/web/showcase/2016/konga

SUUMO 案例研究

通过添加服务工作者和一些其他调整,SUUMO 团队将加载时间减少了 75%。他们还利用了推送通知的热潮,开启率达到了 31%。尝试 PWA 的决定背后的故事可能听起来很熟悉;移动体验很差,所以公司将用户推向原生应用。然而,让他们下载原生应用却很困难,所以他们尝试了 PWA。一个很好的教训--如果你的问题是留存,原生应用可能不是答案。

developers.google.com/web/showcase/2016/suumo

示例应用程序

想看看真正的渐进式 Web 应用程序是什么样子吗?看看以下任何一个。其中一些还包含 GitHub 的链接,供您查看源代码。

PWA.rocks

这是一个渐进式 Web 应用程序的集合,也是以下大部分内容的来源。如果您需要灵感,可以将其作为第一站。我还鼓励您将您添加到列表中的任何 PWA 添加到其中。

pwa.rocks/

Flipboard

Flipboard 是 PWA 领域中最重要的参与者之一,他们的 PWA 应用程序体积小,速度快,而且美观。Flipboard 拥有功能齐全的原生应用程序,但也有 PWA,以便在用户偏好方面进行押注。如果内容丰富的 Flipboard 能够符合 PWA 的性能指南,那么天空就是极限。

flipboard.com/

React Hacker News

这是一个备受欢迎的开发者项目:使用 React 克隆的 Hacker News。作为一个开源项目,ReactHN 是了解如何使用渐进式 Web 应用程序基本原理来管理复杂的前端库的好方法。我们的好朋友Addy Osmani再次出马。因此,ReactHN 是一个深入了解 Chrome 开发人员如何使用 JavaScript 库构建 PWA 的内部视角。

react-hn.appspot.com/

Notes

这是一个很好的、体积小的渐进式 Web 应用程序的例子,值得初学者关注。您可以在网站上直接找到 GitHub 的链接,然后查看Simon Evans应用程序的结构。在桌面上,应用程序外壳与内容有明显的区别,这使得概念特别直观。最重要的是,该应用在 Lighthouse 上得分 94 分。

sii.im/playground/notes/

Twitter

也许你听说过这个。

Twitter 是一个真正全球化应用程序的完美例子。他们的应用程序需要能够被所有大陆的用户在各种条件下访问(只需看看 Twitter 在组织阿拉伯之春中所扮演的角色)。

为了实现全球可访问性,Twitter 团队设法将他们的应用程序减小到 1MB,并添加了本文讨论的所有 PWA 功能:主屏幕安装、推送通知和离线访问。

lite.twitter.com/

2048 Puzzle

2048 拼图游戏的 PWA 实现,最初由 Veewo Studio 创建。它只适用于移动/触摸设备,但它是一个游戏应用程序被制作成 PWA 的例子,它快速、高效且可安装。请注意-对于未经培训的人来说,这个游戏非常容易上瘾。

这个开源项目可以在 GitHub 上找到,所以你可以查看结构(特别是 JavaScript 的结构,需要十个文件来运行游戏)。然而,这个应用的不可告人的秘密是,创作者实际上从未打过这个游戏。

2048-opera-pwa.surge.sh/

阅读的文章

以下文章涵盖了宣言、教程和清单,都是关于 PWA 的崛起以及构建它们的最佳方法。

原生应用注定要失败

JavaScript 大师Eric Elliott对渐进式 Web 应用的热情宣言。这是对原生应用成本和 PWA 好处的深入探讨。这是一个很好的材料,可以说服正在辩论是否要构建原生应用的老板和同事。后续文章也很棒。

medium.com/javascript-scene/native-apps-are-doomed-ac397148a2c0

渐进式 Web 应用的一大堆技巧和窍门

Dean Hume的各种 PWA 技巧的大杂烩。看看有趣的东西,比如离线 Google Analytics 和测试服务工作者(随着我们继续前进,会有更多内容)。

deanhume.com/Home/BlogPost/a-big-list-of-progressive-web-app-tips-and-tricks/10160

测试服务工作者

服务工作者是渐进式 Web 应用功能的核心。我们希望确保它们正常工作。我们如何对它们进行单元测试?

medium.com/dev-channel/testing-service-workers-318d7b016b19

Twitter Lite 和高性能 React 渐进式 Web 应用的规模

Twitter Lite 工程师之一深入探讨了他们的构建过程、挑战,并在开发 Twitter 的 PWA 版本后提出了建议。这是关于部署大规模 PWA 的最接近的操作指南。

medium.com/@paularmstrong/twitter-lite-and-high-performance-react-progressive-web-apps-at-scale-d28a00e780a3

为什么应用安装横幅仍然存在?

这是一个关于当你不是市场领导者时坚持传统应用程序的成本以及渐进式 Web 应用程序如何解决这个问题的优秀总结。阅读到最后,了解一些从原生应用程序转换为 PWA 的公司的统计数据。

medium.com/dev-channel/why-are-app-install-banners-still-a-thing-18f3952d349a

使用 Vue JS 创建渐进式 Web 应用程序

Charles Bochet结合了 VueJS,Webpack 和 Material Design 的元素来构建 PWA。这是一个很好的机会,可以在一个新的库中尝试 PWA 概念。

blog.sicara.com/a-progressive-web-application-with-vue-js-webpack-material-design-part-3-service-workers-offline-ed3184264fd1

将现有的 Angular 应用程序转换为渐进式 Web 应用程序

将常规 Angular 应用程序转换为功能性渐进式 Web 应用程序需要什么?Coskun Deniz一步一步地带领我们完成了这些步骤。

medium.com/@cdeniz/transforming-an-existing-angular-application-into-a-progressive-web-app-d48869ba391f

推动 Web 前进

“实际上,任何网站都可以,也应该成为渐进式 Web 应用。”

Jeremy Keith在他的文章中提出了“推动 Web”的主要论点,他是对的。使您的应用程序(或静态站点)成为渐进式,是为所有用户提供增强体验。对于任何对跳入 PWA 世界持怀疑态度的人来说,这是一篇很好的阅读。

medium.com/@adactio/progressing-the-web-9ab55f63f9fa

设计降级-敌对环境的 UX 模式

Chipotle 餐厅如何帮助改进您的网站。本文并不特别讨论 PWA,但与渐进增强的概念完全契合,即您的网站应该适用于所有人,然后根据他们的条件(网络速度,浏览器现代性等)变得越来越好。

uxdesign.cc/designed-degradations-ux-patterns-for-hostile-environments-7f308d819e50

使用应用程序外壳架构实现即时加载 Web 应用程序

对应用程序外壳模式的深入解释。如果你正在开发 PWA,这是必读的。

medium.com/google-developers/instant-loading-web-apps-with-an-application-shell-architecture-7c0c2f10c73

欺骗用户,让他们觉得你的网站比实际更快

一篇很棒的文章,从用户的角度出发(感知时间与实际时间),然后解释了你可以利用的基本技术,来减少应用的感知加载时间。

www.creativebloq.com/features/trick-users-into-thinking-your-sites-faster-than-it-is

苹果拒绝支持渐进式网络应用对未来的网络是一个损害

一个令人耳目一新的诚实看待在这个时候开发渐进式网络应用的经历,以及与 iOS 相关的挣扎。如果你正在考虑生产 PWA,请阅读这篇文章。

m.phillydevshop.com/apples-refusal-to-support-progressive-web-apps-is-a-serious-detriment-to-future-of-the-web-e81b2be29676

工具

希望你将来会构建(许多)更多的渐进式网络应用。以下工具将使这个过程更容易、更快速。

Workbox

Workbox 是“一组用于渐进式网络应用的 JavaScript 库”。更具体地说,它“使创建最佳服务工作者代码变得容易”,并以最有效的方式维护你的缓存。它很容易集成到 Webpack 中。不幸的是,文档不是很好,定制可能会很困难。

然而,Workbox 有很大的潜力,可以自动化开发的各个方面,并隐藏可能会让新开发者望而却步的复杂性。挑战在于不要用更多的复杂性来取代这种复杂性。

github.com/GoogleChrome/workbox

Sw-precache

作为 Workbox 的子集,sw-precache 值得单独讨论。它可以用来自动生成一个服务工作者,以预缓存你的应用程序资产。你所需要做的就是将它纳入你的构建过程(有一个 Webpack 插件),并注册生成的服务工作者。

github.com/GoogleChrome/sw-precache

Sw-toolbox

不适合初学者!与前面的生成工具不同,sw-toolbox 是一组辅助方法。更令人困惑的是,还有 Google Chrome 团队的 Workbox,采用了更模块化的方法。我给你的建议是,先熟悉直接与服务工作者交互,然后,如果你有一个特定的问题可以通过这些工具之一简化,那就采用它。但是,不要去寻找解决你尚未遇到的问题的工具,但像我说的,看到出现的工具来帮助管理复杂性是令人兴奋的。

github.com/GoogleChrome/sw-toolbox

Offline-plugin

另一个使你的应用程序具有离线功能的插件。这个插件使用服务工作者,但为了更好的支持,会回退到 AppCache API。实现看起来简单而直接。

github.com/NekR/offline-plugin

Manifest-json

一个工具,可以从命令行自动生成 Web 应用程序清单。我的意思是,我觉得我的清单章节还不错,但如果你更喜欢问答式的方法,那也可以,我想。

开玩笑的,这个工具可能会在 Web App 清单发展并承担更多属性时派上用场。

www.npmjs.com/package/manifest-json

Serviceworker-rails

有一个 Ruby on Rails 项目吗?想让你的资产管道使用服务工作者来缓存资产吗?使用这个宝石。文档是 Rails 如何处理缓存和实现服务工作者方法的有趣概述。

github.com/rossta/serviceworker-rails

Sw-offline-google-analytics

前面提到的 Workbox 的一部分,但专门用于具有离线功能的应用程序的 Google Analytics。使用此软件包在连接可用时发送离线请求到 Google Analytics。

www.npmjs.com/package/sw-offline-google-analytics

Dynamic Service Workers (DSW)

使用 JSON 文件配置你的服务工作者;这是一种非常有趣的服务工作者方法,支持关键功能,如推送通知(尽管只能使用 Google Cloud Messaging)。

github.com/naschq/dsw

UpUp

在您的网站上添加两个脚本,并使其在离线状态下工作。UpUp 是服务工作者技术的美丽实现,适用于简单的用例。当然,它并不适用于高级用户,但是是向每个人介绍服务工作者技术的绝佳方式。

www.talater.com/upup/

生成渐进式 Web 应用

从命令行生成渐进式 Web 应用文件结构!这仍然是一个正在进行中的工作。

github.com/hemanth/generator-pwa

渐进式 Web 应用配置

另一个来自 Addy Osmani 的样板文件。如果您要构建非 React PWA,请参考此项目结构。

github.com/PolymerLabs/progressive-webapp-config

延伸目标

Chatastrophe 已经启动,但仍然非常基础。现在我们将讨论一些挑战,您可以选择接受,以拓展您的技能并改进我们的应用。

切换到 Preact

Preact 是 React 库的 3 KB 版本。它具有类似的 API 和功能,但没有冗余。使用它而不是 React 将提高我们应用的性能。如果您选择这条路线,请考虑使用 Webpack 别名来简化转换。

github.com/developit/preact

显示在线状态

告诉其他用户另一个用户何时在线。UI 由您决定。

显示正在输入

在聊天室中常见的功能,用于向用户指示其他人正在输入。对于 Chatastrophe,挑战将是同时表示多个用户正在输入。

包括文件上传

人们想要与朋友分享(可能是表情包)。为他们提供一个文件上传系统。

创建聊天室

您的朋友曾经有一个真正全球聊天室的愿景;那个愿景很糟糕。让我们通过允许用户创建聊天室来大大提高 Chatastrophe 的可用性。是否有一种方法可以让用户在离线状态下在房间之间导航并阅读消息?

无需 React 即可交互

阻碍我们性能的一个问题是需要 React 在向用户显示交互式站点之前启动。如果我们给他们一个纯 HTML 交互式外壳,然后在加载时切换到 React 版本会怎样?这里的挑战将是避免覆盖用户输入,但您可以赢得一些巨大的性能点。

构建自己的后端

在本教程中,我们依赖 Firebase,以便将注意力集中在前端,即 React 开发上。然而,在为 Chatastrophe 设计自己的后端 API 方面,有很大的学习机会。最大的好处是可以对页面进行服务器渲染,以获得额外的性能。

结束语

编程很困难。学习也很困难。在学习全新概念的同时使用实验性技术进行编程尤其困难。如果你完成了本书中的教程,甚至只是其中的某些部分,你应该为此感到自豪。

我真诚地希望你在这里学到的东西对你的职业有所帮助。这本书对我来说也是一次激动人心的旅程。当我开始时,我对渐进式 Web 应用程序的世界感到兴奋,但绝不是专家。现在,深入研究渐进式 Web 后,我对可能性比以往任何时候都更加兴奋。我希望你也有同样的感觉。

如果你想要联系我,我会很乐意听到你的反馈、批评、问题,或者只是闲聊。你可以在 Twitter 上找到我,用户名是@scottdomes,在 LinkedIn 上找到我,也可以在 Medium 上找到我,用户名同样是@scottdomes,或者在我的网站scottdomes.com上找到我,我在那里发布关于各种主题的 Web 开发教程。

总结

我希望提到的资源对你继续 PWA 之旅有所帮助。PWA 是 Web 开发中令人兴奋的一部分,发展迅速;关注前任作者和创作者将帮助你跟上变化的步伐。

在这本书中,我们涵盖了很多内容:从零到一个 React 应用程序,再从一个 React 应用程序到一个渐进式 Web 应用程序。我们从头开始构建了一个完整的应用程序,并部署到世界上可以看到它。我们还使它快速响应,并能够处理各种类型的连接。

我希望你为最终的应用感到自豪,也希望这本书对你有所帮助。祝你未来的所有 PWA 项目好运,让我们继续推动 Web 的发展。