PWA學習總結

簡單介紹

PWA(Progressive Web App)漸進式Web APP,它並非單隻某一項技術,而是一系列技術綜合應用的結果,其中主要包含的相關技術就是Service Worker、Cache Api、Fetch Api、Push API、Notification API 和 postMessage API。使用PWA能夠給咱們帶來什麼好處呢?主要體如今以下幾方面javascript

1 離線緩存css

2 web頁面添加桌面快速入口html

3 消息推送java

相關知識

Service Worker

簡單來講,Service Worker 是一個可編程的 Web Worker,它就像一個位於瀏覽器與網絡之間的客戶端代理,能夠攔截、處理、響應流經的 HTTP 請求。它沒有調用 DOM 和其餘頁面 api 的能力,但他能夠攔截網絡請求,包括頁面切換,靜態資源下載,ajax請求所引發的網絡請求。Service Worker 是一個獨立於JavaScript主線程的瀏覽器線程。Service Worker有以下特性:webpack

  • 必須在 HTTPS 環境下才能工做(在開發模式下http://localhost也能夠工做)
  • 不能直接操做 DOM,(可是能夠經過postMessage發送某些信號,主進程根據信號類型,進行不一樣的操做)
  • 一個獨立的 worker 線程,獨立於當前網頁進程,有本身獨立的 worker context。
  • 運行於瀏覽器後臺,能夠控制打開的做用域範圍下全部的頁面請求
  • Service Worker 必需要在主線中進行註冊
  • 一旦被 install,就永遠存在,除非被手動 unregister
  • 用到的時候能夠直接喚醒,不用的時候自動睡眠

註冊Service Work

咱們須要在主線程中註冊Service Worker,而且通常是在頁面觸發load事件以後進行註冊。當Service Worker註冊成功後便會進入其生命週期。scope表明Service Worker控制該路徑下的全部請求,若是請求路徑不是在該路徑之下,則請求不會被攔截。git

// 註冊service worker
window.addEventListener('load', function () {
  navigator.serviceWorker.register('/sw.js', {scope: '/'})
    .then(function (registration) {

      // 註冊成功
      console.log('ServiceWorker registration successful with scope: ', registration.scope);
    })
    .catch(function (err) {

      // 註冊失敗:(
      console.log('ServiceWorker registration failed: ', err);
    });
});

複製代碼

Service Worker生命週期

Service Worker生命週期大體以下github

install -> installed -> actvating -> Active -> Activated -> Redundantweb

Service Worker生命週期圖

在Service Worker註冊成功以後就會觸發install事件,在觸發install事件後,咱們就能夠開始緩存一些靜態資。waitUntil方法確保全部代碼執行完畢後,Service Worker 纔會完成Service Worker的安裝。須要注意的是隻有CACHE_LIST中的資源所有安裝成功後,纔會完成安裝,不然失敗,進入redundant狀態,因此這裏的靜態資源最好不要太多。若是 sw.js 文件的內容有改動,當訪問網站頁面時瀏覽器獲取了新的文件,它會認爲有更新,因而會安裝新的文件並觸發 install 事件。可是此時已經處於激活狀態的舊的 Service Worker 還在運行,新的 Service Worker 完成安裝後會進入 waiting 狀態。直到全部已打開的頁面都關閉,舊的 Service Worker 自動中止,新的 Service Worker 纔會在接下來打開的頁面裏生效。爲了可以讓新的Service Worker及時生效,咱們使用skipWaiting直接使Service Worker跳過等待時期,從而直接進入下一個階段。ajax

const CACHE_NAME = 'cache_v' + 2;
const CACGE_LIST = [
  '/',
  '/index.html',
  '/main.css',
  '/app.js',
  '/icon.png'
];

function preCache() {
  // 安裝成功後操做 CacheStorage 緩存,使用以前須要先經過 caches.open() 打開對應緩存空間。
  return caches.open(CACHE_NAME).then(cache => {
    // 經過 cache 緩存對象的 addAll 方法添加 precache 緩存
    return cache.addAll(CACGE_LIST);
  })
}

// 安裝
self.addEventListener('install', function (event) {
  // 等待promise執行完
  event.waitUntil(
    // 若是上一個serviceWorker不銷燬 須要手動skipWaiting()
    preCache().then(skipWaiting)
  );
});
複製代碼

在安裝成功後,便會觸發activate事件,在進入這個生命週期後,咱們通常會刪除掉以前已通過期的版本(由於默認狀況下瀏覽器是不會自動刪除過時的版本的),並更新客戶端Service Worker(使用當前處於激活狀態的Service Worker)。編程

// 刪除過時緩存
function clearCache() {
  return caches.keys().then(keys => {
    return Promise.all(keys.map(key => {
      if (key !== CACHE_NAME) {
        return caches.delete(key);
      }
    }))
  })
}

// 激活 activate 事件中一般作一些過時資源釋放的工做
self.addEventListener('activate', function (e) {
  e.waitUntil(
    Promise.all([
      clearCache(),
      self.clients.claim()
    ])
  );
});
複製代碼

在這裏還有一個問題就是sw.js文件有可能會被瀏覽器緩存,因此咱們通常須要設置sw.js不緩存或者較短的緩存時間 更多詳細參考 如何優雅的爲 PWA 註冊 Service Worker

Service Worker 攔截請求

以前說過,Service Worker 是能夠攔截請求的,那麼必定就會存在一個攔截請求的事件fetch。咱們須要在sw.js去監聽這個事件。

self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.open(CACHE_NAME).then(cache => {
      return cache.match(event.request).then(function (response) {

        // 若是 Service Worker 有本身的返回,就直接返回,減小一次 http 請求
        if (response) {
          console.log('cache 緩存', event.request.url, response);
          return response;
        } else {
            
            if (navigator.online) {
            
                return fetch(event.request).then(function(response) {
                    console.log('network', event.request.url, response);
            // 因爲響應是一個JavaScript或者HTML,會認爲這個響應爲一個流,而流是隻能被消費一次的,因此只能被讀一次
            // 第二次就會報錯 參考文章https://jakearchibald.com/2014/reading-responses/
            cache.put(event.request, response.clone());
            return response;
          }).catch(function(error) {
            console.error('請求失敗', error);
            throw error;
          });
          
            } else {
                // 斷網處理
                offlineRequest(fetchRequest);
            }
          
        }
      });
    })
  );
});
複製代碼

