PWA-讓你的web應用變得高大上

前言

對於PWA,在通過屢次被面試官進行靈魂拷問後😭,我對他產生了濃厚的興趣,苦於前段時間筆者忙着面試沒得時間,因而就耽擱到了如今😂。不過對於學習新技術而言,咱們老是須要去懷着一顆敬畏之心去研究的,一項技術的興起老是有着它的意義所在,也必然表明了某種趨勢。說了這麼多,那PWA究竟是什麼呢?javascript

什麼是PWA

PWA(Progressive web apps,漸進式 Web 應用)運用現代的 Web API 以及傳統的漸進式加強策略來建立跨平臺 Web 應用程序。這些應用無處不在、功能豐富,使其具備與原生應用相同的用戶體驗優點css

MDN上的解釋老是很官方的,從字面上來講,咱們能夠知道他是一種漸進式的Web應用,那麼何謂漸進式呢?其實就是表明着若是瀏覽器不支持,那麼對原有應用不會產生影響,對於支持該項技術的瀏覽器,他會在原有基礎上新增它的特性,讓用戶獲得更好的體驗。目前在VueReact腳手架中已經集成了該項技術,一旦你擁有一個web app項目,那麼你的PWA之旅就已經開始了。html

爲何它會這麼火?這就不得不提到它的三大特性了:java

  • 可添加到桌面
  • 離線訪問
  • 後臺通知

對於一個網站來講,怎麼留住用戶就成了咱們必須考慮的一個問題,而對於Web應用而言,被用戶記住的一個比較粗糙的方式莫過於書籤了,可就用戶體驗層次來講,這就沒法與原生應用進行媲美了。對於一個比較大型的項目來講,開發一個原生應用的成本無疑是巨大的。web

因而咱們怎麼讓一個Web應用具有像原生App同樣的桌面添加直接可訪問並具備打開網站的過分效果就成了一種迫切的開發須要,PWA應勢而生。面試

三大特性實現詳解

PWA中有一個必須注意的點,它只支持在https協議和localhost即本地環境下進行使用,也就是你的應用須要被訪問必須具有這個條件。數據庫

桌面添加

其實對於這個功能而言,它的核心在於一個名叫manifest.json的文件,一旦咱們的應用引入了該項配置,它就能被安裝到桌面進行使用。編程

manifest配置

{
    "name": "HackerWeb",//應用名稱
    "short_name": "HackerWeb",//短名稱,用於在桌面顯示
    "start_url": ".",//入口url
    "display": "standalone",//應用的展示模式,通常來講這個模式體驗最優
    "background_color": "#fff",//應用的主題顏色,通常會改變你的上方菜單欄背景顏色
    "description": "A simply readable Hacker News app.",//應用描述
    "icons": [{//在不一樣環境下展示的應用圖標
    "src": "images/touch/homescreen48.png",
    "sizes": "144x144",
    "type": "image/png"
    }]
 }
複製代碼

具體配置的詳情描述能夠參照:Web App Manifestjson

配置好以後咱們只需使用link標籤進行引入就足夠了api

<link rel="manifest" href="manifest.json">
複製代碼

這樣你的應用就已經具有了被安裝到桌面的能力,是否是很簡單😏。

離線訪問

這個描述功能的實現,筆者就開始要準備放大招了🐤,它的一個核心概念能夠用一張圖來描述:

原理圖

其實這項技術的實現就須要藉助咱們的ServiceWorker以及這一個Cache Storage來進行配合實現了。

功能的實現思路就在於ServiceWorker能夠攔截全部請求,並能夠操做Cache Storage進行存取操做,若是用戶斷網,咱們就能夠選擇從緩存中讀取須要的數據,這樣咱們就能實現離線緩存功能了🤒。

ServiceWorker詳解

  • service worker容許web應用在網絡環境比較差或者是離線的環境下依舊可使用
  • service worker能夠極大的提高web app的用戶體驗
  • service worker是一個獨立的 worker 線程,獨立於當前網頁進程,是一種特殊的web worker
  • Web Worker 是臨時的,每次作的事情的結果還不能被持久存下來,若是下次有一樣的複雜操做,還得費時間的從新來一遍
  • 一旦被 install,就永遠存在,除非被手動 unregister
  • 用到的時候能夠直接喚醒,不用的時候自動睡眠
  • 可編程攔截代理請求和返回,緩存文件,緩存的文件能夠被網頁進程取到(包括網絡離線狀態)
  • 離線內容開發者可控
  • 必須在 HTTPS 環境下才能工做
  • 異步實現,內部大都是經過 Promise 實現

