构建离线web应用(二)

2017 年 12 月 7 日 前端黑板报 墨白

本文由哔哩哔哩前端工程师 墨白 翻译分享

上一篇文章中,我们成功尝试使用 service workers。我们也可以在应用中缓存一些资源。这篇文章我们准备了解这些:service workers 以及缓存是如何一起配合给用户一个完美的离线体验。

在前一个章节当我们学习如何 debugger 的时候,我们了解到浏览器的缓存存储。提及缓存时,不仅仅是指存储,还包括浏览器内用来保存数据以供离线使用的策略。

在这篇文章中,我们将要:

  • 了解社区中常见的缓存策略

  • 尝试可用的缓存 api

  • 做一个用来展示 Github trending project 的 demo

  • 在 demo 中演示离线状态下利用缓存所带来的体验

缓存策略

软件工程中的每一个理论都是对同一类问题解决方案的总结,每一个都需要时间整理并被大众接受,成为推荐的解决方案。对于 PWA 的缓存策略来说同样如此。Jake Archibald 汇总了很多常用的方案,但我们只打算介绍其中一些常用的:

Install 期间缓存

这个方案我们在上一篇文章中介绍过,缓存 app shell 展示时需要的所有资源:

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

  2.  console.log('[ServiceWorker] Install');

  3.  e.waitUntil(

  4.    caches.open(cacheName).then(function(cache) {

  5.      console.log('[ServiceWorker] Caching app shell');

  6.      return cache.addAll(filesToCache);

  7.    })

  8.  );

  9. });

缓存的资源包括 HTML 模板,CSS 文件,JavaScript,fonts,少量的图片。

缓存请求返回的数据

这个方案是指如果之前的网络请求数据被缓存了,那么就用缓存的数据更新页面。如果缓存不可用,那直接去网络请求数据。当请求成功返回时,利用返回的数据更新页面并缓存返回的数据。

  
    
    
    
  1. self.addEventListener('fetch', function(event) {

  2.  event.respondWith(

  3.    caches.open(cacheName).then(function(cache) {

  4.      return cache.match(event.request).then(function (response) {

  5.        return response || fetch(event.request).then(function(response) {

  6.          cache.put(event.request, response.clone());

  7.          return response;

  8.        });

  9.      });

  10.    })

  11.  );

  12. });

这种方案主要应用用户频繁手动更新内容的场景,比如用户的收件箱或者文章内容。

先展示缓存,再根据请求的数据更新页面

这种方案将同时请求缓存以及服务端的数据。如果某一项在缓存中有对应的数据,好,直接在页面中展示。当网络请求的数据返回时,利用返回的数据更新页面:

  
    
    
    
  1. let networkReturned = false;

  2. if ('caches' in window) {

  3.  caches.match(app.apiURL).then(function(response) {

  4.    if (response) {

  5.      response.json().then(function(trends) {

  6.        console.log('From cache...')

  7.        if(!networkReturned) {

  8.          app.updateTrends(trends);

  9.        }

  10.      });

  11.    }

  12.  });

  13. }

  14. fetch(app.apiURL)

  15. .then(response => response.json())

  16. .then(function(trends) {

  17.  console.log('From server...')

  18.  networkReturned = true;

  19.  app.updateTrends(trends.items)

  20. }).catch(function(err) {

  21.  // Error

  22. });

在大多数情况下,网络请求返回的数据会将从缓存中取出的数据覆盖。但在网页中,什么情况都有可能发生,有时候网络请求数据比从缓存中取数据要快。因此,我们需要设置一个 flag 来判断网络请求有没有返回,这就是上例中的 networkReturned。

缓存部分技术选型

目前有两种可持续性数据存储方案 -- Cache Storage 以及 Index DB(IDB)。

  • Cache Storage:在过去的一段时间里,我们依赖 AppCache 来进行缓存处理,但我们需要一个可操作性更强的 API。幸运的是,浏览器提供了 Cache 这样的一个 API,给 Service Worker 的缓存操作带来了更多的可能。并且,这个 API 同时支持 service workers 以及 web 页面。在前一篇文章中,我们已经使用过了这个 API。

  • Index DB:Index DB 是一个异步数据存储方案。对于这个 API 是又爱又恨,还好,像localForage这样的类库使用类似localStorage的操作方式简化了API。

