構建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)
})

下一步?

感受不是很知足?如今你已經知道了如何建立離線應用,在接下來的文章中,咱們將繼續討論這項技術的有趣之處,包括推送通知,主屏幕圖標建立等等···

相關文章
相關標籤/搜索