具體什麼是webWoker,本文就再也不贅述了,詳細概念能夠參見阮一峯老師這篇博客,Web Worker 使用教程

註冊ServiceWorker

想要使用它,咱們通常會在用戶首次訪問網站的時候進行註冊。爲了避免影響頁面正常的解析和頁面資源的下載,咱們會選擇在onload事件觸發時進行ServiceWorker的註冊,它的註冊很簡單,只須要調用一個Api便可:

window.onload = function() {
  if (navigator.serviceWorker) {
    navigator.serviceWorker
      .register("./sw.js")
      .then(registration => {
        console.log(registration);
      })
      .catch(err => {
        console.log(err);
      });
  }
};
複製代碼

咱們首先會判斷該瀏覽器是否支持ServiceWorker,若是支持就進行註冊,不支持就直接跳過,不會影響頁面。這個註冊方法返回的是一個Promise對象,咱們能夠在then方法中獲取到registration,這個對象包含了一些註冊成功後的信息,若是失敗,咱們能夠在catch方法中進行捕獲。

serviceWorker的生命週期

註冊完咱們的sw.js(文件名自定義)後,咱們就能夠在sw.js文件中來研究它的三個核心生命週期函數了。

  • install - 會在service worker註冊成功的時候觸發,主要用於緩存資源
  • activate - 會在service worker激活的時候觸發,主要用於刪除舊的資源
  • fetch - 攔截頁面全部請求,當有攔截到請求就會觸發(核心),主要用於操做緩存或者讀取網絡資源

install階段

通常在這個階段咱們主要會將須要離線緩存的一些頁面、資源等存入緩存中,以便在無網絡的狀況下能夠繼續訪問網站。

self.addEventListener("install", async e => {
  cacheData(); //調用緩存方法
  await self.skipWaiting(); //跳過等待
  // e.waitUtil(self.skipWaiting()); //另外一種跳過等待方式
});
複製代碼

首先我會調用相應的緩存資源方法,而後後面的self.skipWating方法主要就是用於若是你的sw.js也就是被註冊的文件發生改變就會從新觸發install生命週期函數,可是卻不會當即觸發activite週期,它會等待上一個sw.js銷燬後纔會激活下一個,這個時候咱們新註冊的sw.js並無被激活,因此爲了可以讓新註冊的sw.js能馬上生效,咱們能夠加上這麼一句進行跳過等待。

這個地方爲何會有這麼兩種寫法呢?實際上是由於self.skipWating返回的是一個Promise,是異步的,爲了保證當前周期函數執行完再進入下一個因此咱們須要等待它執行完成,這裏可使用async await來實現,也可使用內置的一個工具方法waitUtil來實現相應功能。

下面咱們來解析一下代碼中cacheData方法:

//緩存方法
const CHACH_NAME = "cache_v2";
async function cacheData() {
  const cache = await caches.open(CHACH_NAME); //打開一個數據庫
  const cacheList = [
    "/",
    "/index.html",
    "/images/logo.png",
    "/manifest.json",
    "/index.css",
    "/setting.js"
  ]; //須要緩存的清單
  await cache.addAll(cacheList); //緩存起來
}
複製代碼

其實在這裏就用上了咱們另外一個須要研究的知識點cache storage了。他其實有點相似於一個數據庫,通常想要使用數據庫,咱們就須要先打開一個數據庫,每一個數據庫都有一個本身的名字,知足了這些條件,咱們就能往cache storage中存入數據了。

cache storage

  • caches api 相似於數據庫的操做:
    • caches.open(cacheName).then(function(cache) {}): 用於打開緩存,返回一個匹配cacheName的cache對象的promise,相似於鏈接數據庫
    • caches.keys() 返回一個promise對象,包括全部的緩存的key(數據庫名)
    • caches.delete(key) 根據key刪除對應的緩存(數據庫)
  • cache對象經常使用方法(單條數據的操做)
    • cache.put(req, res) 把請求當成key,而且把對應的響應存儲起來
    • cache.add(url) 根據url發起請求,而且把響應結果存儲起來
    • cache.addAll(urls) 抓取一個url數組,而且把結果都存儲起來
    • cache.match(req) : 獲取req對應的response

咱們須要先列出咱們須要進行緩存的清單,也就是代碼中的cacheList,調用cache storage中的addAll方法就能將須要緩存的資源存入cache storage中了😀。

