xjcloudy

Progressive Web Apps 入门 (2)

xjcloudy · 2017-01-08翻译 · 746阅读 原文链接

TL;DR: 近几年随着Web技术的显著发展使得开发者能够很轻松的通过部署一个网站或者web应用服务全球成千上万的用户。仅仅通过一个浏览器,通过URL就可以访问到一个web应用。有了PWA ,开发者能够使用先进的web技术来给用户带来很棒的类似app一样的体验。在上一篇文章中,我们构建了一个能够缓存页面并且在离线状态下能够部分工作的pwa应用。这次,我们将实现即时加载同时完成全部的离线功能。


回顾和概述

上一篇文章里,我们介绍了PWA应用的典型特征和service work。目前为止,我们已经实现了缓存功能。indexlatest页面已经可以离线加载同时也提高了再次访问时的加载速度。在上一篇文章的最后,我们已经实现了在离线状态下lastest页面的加载,但是没法动态获取数据。

接下来的教程将涉及如下内容:

  • 缓存latest页面上得数据,以便用户离线时也有数据可以展示

  • 使用 localStorage 存储应用数据

  • 当用户重新联网时,获取新的数据替换掉老的数据

离线存储

当我们构建PWA应用时,我们可以选择的存储机制有很多种:

  • IndexedDB: 这是一种基于javascript的客户端存储系统. 使用了索引来提高搜索效率。InexedDB暴露了一个异步的接口,不会阻断页面渲染。但是有些研究表明仍然会阻断页面渲染。我建议通过第三方扩展来使用IndexedDB,比如localForage,idbidb-keyval。因为兼容性是个大问题。

IndexedDB Browser Support

IndexedDB 的浏览器支持情况
  • Cache API: 是存储url资源的最好选择,与service work 配合使用效果很好。

  • PouchDB:CouchDB的javascript开源版。当应用离线时存储数据到本地,当应用在线时与CouchDB或者其他兼容的服务同步数据。 无论用户下次在哪里登陆都可以同步用户的数据。PouchDB 支持所有现代浏览器,在底层使用IndexedDB,当IndexedDB不可用时会降级使用WebSQL。支持Firefox 29+(包括Firefox OS 和 Firefox for Android),Chrome 30+,Safari 5+,IE10+,Opera 21+,Android 4.0+,ios 7.1+和Windows Phone 8+。

  • Web Storage e.g localStorage: 调用时是同步的,会阻断DOM渲染。在大多数浏览器中限制在5m大小。使用k-v结构存储数据

Web Storage Browser Support

Web Storage 浏览器支持情况

  • WebSQL : 是一个浏览器环境下的关系型数据库解决方案。已经被废弃。声明在这里 未来浏览器将不再会支持这一特性。

Quota for Mobile Storage

移动端存储限制

Addy Osmani写了一篇评测文章 非常有参考价值。

PouchDB的维护者Nolan Lawson建议在使用数据库之前需要先问自己如下几个问题:

  • 内存数据库还是磁盘数据库(PouchDB,IndexedDB)?

  • 什么东西需要被存到磁盘?那些数据在应用关闭或崩溃时可以保存下来。

  • 哪些地方需要被索引?是否可以使用内存索引替代磁盘索引?

  • 怎样根据数据库的数据构造内存数据? 两者之间的映射策略如何选择?

  • 我的应用需要什么样的查询语句?是否为了展示一个简介需要查询出完整的数据,还是可以只获取需要的那部分数据?是否可以延迟加载数据?

这篇文章可以帮助你拓展一下相关的知识。

我们来动手实现即时加载

本例的应用中我们使用localStorage。但是我建议在产品级应用中不要使用localStorage。原因就是上文提到的存储空间的限制。 我们这个例子只是个小demo所以没有问题。

