构建web离线应用(二)

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

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

在这篇文章中,咱们将要:html

  • 了解社区中常见的缓存策略
  • 尝试可用的缓存 api
  • 作一个用来展现 Github trending project 的 demo
  • 在 demo 中演示离线状态下利用缓存所带来的体验

缓存策略

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

Install 期间缓存

这个方案咱们在上一篇文章中介绍过,缓存 app shell 展现时须要的全部资源:git

self.addEventListener('install', function(e) {
  console.log('[ServiceWorker] Install');
  e.waitUntil(
    caches.open(cacheName).then(function(cache) {
      console.log('[ServiceWorker] Caching app shell');
      return cache.addAll(filesToCache);
    })
  );
});

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

缓存请求返回的数据

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

self.addEventListener('fetch', function(event) {
  event.respondWith(
    caches.open(cacheName).then(function(cache) {
      return cache.match(event.request).then(function (response) {
        return response || fetch(event.request).then(function(response) {
          cache.put(event.request, response.clone());
          return response;
        });
      });
    })
  );
});

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

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

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

let networkReturned = false;
if ('caches' in window) {
  caches.match(app.apiURL).then(function(response) {
    if (response) {
      response.json().then(function(trends) {
        console.log('From cache...')
        if(!networkReturned) {
          app.updateTrends(trends);
        }
      });
    }
  });
}

fetch(app.apiURL)
.then(response => response.json())
.then(function(trends) {
  console.log('From server...')
  networkReturned = true;
  app.updateTrends(trends.items)
}).catch(function(err) {
  // Error
});

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

缓存部分技术选型

目前有两种可持续性数据存储方案 -- 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 文件:

npm init -y

安装 sw-precache:

npm install --save-dev sw-precache

建立一个配置文件:

// ./tools/precache.js

const name = 'scotchPWA-v1'
module.exports = {
  staticFileGlobs: [
    './index.html',
    './images/*.{png,svg,gif,jpg}',
    './fonts/**/*.{woff,woff2}',
    './js/*.js',
    './css/*.css',
    'https://fonts.googleapis.com/icon?family=Material+Icons'
  ],
  stripPrefix: '.'
};

staticFileGlobs 里面利用正则匹配咱们想要缓存的文件。只须要利用正则,比以前枚举全部的文件简单不少。

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

"scripts": {
  "sw": "sw-precache --config=tools/precache.js --verbose"
},

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

npm run sw

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

完成 demo

在作 web 应用离线功能以前,让咱们先来完成应用的基本功能。

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

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

  app.getTrends = function() {
    fetch(app.apiURL)
    .then(response => response.json())
    .then(function(trends) {
      console.log('From server...')
      app.updateTrends(trends.items)
    }).catch(function(err) {
      // Error
    });
  }

  document.addEventListener('DOMContentLoaded', function() {
    app.getTrends()
  })

  if ('serviceWorker' in navigator) {
    navigator.serviceWorker
     .register('/service-worker.js')
     .then(function() { 
        console.log('Service Worker Registered'); 
      });
  }
})()

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

Date.prototype.yyyymmdd = function() {
  // getMonth is zero based,
  // so we increment by 1
  let mm = this.getMonth() + 1;
  let dd = this.getDate();

  return [this.getFullYear(),
          (mm>9 ? '' : '0') + mm,
          (dd>9 ? '' : '0') + dd
        ].join('-');
};

const dates = {
  startDate: function() {
     const startDate = new Date();
     startDate.setDate(startDate.getDate() - 7);
     return startDate.yyyymmdd();
   },
   endDate: function() {
     const endDate = new Date();
     return endDate.yyyymmdd();
   }
 }

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

getTrends 获取数据以后,调用了 updateTrends 方法,传入获取到的数据。让咱们看看这个方法作了些什么:

app.updateTrends = function(trends) {
 const trendsRow = document.querySelector('.trends');
  for(let i = 0; i < trends.length; i++) {
    const trend = trends[i];
    trendsRow.appendChild(app.createCard(trend));
  }
}

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

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

<div class="row trends">
 <!-- append here -->
</div>

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

const app = {
  apiURL: `...`,
  cardTemplate: document.querySelector('.card-template')
}

app.createCard = function(trend) {
  const card = app.cardTemplate.cloneNode(true);
  card.classList.remove('card-template')
  card.querySelector('.card-title').textContent = trend.full_name
  card.querySelector('.card-lang').textContent = trend.language
  card.querySelector('.card-stars').textContent = trend.stargazers_count
  card.querySelector('.card-forks').textContent = trend.forks
  card.querySelector('.card-link').setAttribute('href', trend.html_url)
  card.querySelector('.card-link').setAttribute('target', '_blank')
  return card;
}

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

<div class="row trends">
  <divclass="col s12 m4 card-template">
    <div class="card horizontal">
      <div class="card-stacked">
        <div class="card-content white-text">
          <span class="card-title">Card Title</span>
          <div class="card-sub grey-text text-lighten-2">
            <i class="material-icons">info</i><span class="card-lang"> JavaScript</span>
            <i class="material-icons">star</i><span class="card-stars"> 299</span>
            <i class="material-icons">assessment</i><span class="card-forks"> 100</span>
          </div>
          <p>A set of best practices for JavaScript projects</p>
        </div>
        <div class="card-action">
          <a href="#" class="card-link">Visit Repo</a>
        </div>
      </div>
    </div>
  </div>
</div>

pwa-card

运行时缓存的内容

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

咱们须要提早配置告诉 service worker ,在运行时须要缓存的文件:

// ./tools/precache.js
const name = 'scotchPWA-v1'
module.exports = {
  staticFileGlobs: [
    // ...
  ],
  stripPrefix: '.',
  // Run time cache
  runtimeCaching: [{
    urlPattern: /https:\/\/api\.github\.com\/search\/repositories/,
    handler: 'networkFirst',
    options: {
      cache: {
        name: name
      }
    }
  }]
};

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

这样,咱们先展现缓存的内容,当有网络链接时候,更新内容:

app.getTrends = function() {
 const networkReturned = false;
  if ('caches' in window) {
    caches.match(app.apiURL).then(function(response) {
      if (response) {
        response.json().then(function(trends) {
          console.log('From cache...')
          if(!networkReturned) {
            app.updateTrends(trends);
          }
        });
      }
    });
  }

  fetch(app.apiURL)
  .then(response => response.json())
  .then(function(trends) {
    console.log('From server...')
    app.updateTrends(trends.items)
    networkReturned = true;
  }).catch(function(err) {
    // Error
  });
}

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

const name = 'scotchPWA-v2'
npm run sw

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

图片描述

刷新

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

document.addEventListener('DOMContentLoaded', function() {
 app.getTrends()

 // Event listener for refresh button
 const refreshButton = document.querySelector('.refresh');
 refreshButton.addEventListener('click', app.getTrends)
})

下一步?

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

相关文章
相关标签/搜索