這裏咱們在fetch事件中監聽請求事件,咱們經過cache.match來進行請求的比較,若是存再這個請求的響應咱們就直接返回緩存結果,不然就去請求。在這裏咱們經過cache.add來添加新的緩存,他實際上內部是包含了fetch請求過程的(注意:Cache.put, Cache.add和Cache.addAll只能在GET請求下使用)。在match的時候,須要請求的url和header都一致纔是相同的資源,能夠設定第二個參數ignoreVary:true。caches.match(event.request, {ignoreVary: true}) 表示只要請求url相同就認爲是同一個資源。另外須要提到一點,Fetch 請求默認是不附帶 Cookies 等信息的,在請求靜態資源上這沒有問題,並且節省了網絡請求大小。但對於動態頁面,則可能會由於請求缺失 Cookies 而存在問題。此時能夠給 Fetch 請求設置第二個參數。示例:fetch(fetchRequest, { credentials: 'include' } );

Cache API

Cache API 不只在Service Worker中可使用,在主頁面中也可使用。咱們經過 caches.open(cacheName)來打開一個緩存空間,在,默認狀況下,若是咱們不手動去清除這個緩存空間,這個緩存會一直存在,不會過時。在使用Cache API以前,咱們都須要經過caches.open先去打開這個緩存空間,而後在使用相應的Cache方法。這裏有幾個注意點:

  • Cache.put, Cache.add和Cache.addAll只能在GET請求下使用
  • 自Chrome 46版本起,Cache API只保存安全來源的請求,即那些經過HTTPS服務的請求。
  • Cache API不支持HTTP緩存頭

在使用cache.add和cache.addAll的時候,是先根據url獲取到相應的response,而後再添加到緩存中。過程相似於調用 fetch(), 而後使用 Cache.put() 將response添加到cache中

詳細MDN文檔

Fetch API

Fetch API不只能夠在主線程中進行使用,也能夠在Service Worker中進行使用。fetch 和 XMLHttpRequest有兩種方式不一樣:

  • 當接收到一個表明錯誤的 HTTP 狀態碼時,從 fetch()返回的 Promise 不會被標記爲 reject, 即便該 HTTP 響應的狀態碼是 404 或 500。相反,它會將 Promise 狀態標記爲 resolve (可是會將 resolve 的返回值的 ok 屬性設置爲 false ),僅當網絡故障時或請求被阻止時,纔會標記爲 reject。

  • 默認狀況下,fetch 不會從服務端發送或接收任何 cookies, 若是站點依賴於用戶 session,則會致使未經認證的請求(要發送 cookies,必須設置 credentials 選項)