Service Worker 对于这两种存储方案都提供支持。那么问题来了,什么场景下选择哪一种技术方案呢? Addy Osmani 的博客已经总结好了。

对于利用 URL 可直接查看的资源,使用支持 Service Worker 的 Cache Storage。其它类型的资源,使用利用 Promise 包裹之后的 IndexedDB。

SW Precache

上文已经介绍了缓存策略以及数据缓存数据。在实战之前,还想给大家介绍一下谷歌的 SW Precache。

这个工具还有一个额外的功能:将我们之前讨论的缓存文件设置利用正则简化成一个配置对象。所有你需要做的就是在一个数组中定义缓存的项目。

让我们来尝试使用一下 precache,让其自动生成 service-worker.js。首先,我们需要在项目的根目录下新增一个 package.json 文件:

  
    
    
    
  1. npm init -y

安装 sw-precache:

  
    
    
    
  1. npm install --save-dev sw-precache

创建一个配置文件:

  
    
    
    
  1. // ./tools/precache.js

  2. const name = 'scotchPWA-v1'

  3. module.exports = {

  4.  staticFileGlobs: [

  5.    './index.html',

  6.    './images/*.{png,svg,gif,jpg}',

  7.    './fonts/**/*.{woff,woff2}',

  8.    './js/*.js',

  9.    './css/*.css',

  10.    'https://fonts.googleapis.com/icon?family=Material+Icons'

  11.  ],

  12.  stripPrefix: '.'

  13. };

staticFileGlobs 里面利用正则匹配我们想要缓存的文件。只需要利用正则,比之前枚举所有的文件简单很多。

在 package.json 中新增一个 script 用来生成 service worker 文件:

  
    
    
    
  1. "scripts": {

  2.  "sw": "sw-precache --config=tools/precache.js --verbose"

  3. },

运行下面的命令即可生成 service worker 文件:

  
    
    
    
  1. npm run sw

查看生成的文件,是不是很熟悉?

完成 demo

在做 web 应用离线功能之前,让我们先来完成应用的基本功能。

回到 app.js 文件,我们要在页面加载完成时去获取当前 Github 流行的项目(项目以 star 数的多少来排序):

  
    
    
    
  1. (function() {

  2.  const app = {

  3.    apiURL: `https://api.github.com/search/repositories?q=created:%22${dates.startDate()}+..+${dates.endDate()}%22%20language:javascript&sort=stars&order=desc`

  4.  }

  5.  app.getTrends = function() {

  6.    fetch(app.apiURL)

  7.    .then(response => response.json())

  8.    .then(function(trends) {

  9.      console.log('From server...')

  10.      app.updateTrends(trends.items)

  11.    }).catch(function(err) {

  12.      // Error

  13.    });

  14.  }

  15.  document.addEventListener('DOMContentLoaded', function() {

  16.    app.getTrends()

  17.  })

  18.  if ('serviceWorker' in navigator) {

  19.    navigator.serviceWorker

  20.     .register('/service-worker.js')

  21.     .then(function() {

  22.        console.log('Service Worker Registered');

  23.      });

  24.  }

  25. })()

注意 API URL 字符串中的日期。我们是这样构造的:

  
    
    
    
  1. Date.prototype.yyyymmdd = function() {

  2.  // getMonth is zero based,

  3.  // so we increment by 1

  4.  let mm = this.getMonth() + 1;

  5.  let dd = this.getDate();

  6.  return [this.getFullYear(),

  7.          (mm>9 ? '' : '0') + mm,

  8.          (dd>9 ? '' : '0') + dd

  9.        ].join('-');

  10. };

  11. const dates = {

  12.  startDate: function() {

  13.     const startDate = new Date();

  14.     startDate.setDate(startDate.getDate() - 7);

  15.     return startDate.yyyymmdd();

  16.   },

  17.   endDate: function() {

  18.     const endDate = new Date();

  19.     return endDate.yyyymmdd();

  20.   }

  21. }

yyyymmdd 帮我们将日期构造成 Github API 所规定的格式( yyyy-mm-dd)。

当 getTrends 获取数据之后,调用了 updateTrends 方法,传入获取到的数据。让我们看看这个方法做了些什么:

  
    
    
    
  1. app.updateTrends = function(trends) {

  2. const trendsRow = document.querySelector('.trends');

  3.  for(let i = 0; i < trends.length; i++) {

  4.    const trend = trends[i];

  5.    trendsRow.appendChild(app.createCard(trend));

  6.  }

  7. }