打开js/latest.js文件。为了将通过Github API获取到的数据存到localStorage中, 我们需要这样修改fetchCommits方法:

 function fetchCommits() {
    var url = 'https://api.github.com/repos/unicodeveloper/resources-i-like/commits';

    fetch(url)
    .then(function(fetchResponse){ 
      return fetchResponse.json();
    })
    .then(function(response) {
        console.log("Response from Github", response);

        var commitData = {};

        for (var i = 0; i < posData.length; i++) {
          commitData[posData[i]] = {
            message: response[i].commit.message,
            author: response[i].commit.author.name,
            time: response[i].commit.author.date,
            link: response[i].html_url
          };
        }

        localStorage.setItem('commitData', JSON.stringify(commitData));

        for (var i = 0; i < commitContainer.length; i++) {

          container.querySelector("" + commitContainer[i]).innerHTML = 
          "<h4> Message: " + response[i].commit.message + "</h4>" +
          "<h4> Author: " + response[i].commit.author.name + "</h4>" +
          "<h4> Time committed: " + (new Date(response[i].commit.author.date)).toUTCString() +  "</h4>" +
          "<h4>" + "<a href='" + response[i].html_url + "'>Click me to see more!</a>"  + "</h4>";

        }

        app.spinner.setAttribute('hidden', true); // hide spinner
    })
    .catch(function (error) {
      console.error(error);
    });
};

上面这段代码的作用是,当页面首次加载时,将commit 数据存到localStorage里。我们接下来实现取数据的方法:

 // Get the commits Data from the Web Storage
  function fetchCommitsFromLocalStorage(data) {
    var localData = JSON.parse(data);

    app.spinner.setAttribute('hidden', true); //hide spinner

    for (var i = 0; i < commitContainer.length; i++) {

      container.querySelector("" + commitContainer[i]).innerHTML = 
      "<h4> Message: " + localData[posData[i]].message + "</h4>" +
      "<h4> Author: " + localData[posData[i]].author + "</h4>" +
      "<h4> Time committed: " + (new Date(localData[posData[i]].time)).toUTCString() +  "</h4>" +
      "<h4>" + "<a href='" + localData[posData[i]].link + "'>Click me to see more!</a>"  + "</h4>";

    }
  };

这段代码可以从localStorage中取到commit数据。然后插入到页面中。 我们需要设置一个条件来决定什么时候调用fetchCommitsfetchCommitsFromLocalStorage方法。 最新的latest.js是这样的:

latest.js

 (function() {
  'use strict';

  var app = {
    spinner: document.querySelector('.loader')
  };

  var container = document.querySelector('.container');
  var commitContainer = ['.first', '.second', '.third', '.fourth', '.fifth'];
  var posData = ['first', 'second', 'third', 'fourth', 'fifth'];

  // 检查localStorage是否可用
  function storageAvailable(type) {
    try {
      var storage = window[type],
        x = '__storage_test__';
      storage.setItem(x, x);
      storage.removeItem(x);
      return true;
    }
    catch(e) {
      return false;
    }
  }

  // 通过GitHub API获取commit 数据
  function fetchCommits() {
    var url = 'https://api.github.com/repos/unicodeveloper/resources-i-like/commits';

    fetch(url)
    .then(function(fetchResponse){ 
      return fetchResponse.json();
    })
    .then(function(response) {
        console.log("Response from Github", response);

        var commitData = {};

        for (var i = 0; i < posData.length; i++) {
          commitData[posData[i]] = {
            message: response[i].commit.message,
            author: response[i].commit.author.name,
            time: response[i].commit.author.date,
            link: response[i].html_url
          };
        }

        localStorage.setItem('commitData', JSON.stringify(commitData));

        for (var i = 0; i < commitContainer.length; i++) {

          container.querySelector("" + commitContainer[i]).innerHTML = 
          "<h4> Message: " + response[i].commit.message + "</h4>" +
          "<h4> Author: " + response[i].commit.author.name + "</h4>" +
          "<h4> Time committed: " + (new Date(response[i].commit.author.date)).toUTCString() +  "</h4>" +
          "<h4>" + "<a href='" + response[i].html_url + "'>Click me to see more!</a>"  + "</h4>";

        }

        app.spinner.setAttribute('hidden', true); // hide spinner
      })
      .catch(function (error) {
        console.error(error);
      });
  };

  // 从localStorage中取数据
  function fetchCommitsFromLocalStorage(data) {
    var localData = JSON.parse(data);

    app.spinner.setAttribute('hidden', true); //hide spinner

    for (var i = 0; i < commitContainer.length; i++) {

      container.querySelector("" + commitContainer[i]).innerHTML = 
      "<h4> Message: " + localData[posData[i]].message + "</h4>" +
      "<h4> Author: " + localData[posData[i]].author + "</h4>" +
      "<h4> Time committed: " + (new Date(localData[posData[i]].time)).toUTCString() +  "</h4>" +
      "<h4>" + "<a href='" + localData[posData[i]].link + "'>Click me to see more!</a>"  + "</h4>";

    }
  };

  if (storageAvailable('localStorage')) {
    if (localStorage.getItem('commitData') === null) {
      /* The user is using the app for the first time, or the user has not
       * saved any commit data, so show the user some fake data.
       */
      fetchCommits();
      console.log("Fetch from API");
    } else {
      fetchCommitsFromLocalStorage(localStorage.getItem('commitData'));
      console.log("Fetch from Local Storage");
    }   
  }
  else {
    toast("We can't cache your app data yet..");
  }
})();