// Example POST method implementation:

postData('http://example.com/answer', {answer: 42})
  .then(data => console.log(data)) // JSON from `response.json()` call
  .catch(error => console.error(error))

function postData(url, data) {
  // Default options are marked with *
  return fetch(url, {
    body: JSON.stringify(data), // must match 'Content-Type' header
    cache: 'no-cache', // *default, no-cache, reload, force-cache, only-if-cached
    credentials: 'same-origin', // include(始終攜帶), same-origin(同源攜帶cookie), omit(始終不攜帶)
    headers: {
      'user-agent': 'Mozilla/4.0 MDN Example',
      'content-type': 'application/json'
    },
    method: 'POST', // *GET, POST, PUT, DELETE, etc.
    mode: 'cors', // no-cors, cors, *same-origin
    redirect: 'follow', // manual, *follow, error
    referrer: 'no-referrer', // *client, no-referrer
  })
  .then(response => response.json()) // parses response to JSON
}

複製代碼

更多信息請查閱:使用 Fetch

Notification

Notification API 用來進行瀏覽器通知,當用戶容許時,瀏覽器就能夠彈出通知。這個API在主頁面和Service Worker中均可以使用,MDN文檔

  • 在主頁面中使用
// 先檢查瀏覽器是否支持
  if (!("Notification" in window)) {
    alert("This browser does not support desktop notification");
  }

  // 檢查用戶是否贊成接受通知
  else if (Notification.permission === "granted") {
    // If it's okay let's create a notification
    new Notification(title, {
      body: desc,
      icon: '/icon.png',
      requireInteraction: true
    });
  }

  // 不然咱們須要向用戶獲取權限
  else if (Notification.permission !== 'denied') {
    Notification.requestPermission(function (permission) {
      // 若是用戶贊成,就能夠向他們發送通知
      if (permission === "granted") {
        new Notification(title, {
          body: desc,
          icon: '/icon.png',
          requireInteraction: true
        });
      } else {
        console.warn('用戶拒絕通知');
      }
    });
  }

複製代碼
  • 在Service Worker中使用
// 發送 Notification 通知
function sendNotify(title, options={}, event) {

  if (Notification.permission !== 'granted') {
    console.log('Not granted Notification permission.');

    // 經過post一個message信號量,來在主頁面中詢問用戶獲取頁面通知權限
    postMessage({
      type: 'applyNotify'
    })
  } else {

    // 在Service Worker 中 觸發一條通知
    self.registration.showNotification(title || 'Hi:', Object.assign({
      body: '這是一個通知示例',
      icon: '/icon.png',
      requireInteraction: true
    }, options));
  }
  
}

複製代碼

咱們能夠看見當咱們在Service Worker中進行消息提示時,用戶可能關閉了消息提示的功能,因此咱們首先要再次詢問用戶是否開啓消息提示的功能,可是在Service Worker中是不可以直接詢問用戶的,咱們必需要在主頁面中去詢問,這個時候咱們能夠經過postMessage去發送一個信號量,根據這個信號量的類型,來作響應的處理(例如:詢問消息提示的權限,DOM操做等等)

function postMessage(data) {
  self.clients.matchAll().then(clientList => {
    clientList.forEach(client => {
      // 當前打開的標籤頁發送消息
      if (client.visibilityState === 'visible') {
        client.postMessage(data);
      }
    })
  })
}
複製代碼

在這裏咱們只向打開的標籤頁發送該信號量,避免重複詢問

message 事件

因爲Service Worker是一個單獨的瀏覽器線程,與JavaScript主線程互不干擾,可是咱們仍是能夠經過postMessage實現通訊,並且能夠經過post特定的消息,從而讓主線程去進行相應的DOM操做,實現間接操做DOM的方式。

  • 頁面發送消息給Service Worker 在頁面上經過 navigator.serviceWorker.controller 得到 ServiceWorker 的句柄。但只有 ServiceWorker 註冊成功後該句柄纔會存在。
function sendMsg(msg) {
    const controller = navigator.serviceWorker.controller;

    if (!controller) {
        return;
    }

    controller.postMessage(msg, []);
}