遍历请求返回的数据,利用 createCard 来创建 DOM 模板,然后,将这段 DOM 插入 .trends 元素:

  
    
    
    
  1. <!-- ./index.html -->

  2. <div class="row trends">

  3. <!-- append here -->

  4. </div>

createCard 利用下面的代码来创建模板:

  
    
    
    
  1. const app = {

  2.  apiURL: `...`,

  3.  cardTemplate: document.querySelector('.card-template')

  4. }

  5. app.createCard = function(trend) {

  6.  const card = app.cardTemplate.cloneNode(true);

  7.  card.classList.remove('card-template')

  8.  card.querySelector('.card-title').textContent = trend.full_name

  9.  card.querySelector('.card-lang').textContent = trend.language

  10.  card.querySelector('.card-stars').textContent = trend.stargazers_count

  11.  card.querySelector('.card-forks').textContent = trend.forks

  12.  card.querySelector('.card-link').setAttribute('href', trend.html_url)

  13.  card.querySelector('.card-link').setAttribute('target', '_blank')

  14.  return card;

  15. }

下面就是所创建的 DOM 结构:

  
    
    
    
  1. <div class="row trends">

  2.  <divclass="col s12 m4 card-template">

  3.    <div class="card horizontal">

  4.      <div class="card-stacked">

  5.        <div class="card-content white-text">

  6.          <span class="card-title">Card Title</span>

  7.          <div class="card-sub grey-text text-lighten-2">

  8.            <i class="material-icons">info</i><span class="card-lang"> JavaScript</span>

  9.            <i class="material-icons">star</i><span class="card-stars"> 299</span>

  10.            <i class="material-icons">assessment</i><span class="card-forks"> 100</span>

  11.          </div>

  12.          <p>A set of best practices for JavaScript projects</p>

  13.        </div>

  14.        <div class="card-action">

  15.          <a href="#" class="card-link">Visit Repo</a>

  16.        </div>

  17.      </div>

  18.    </div>

  19.  </div>

  20. </div>

运行时缓存的内容

在应用程序运行时,需要缓存从服务端获取的动态内容。不再是 app shell 了,而是用户真正浏览的内容。

我们需要提前配置告诉 service worker ,在运行时需要缓存的文件:

  
    
    
    
  1. // ./tools/precache.js

  2. const name = 'scotchPWA-v1'

  3. module.exports = {

  4.  staticFileGlobs: [

  5.    // ...

  6.  ],

  7.  stripPrefix: '.',

  8.  // Run time cache

  9.  runtimeCaching: [{

  10.    urlPattern: /https:\/\/api\.github\.com\/search\/repositories/,

  11.    handler: 'networkFirst',

  12.    options: {

  13.      cache: {

  14.        name: name

  15.      }

  16.    }

  17.  }]

  18. };

我们定义了一个 url 正则匹配符,匹配成功时,读取缓存。这个正则匹配所有的 Github 搜索 API。我们打算应用“Cache, Then network.”的策略。

这样,我们先展示缓存的内容,当有网络连接时候,更新内容:

  
    
    
    
  1. app.getTrends = function() {

  2. const networkReturned = false;

  3.  if ('caches' in window) {

  4.    caches.match(app.apiURL).then(function(response) {

  5.      if (response) {

  6.        response.json().then(function(trends) {

  7.          console.log('From cache...')

  8.          if(!networkReturned) {

  9.            app.updateTrends(trends);

  10.          }

  11.        });

  12.      }

  13.    });

  14.  }

  15.  fetch(app.apiURL)

  16.  .then(response => response.json())

  17.  .then(function(trends) {

  18.    console.log('From server...')

  19.    app.updateTrends(trends.items)

  20.    networkReturned = true;

  21.  }).catch(function(err) {

  22.    // Error

  23.  });

  24. }

在 precache.js 中更新缓存的版本,重新生成 service worker:

  
    
    
    
  1. const name = 'scotchPWA-v2'

  
    
    
    
  1. npm run sw

当你运行应用的时候,尝试刷新,打开控制台,勾选 offline 选项。之后,刷新,以及见证奇迹的时刻:

刷新