首先检查localStorage是否可用,如果可用则检查是否缓存有commit数据。如果没有缓存则通过请求获取数据。然后缓存下来并且展示。

接下来强刷一下浏览器,确保执行的是我们最新的代码。 然后断网,访问latest页面。看看发生了什么?Yeah!有数据!

Load Dynamic Data Offline

打开调试工具,你可用看到存到localStorage中的数据。

Store Data Locally

看一下离线状态下的加载速度。

Loads From Service Worker

还要一件事情

现在我们已经通过localStorage实现了即时加载。那我们怎样获取更新数据呢?我们需要想办法能够及时获取到最新的数据,尤其当用户在线时。 最简单的做法,就是加一个刷新按钮。强制通过API获取新的数据。

让我们做一些修改

 <button id="butRefresh" class="headerButton" aria-label="Refresh"></button>

加了按钮后的 <head>标签是这样的:

 <header>
  <span class="header__icon">
    <svg class="menu__icon no--select" width="24px" height="24px" viewBox="0 0 48 48" fill="#fff">
      <path d="M6 36h36v-4H6v4zm0-10h36v-4H6v4zm0-14v4h36v-4H6z"></path>
    </svg>
  </span>
  <span class="header__title no--select">PWA - Commits</span>
  <button id="butRefresh" class="headerButton" aria-label="Refresh"></button>
</header>

最后,绑定click事件,然后实现处理方法。

 document.getElementById('butRefresh').addEventListener('click', function() {
    // Get fresh, updated data from GitHub whenever you are clicked
    toast('Fetching latest data...');
    fetchCommits();
    console.log("Getting fresh data!!!");
});

清一下缓存,重新访问应用,现在看起来是这样的了:

PWA - Get Updated data

用户可以随时通过点击刷新按钮来获取最新数据。

题外话: 使用Auth0

试试用Auth0 Lock来快速实现登陆功能,只需要加载auth0-lock扩展,然后像这样使用:

 // 初始化  Auth0Lock
var lock = new Auth0Lock(
  'YOUR_CLIENT_ID',
  'YOUR_AUTH0_DOMAIN'
);

// 监听 authenticated event
lock.on("authenticated", function(authResult) {
  // Use the token in authResult to getProfile() and save it to localStorage
  lock.getProfile(authResult.idToken, function(error, profile) {
    if (error) {
      // Handle error
      return;
    }

    localStorage.setItem('idToken', authResult.idToken);
    localStorage.setItem('profile', JSON.stringify(profile));
  });
});
 document.getElementById('btn-login').addEventListener('click', function() {
  lock.show();
});

显示登陆页

Auth0 Lock Screen

实际效果

在离线情况下,不可能通过远程的服务器来鉴权用户身份。但是,有了service workers,就可以完全控制离线状态下哪些页面和脚本可以被加载。意思是我们可以通过配置offline.html来显示一个提示信息,告知用户需要恢复网络链接以便登陆。而不是直接显示一个登陆页。

结论

在这篇文章中,我们实现了即时加载。并且实现了缓存动态数据。使应用离线可以。 在最后一篇文章中,我们将学习如何实现推送,使用manifest实现添加我们的应用到用户主屏。

译者xjcloudy尚未开通打赏功能

相关文章