activate階段

在這個階段中,咱們通常會作的事情無非就一件事,把舊的資源或cache storage刪除掉。

但因爲serviceWoker在用戶瀏覽器中安裝激活後咱們並不能立馬就生效,通常會須要用戶在刷新頁面後的第二次訪問才能生效,因此咱們會在activate階段中調用一個API,讓咱們可以在第一訪問就能生效,具體代碼以下:

const CHACH_NAME = "cache_v2";//在全局定義了當前數據庫名
self.addEventListener("activate", async e => {
  /**查出數據庫全部庫名,清除舊版本庫 */
  const keys = await caches.keys();
  keys.forEach(key => {
    //若是該數據庫名不是當前定義的名字就進行刪除
    if (key !== CHACH_NAME) {
      caches.delete(key);
    }
  });
  
  /**用於馬上獲取頁面控制權,確保用戶第一次打開瀏覽器就是立馬生效*/
  await self.clients.claim();
});
複製代碼

由於self.clients.claim()返回的也是一個Promise對象,因此咱們也須要等待其執行完成。

fetch階段

這個階段能夠說就是比較核心的生命週期函數了,由於前面兩個主要用於一些初始化的操做,而fetch階段就真正實現離線緩存的中心樞紐,它會攔截全部頁面請求,由於這一特性,咱們就能在無網絡的狀況下將用戶須要請求的資源從緩存中讀取出來返回給用戶。

通常對於處理用戶請求,咱們會有多種策略,下面筆者就講兩種經常使用的:

網絡優先

顧名思義,就是先去網絡上請求,若是請求不到,再去緩存中讀取,具體代碼以下:

self.addEventListener("fetch", async e => {
  const req = e.request;//拿到請求頭
  await e.respondWith(networkFirst(req));//將用戶請求的資源響應給瀏覽器
});

//網絡優先
async function networkFirst(req) {
  /**使用try.catch進行異常捕獲*/
  try {
    const res = await fetch(req);
    return res;
  } catch (error) {
    const cache = await caches.open(CHACH_NAME); //打開一個數據庫
    return await cache.match(req);//讀取緩存
  }
}
複製代碼

首先會使用Fetch向對應網絡地址發起請求,若是請求不到資源就會拋出異常,就能被try catch捕獲,而後進入catch中進行緩存讀取。

緩存優先

先讀取緩存中數據,若是沒有再發起網絡請求。

//緩存優先
async function cachekFirst(req) {
  const cache = await caches.open(CHACH_NAME); //打開一個數據庫
  let res = await cache.match(req);//讀取緩存
  if (res) {
    return res;
  } else {
    res = await fetch(req);
    return res;
  }
}
複製代碼

具體代碼含義就很少加贅述了,能看到這一步應該對你沒什麼問題了吧😜。

拿到對應資源以後,咱們就只須要調用e.respondWith方法就能把返回值響應給瀏覽器進行渲染了,至此咱們已經完成了一大步,最後就是怎麼進行系統通知了。

Notification

這個處理部分就不能放在sw.js文件中了,由於咱們須要用到window中的Notification函數。

//先獲取通知權限
if (Notification.permission == "default") {
  Notification.requestPermission();
}
if (!navigator.onLine) {
  new Notification("提示", { body: "您已斷線,如今訪問的是緩存內容" });
}
複製代碼

對於這種系統級別的api,第一步天然就是獲取用戶權限,而後才能進行下一步操做。在這裏筆者就只寫了一個通知用戶已經離線的功能。

最後

筆者的文件目錄:

  • images
    • logo.png
  • index.css
  • index.html
  • manifest.json
  • sw.js
  • setting.js
  • server.js

index.html文件中只須要用link標籤引入manifest.jsonsetting.jssetting.js中內容以下:

window.onload = function() {
  if (this.navigator.serviceWorker) {
    this.navigator.serviceWorker
      .register("./sw.js")
      .then(registration => {
        console.log(registration);
      })
      .catch(err => {
        console.log(err);
      });
  }
};

/** * 判斷用戶是否聯網,並給與通知提示 */
//先獲取通知權限
if (Notification.permission == "default") {
  Notification.requestPermission();
}
if (!navigator.onLine) {
  new Notification("提示", { body: "您已斷線,如今訪問的是緩存內容" });
}

複製代碼

洋洋灑灑也寫了3k多字,但願可以對你們有所幫助,同時也歡迎你們對錶述不正確的地方加以指正🧐。

相關文章
相關標籤/搜索