用户可能需要在网络情况更佳的时候刷新页面,我们需要给予用户这样的权利。我们可以给刷新按钮添加一个事件,当时间触发时,调用 getTrends 方法:

  
    
    
    
  1. document.addEventListener('DOMContentLoaded', function() {

  2. app.getTrends()

  3. // Event listener for refresh button

  4. const refreshButton = document.querySelector('.refresh');

  5. refreshButton.addEventListener('click', app.getTrends)

  6. })

下一步?

感觉不是很满足?现在你已经知道了如何创建离线应用,在接下来的文章中,我们将继续讨论这项技术的有趣之处,包括推送通知,主屏幕图标创建等等···


登录查看更多
3

相关内容

【2020新书】实战R语言4,323页pdf
专知会员服务
98+阅读 · 2020年7月1日
干净的数据:数据清洗入门与实践,204页pdf
专知会员服务
160+阅读 · 2020年5月14日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
115+阅读 · 2020年5月10日
【SIGMOD2020-腾讯】Web规模本体可扩展构建
专知会员服务
28+阅读 · 2020年4月12日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
68+阅读 · 2020年1月17日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
94+阅读 · 2019年12月4日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
20+阅读 · 2019年11月7日
滴滴离线索引快速构建FastIndex架构实践
InfoQ
21+阅读 · 2020年3月19日
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
DataCanvas周晓凌:如何为用户提供最佳体验的实时推荐系统
DataCanvas大数据云平台
5+阅读 · 2018年11月12日
基于 Storm 的实时数据处理方案
开源中国
4+阅读 · 2018年3月15日
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
设计和实现一款轻量级的爬虫框架
架构文摘
13+阅读 · 2018年1月17日
深度学习在情感分析中的应用
CSDN
7+阅读 · 2017年8月23日
Arxiv
3+阅读 · 2019年3月1日
Arxiv
5+阅读 · 2018年10月23日
Rapid Customization for Event Extraction
Arxiv
7+阅读 · 2018年9月20日
Next Item Recommendation with Self-Attention
Arxiv
5+阅读 · 2018年8月25日
Arxiv
14+阅读 · 2018年5月15日
Arxiv
7+阅读 · 2018年1月30日
VIP会员
相关VIP内容
【2020新书】实战R语言4,323页pdf
专知会员服务
98+阅读 · 2020年7月1日
干净的数据:数据清洗入门与实践,204页pdf
专知会员服务
160+阅读 · 2020年5月14日
【实用书】Python爬虫Web抓取数据,第二版,306页pdf
专知会员服务
115+阅读 · 2020年5月10日
【SIGMOD2020-腾讯】Web规模本体可扩展构建
专知会员服务
28+阅读 · 2020年4月12日
TensorFlow Lite指南实战《TensorFlow Lite A primer》,附48页PPT
专知会员服务
68+阅读 · 2020年1月17日
【干货】大数据入门指南:Hadoop、Hive、Spark、 Storm等
专知会员服务
94+阅读 · 2019年12月4日
【电子书】Flutter实战305页PDF免费下载
专知会员服务
20+阅读 · 2019年11月7日
相关资讯
滴滴离线索引快速构建FastIndex架构实践
InfoQ
21+阅读 · 2020年3月19日
用Now轻松部署无服务器Node应用程序
前端之巅
16+阅读 · 2019年6月19日
PHP使用Redis实现订阅发布与批量发送短信
安全优佳
7+阅读 · 2019年5月5日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
DataCanvas周晓凌:如何为用户提供最佳体验的实时推荐系统
DataCanvas大数据云平台
5+阅读 · 2018年11月12日
基于 Storm 的实时数据处理方案
开源中国
4+阅读 · 2018年3月15日
浅谈浏览器 http 的缓存机制
前端大全
6+阅读 · 2018年1月21日
设计和实现一款轻量级的爬虫框架
架构文摘
13+阅读 · 2018年1月17日
深度学习在情感分析中的应用
CSDN
7+阅读 · 2017年8月23日
相关论文
Arxiv
3+阅读 · 2019年3月1日
Arxiv
5+阅读 · 2018年10月23日
Rapid Customization for Event Extraction
Arxiv
7+阅读 · 2018年9月20日
Next Item Recommendation with Self-Attention
Arxiv
5+阅读 · 2018年8月25日
Arxiv
14+阅读 · 2018年5月15日
Arxiv
7+阅读 · 2018年1月30日
Top
微信扫码咨询专知VIP会员