建立一個離線優先,數據驅動的漸進式 Web 應用程序

原文地址: Build an offline-first, data-driven PWA
譯文出自: 個人我的博客

概述

在本文中,您將學習如何使用 Workbox 和 IndexedDB 建立離線優先、數據驅動的漸進式Web應用程序(PWA)。在離線的狀況下也可使用後臺同步功能將應用程序與服務器同步。css

將會學習到

  • 如何使用 Workbox 緩存應用程序
  • 如何使用 IndexedDB 存儲數據
  • 如何在用戶脫機時從 IndexedDB 中檢索和顯示數據
  • 脫機時如何保存數據
  • 如何在脫機時使用後臺同步更新應用程序

應該瞭解的

  • HTML, CSS, 和 JavaScript
  • ES2015 Promises
  • 如何使用命令行
  • 熟悉一下 Workbox
  • 熟悉一下 Gulp
  • 熟悉一下 IndexedDB

需具有的條件

  • 擁有 terminal/shell 訪問權限的電腦
  • Chrome 52 或更高版本
  • 編輯器
  • Nodejs 和 npm

設置

若是你沒有安裝 Nodejs 須要安裝一下html

以後經過下面的方式 clone 快速啓動倉庫node

git clone https://github.com/googlecodelabs/workbox-indexeddb.git

或者直接下載 壓縮包git

安裝依賴並啓動服務

到下載好的 git 倉庫目錄中,轉到 project 文件夾github

cd workbox-indexeddb/project/

而後安裝依賴並啓動服務web

npm install
npm start

說明

這個步驟中會根據 package.json 定義的依賴並安裝,打開 package.json 文件查看,有不少依賴,大部分是開發環境須要的(你能夠忽略),主要的依賴是:chrome

npm start 會構建並輸出到 build 文件夾,啓動 dev server,而且會開啓一個 gulp watch 任務。gulp watch 會監聽文件的修改自動構建。concurrently 能夠同時跑 gulp 和 dev servershell

打開應用

打開 Chrome 而且跳轉到 localhost:8081 你會看到一個事件列表的控制檯,在彈出的權限確認菜單中點擊容許數據庫

img

咱們使用通知系統來告知用戶 app 的後臺同步已經更新,試着測試一下頁面底部的添加功能npm

說明

這個小項目的目標是離線保存用戶的事件日曆。你能夠查看一下 app/js/main.js 文件的 loadContentNetworkFirst 方法當前是怎麼工做的,首先會請求 server,成功則更新頁面,失敗會在控制檯打印一個信息,目前脫機是沒法使用的,接下來咱們添加一些方法使它脫機可用。

緩存 app shell

編寫 service worker

要想脫機工做,就須要 server worker,如今寫一個。

把下面的代碼添加到 app/src/sw.js

importScripts('workbox-sw.dev.v2.0.0.js');
importScripts('workbox-background-sync.dev.v2.0.0.js');

const workboxSW = new WorkboxSW();
workboxSW.precache([]);

說明

在開頭咱們引入了 workbox-swworkbox-background-sync

  • workbox-sw 包含了 precache 和向 service worker 添加路由的方法
  • workbox-background-sync 是在 service worker 中後臺同步的庫,稍後會提到

precache 方法接收一個文件列表的數組,先用一個空的,下一步咱們會用 workbox-build 去計算出這個數組的結果。

構建 service worker

推薦使用 Workbox 的構建模塊,好比 workbox-build

把下面的代碼添加進 project/gulpfile.js

gulp.task('build-sw', () => {
  return wbBuild.injectManifest({
    swSrc: 'app/src/sw.js',
    swDest: 'build/service-worker.js',
    globDirectory: 'build',
    staticFileGlobs: [
      'style/main.css',
      'index.html',
      'js/idb-promised.js',
      'js/main.js',
      'images/**/*.*',
      'manifest.json'
    ],
    templatedUrls: {
      '/': ['index.html']
    }
  }).catch((err) => {
    console.log('[ERROR] This happened: ' + err);
  });
});

如今取消一些註釋:

gulpfile.js:

// uncomment the line below:
const wbBuild = require('workbox-build');

// ...

gulp.task('default', ['clean'], cb => {
  runSequence(
    'copy',
    // uncomment the line below:
    'build-sw',
    cb
  );
});

保存修改,由於修改了 gulp,咱們得從新跑一下,Ctrl + C 退出當前的進程,從新運行 npm start,會看到 service worker 的文件被生成在了 build/service-worker.js

取消 app/index.html 中 service worker 註冊代碼的註釋