// 在 serviceWorker 註冊成功後,頁面上便可經過 navigator.serviceWorker.controller 發送消息給它
navigator.serviceWorker
    .register('/test/sw.js', {scope: '/test/'})
    .then(registration => console.log('ServiceWorker 註冊成功!做用域爲: ', registration.scope))
    .then(() => sendMsg('hello sw!'))
    .catch(err => console.log('ServiceWorker 註冊失敗: ', err));
    
複製代碼

在 ServiceWorker 內部,能夠經過監聽 message 事件便可得到消息:

self.addEventListener('message', function(ev) {
    console.log(ev.data);
});
複製代碼
  • Service Worker發送消息給頁面
// self.clients.matchAll方法獲取當前serviceWorker實例所接管的全部標籤頁,注意是當前實例 已經接管的
self.clients.matchAll().then(clientList => {
    clientList.forEach(client => {
        client.postMessage('Hi, I am send from Service worker!');
    })
});
複製代碼

在主頁面中監聽

navigator.serviceWorker.addEventListener('message', event => {
  console.log(event.data);
}); 
複製代碼

Client.postMessage

manifest

3 manifest.json 做用 PWA 添加至桌面的功能實現依賴於 manifest.json,也就是說若是要實現添加至主屏幕這個功能,就必需要有這個文件

{
  "short_name": "短名稱",
  "name": "這是一個完整名稱",
  "icons": [
  {
    "src": "icon.png",
    "type": "image/png",
    "sizes": "144x144"
  }
],
  "start_url": "index.html"
}
複製代碼

<link rel="manifest" href="path-to-manifest/manifest.json">

name —— 網頁顯示給用戶的完整名稱

short_name —— 當空間不足以顯示全名時的網站縮寫名稱

description —— 關於網站的詳細描述

start_url —— 網頁的初始 相對 URL(好比 /)

scope —— 導航範圍。好比,/app/的scope就限制 app 在這個文件夾裏。

background-color —— 啓動屏和瀏覽器的背景顏色

theme_color —— 網站的主題顏色,通常都與背景顏色相同,它能夠影響網站的顯示

orientation —— 首選的顯示方向:any, natural, landscape, landscape-primary, landscape-secondary, portrait, portrait-primary, 和 portrait-secondary。

display —— 首選的顯示方式:fullscreen, standalone(看起來像是native app),minimal-ui(有簡化的瀏覽器控制選項) 和 browser(常規的瀏覽器 tab)

icons —— 定義了 src URL, sizes和type的圖片對象數組。
複製代碼

詳細配置

MDN詳細配置

manifest驗證

相關問題

  • 對於不一樣的資源,咱們可能有不一樣的緩存策略,怎麼方便的去實現這些複雜的場景

使用workbox,若是使用webpack進行項目打包,咱們可使用workbox-webpack-plugin插件

  • 爲何不適用其餘的本地緩存方案

Web Storage(例如 LocalStorage 和 SessionStorage)是同步的,不支持網頁工做線程,並對大小和類型(僅限字符串)進行限制。 Cookie 具備自身的用途,但它們是同步的,缺乏網頁工做線程支持,同時對大小進行限制。WebSQL 不具備普遍的瀏覽器支持,所以不建議使用它。File System API 在 Chrome 之外的任意瀏覽器上都不受支持。目前正在 File and Directory Entries API 和 File API 規範中改進 File API,但該 API 還不夠成熟也未徹底標準化,所以沒法被普遍採用。

同步的問題 就是負擔大,若是有大量請求緩存在本地緩存中,若是是同步,可能負擔重

  • 在將相應存在cache中並返回給瀏覽器報錯

resulted in a network error response: a Response whose "body" is locked cannot be used to respond to a request

這是由於在使用put的時候,是流的一個pipe操做,流是隻能被消費一次的。咱們能夠clone這個response或者reques參考文章

  • 在通過webpack打包後,全部的靜態資源都會帶有hash值,怎麼辦

使用某些webpack插件,例如offline-plugin或者webpack-workbox-plugin

代碼示例

pwa-study

pwa-webpack-study

參考資料

最後(歡迎你們關注我)

DJL簫氏我的博客

博客GitHub地址(歡迎star)

簡書

掘金

我的公衆號

我的公衆號
相關文章
相關標籤/搜索