if ('serviceWorker' in navigator) {
  navigator.serviceWorker.register('service-worker.js')
    .then(function(registration) {
      console.log('Service Worker registration successful with scope: ',
      registration.scope);
    })
    .catch(function(err) {
      console.log('Service Worker registration failed: ', err);
    });
}

保存修改,刷新瀏覽器 service worker 就會被安裝。Ctrl + C 關閉 dev server,再返回到瀏覽器中刷新頁面,已經能夠脫機運行了!

說明

在這一步中,workbox-buildbuild-sw 任務被合併到咱們的 gulp 文件中,咱們的構建過程是使用 workbox-build 庫來從 swSrc(app/src/sw.js) 中生成 service work 到 swDest(build/service-worker.js),來自 globDirectory(build)staticFileGlobs 文件被注入到 build/service-worker.js 以供 precache 調用,還有每一個文件的修訂哈希。templatedUrls 選項告訴 Workbox 咱們的站點以 index.html 的內容響應請求。

順便貼一個 injectManifest 的連接

安裝生成好的 service worker 緩存 app shell 的資源文件,Workbox 會自動去:

  • 爲緩存資源設置緩存優先策略,容許應用程序離線加載
  • service work 更新時,使用修訂哈希來更新緩存的文件

建立 IndexedDB 數據庫

目前爲止還不能離線加載數據,咱們接下來建立一個 IndexDB 來保存程序的數據,數據庫命名爲 dashboardr

添加下面代碼到 app/js/main.js

function createIndexedDB() {
  if (!('indexedDB' in window)) {return null;}
  return idb.open('dashboardr', 1, function(upgradeDb) {
    if (!upgradeDb.objectStoreNames.contains('events')) {
      const eventsOS = upgradeDb.createObjectStore('events', {keyPath: 'id'});
    }
  })
}

取消調用 createIndexedDB 的註釋:

const dbPromise = createIndexedDB();

保存文件,重啓 server:

npm start

回到瀏覽器刷新頁面,激活 skipWaiting 並再次刷新頁面,在 Chrome 中,你能夠在開發者工具中的 Application 面板中選擇 Service Workers 點擊 skipWaiting,以後使用 開發者工具 檢查數據庫是否存在。在 Chrome 中你能夠在 Application 面板中點擊 IndexedDB 選擇 dashboardr 查看 events 對象是否存在。

注意:開發者工具的 IndexedDB UI 可能不會準確的反應你數據庫的狀況,在 Chrome 中你能夠刷新數據庫查看,或者從新打開開發者工具

說明

在上面的代碼中,咱們建立了一個 dashboardr 數據庫,並把他的版本號設置爲 1 ,而後檢查 events 對象是否存在,這個檢查是爲了不潛在的錯誤,咱們還給 event 提供了一個惟一的 key path id

因爲咱們修改了 app/main.js 文件,gulp 的 watch 任務會自動構建,Workbox 會自動更新修訂哈希,而後智能更新緩存中的 main.js

保存數據到 IndexedDB 中

如今咱們保存數據到剛建立的數據庫 dashboardr 中的 event 對象中。

function saveEventDataLocally(events) {
  if (!('indexedDB' in window)) {return null;}
  return dbPromise.then(db => {
    const tx = db.transaction('events', 'readwrite');
    const store = tx.objectStore('events');
    return Promise.all(events.map(event => store.put(event)))
    .catch(() => {
      tx.abort();
      throw Error('Events were not added to the store');
    });
  });
}

而後更新 loadContentNetworkFirst 方法,如今這是完整的方法:

function loadContentNetworkFirst() {
  getServerData()
  .then(dataFromNetwork => {
    updateUI(dataFromNetwork);
    saveEventDataLocally(dataFromNetwork)
    .then(() => {
      setLastUpdated(new Date());
      messageDataSaved();
    }).catch(err => {
      messageSaveError(); 
      console.warn(err);
    });
  }).catch(err => { // if we can't connect to the server...
    console.log('Network requests have failed, this is expected if offline');
  });
}

取消註釋 addAndPostEvent 中的 saveEventDataLocally 調用

function addAndPostEvent() {
  // ...
  saveEventDataLocally([data]);
  // ...
}

保存文件,刷新頁面從新激活 service worker。再次刷新頁面,檢查一下來自網絡的數據是否被保存到 events 中去(你可能須要刷新一下開發者工具中的 IndexedDB

說明

saveEventDataLocally 接收一個數組並一條條的保存到 IndexedDB 數據庫中,咱們把 store.put 寫在了 Promise.all 中,這樣若是某一條更新出錯咱們就能夠終止事務。

loadContentNetworkFirst 方法中,一旦收到來自服務器的數據,就會更新 IndexedDB 和頁面。而後,數據成功保存時,將存儲時間戳,並通知用戶數據可供離線使用。

addAndPostEvent 中調用 saveEventDataLocally 方法保證了添加新的 event 時本地會存有最新的數據。

從 IndexedDB 中獲取數據

離線的時候,咱們就要查詢本地緩存的數據。

添加下面的代碼到 app/js/main.js 中:

function getLocalEventData() {
  if (!('indexedDB' in window)) {return null;}
  return dbPromise.then(db => {
    const tx = db.transaction('events', 'readonly');
    const store = tx.objectStore('events');
    return store.getAll();
  });
}

而後更新 loadContentNetworkFirst 方法,完整的方法以下:

function loadContentNetworkFirst() {
  getServerData()
  .then(dataFromNetwork => {
    updateUI(dataFromNetwork);
    saveEventDataLocally(dataFromNetwork)
    .then(() => {
      setLastUpdated(new Date());
      messageDataSaved();
    }).catch(err => {
      messageSaveError();
      console.warn(err);
    });
  }).catch(err => {
    console.log('Network requests have failed, this is expected if offline');
    getLocalEventData()
    .then(offlineData => {
      if (!offlineData.length) {
        messageNoData();
      } else {
        messageOffline();
        updateUI(offlineData); 
      }
    });
  });
}

保存文件,刷新瀏覽器激活更新的 service worker,如今 Ctrl + C 關閉 dev server,返回到瀏覽器中刷新頁面,如今 app 和數據均可以離線加載了!

說明

loadContentNetworkFirst 被調用的時候若是沒有網絡鏈接,getServerData 會被 reject,以後便會進入到 catch 中去,而後 getLocalEventData 會調用本地緩存的數據。有網絡鏈接的話會正常的請求 server 而且 updateUI

使用 workbox-background-sync

咱們的 app 已經能夠離線保存和瀏覽數據,如今咱們來用 workbox-background-sync 把離線狀態下保存的數據同步到服務端去。

把下面的的代碼添加到 app/src/sw.js

let bgQueue = new workbox.backgroundSync.QueuePlugin({
  callbacks: {
    replayDidSucceed: async(hash, res) => {
      self.registration.showNotification('Background sync demo', {
        body: 'Events have been updated!'
      });
    }
  }
});

workboxSW.router.registerRoute('/api/add',
  workboxSW.strategies.networkOnly({plugins: [bgQueue]}), 'POST'
);

保存,如今轉到命令行:

npm run start

刷新瀏覽器,激活更新的 service worker

Ctrl + C 把 app 變爲離線狀態,添加一個 event 確認請求 /api/add 已經被添加進 bgQueueSyncDBQueueStore 對象。

說明

當用戶試圖在離線狀況下添加 event 的時候,workbox-background-sync 會把失敗的請求保存爲一個離線隊列,當用戶從新聯網 backgroundSync 會從新發送這些請求,甚至都不須要用戶打開 app!可是,從聯網到從新發請求的這個過程大概須要 5 分鐘,下一節咱們將會介紹如何在 app 中當即發送這些請求。

重發請求

由於重發請求會有延遲,因此用戶可能回到 app 以後尚未同步數據,因此咱們在用戶聯網的時候當即發送這些請求。

把下面的代碼添加到 app/src/sw.js

workboxSW.router.registerRoute('/api/getAll', () => {
  return bgQueue.replayRequests().then(() => {
    return fetch('/api/getAll');
  }).catch(err => {
    return err;
  });
});

只要用戶請求服務端數據(加載或刷新頁面時),該路由就會 replay 排隊的請求,而後返回最新的服務端數據。這很好,可是用戶仍是得刷新頁面去從新獲取數據,咱們還有更好的作法。

把下面的代碼添加進 app/js/main.js

window.addEventListener('online', () => {
  container.innerHTML = '';
  loadContentNetworkFirst();
});

重啓 server

npm start

刷新瀏覽器激活新的 service worker,並再次刷新頁面。

Ctrl + C 把 app 變爲離線狀態

添加一條 event

重啓 server

npm start

這時你應該能當即收到一條數據更新的通知,檢查 server-data/events.json 中的數據是否已經更新。

說明

頁面加載的時候會請求 /api/getAll,咱們攔截了這個請求,以後主要作了兩件事:

  • 同步本地的離線數據
  • 從新請求 /api/getAll

也就是在從新獲取服務端的數據以前先同步

注意:本例中的網絡請求設計的很是簡單,實際狀況下你可能須要考慮更多因素去減小請求的數量。

添加刪除功能

下面的時間就交給你了,添加一個刪除的功能,記得刪除 IndexedDB 中的數據。

相關文章
相關標籤/搜索