[轉(zhuǎn)]講講PWA

聲明

本人也在不斷的學習和積累中,文章中有不足和誤導的地方還請見諒抒巢,可以給我留言指正胎撇。希望和大家共同進步介粘,共建和諧學習環(huán)境。

原文地址

一晚树、背景

大家都知道Native app體驗確實很好姻采,下載到手機上之后入口也方便。它也有一些缺點:

  • 開發(fā)成本高(ios和安卓)
  • 軟件上線需要審核
  • 版本更新需要將新版本上傳到不同的應(yīng)用商店
  • 想使用一個app就必須去下載才能使用爵憎,即使是偶爾需要使用一下下

而web網(wǎng)頁開發(fā)成本低慨亲,網(wǎng)站更新時上傳最新的資源到服務(wù)器即可,用手機帶的瀏覽器打開就可以使用宝鼓。但是除了體驗上比Native app還是差一些刑棵,還有一些明顯的缺點

  • 手機桌面入口不夠便捷,想要進入一個頁面必須要記住它的url或者加入書簽
  • 沒網(wǎng)絡(luò)就沒響應(yīng)愚铡,不具備離線能力
  • 不像APP一樣能進行消息推送

那么什么是PWA呢蛉签?

二胡陪、What's PWA?

PWA全稱Progressive Web App,即漸進式WEB應(yīng)用碍舍。

一個 PWA 應(yīng)用首先是一個網(wǎng)頁, 可以通過 Web 技術(shù)編寫出一個網(wǎng)頁應(yīng)用. 隨后添加上 App Manifest 和 Service Worker 來實現(xiàn) PWA 的安裝和離線等功能
解決了哪些問題柠座?

  • 可以添加至主屏幕,點擊主屏幕圖標可以實現(xiàn)啟動動畫以及隱藏地址欄
  • 實現(xiàn)離線緩存功能片橡,即使用戶手機沒有網(wǎng)絡(luò)妈经,依然可以使用一些離線功能
  • 實現(xiàn)了消息推送

它解決了上述提到的問題,這些特性將使得 Web 應(yīng)用漸進式接近原生 App捧书。

三吹泡、PWA的實現(xiàn)

3.1 Manifest實現(xiàn)添加至主屏幕

index.html

<head>
  <title>Minimal PWA</title>
  <meta name="viewport" content="width=device-width, user-scalable=no" />
  <link rel="manifest" href="manifest.json" />
  <link rel="stylesheet" type="text/css" href="main.css">
  <link rel="icon" href="/e.png" type="image/png" />
</head>

manifest.json

{
  // 復制后需要把注釋刪除掉
  "name": "Minimal PWA", // 必填 顯示的插件名稱
  "short_name": "PWA Demo", // 可選  在APP launcher和新的tab頁顯示,如果沒有設(shè)置经瓷,則使用name
  "description": "The app that helps you understand PWA", //用于描述應(yīng)用
  "display": "standalone", // 定義開發(fā)人員對Web應(yīng)用程序的首選顯示模式爆哑。standalone模式會有單獨的
  "start_url": "/", // 應(yīng)用啟動時的url
  "theme_color": "#313131", // 桌面圖標的背景色
  "background_color": "#313131", // 為web應(yīng)用程序預定義的背景顏色。在啟動web應(yīng)用程序和加載應(yīng)用程序的內(nèi)容之間創(chuàng)建了一個平滑的過渡了嚎。
  "icons": [ // 桌面圖標泪漂,是一個數(shù)組
    {
    "src": "icon/lowres.webp",
    "sizes": "48x48",  // 以空格分隔的圖片尺寸
    "type": "image/webp"  // 幫助userAgent快速排除不支持的類型
  },
  {
    "src": "icon/lowres",
    "sizes": "48x48"
  },
  {
    "src": "icon/hd_hi.ico",
    "sizes": "72x72 96x96 128x128 256x256"
  },
  {
    "src": "icon/hd_hi.svg",
    "sizes": "72x72"
  }
  ]
}

Manifest參考文檔:https://developer.mozilla.org/zh-CN/docs/Web/Manifest

可以打開網(wǎng)站https://developers.google.cn/web/showcase/2015/chrome-dev-summit查看添加至主屏幕的動圖。

如果用的是安卓手機歪泳,可以下載chrome瀏覽器自己操作看看

3.2 service worker實現(xiàn)離線緩存

3.2.1 什么是service worker

Service Worker 是 Chrome 團隊提出和力推的一個 WEB API萝勤,用于給 web 應(yīng)用提供高級的可持續(xù)的后臺處理能力。

image.png

Service Workers 就像介于服務(wù)器和網(wǎng)頁之間的攔截器呐伞,能夠攔截進出的HTTP 請求敌卓,從而完全控制你的網(wǎng)站。

最主要的特點

  • 在頁面中注冊并安裝成功后伶氢,運行于瀏覽器后臺趟径,不受頁面刷新的影響,可以監(jiān)聽和截攔作用域范圍內(nèi)所有頁面的 HTTP 請求癣防。
  • 網(wǎng)站必須使用 HTTPS蜗巧。除了使用本地開發(fā)環(huán)境調(diào)試時(如域名使用 localhost)
  • 運行于瀏覽器后臺,可以控制打開的作用域范圍下所有的頁面請求
  • 單獨的作用域范圍蕾盯,單獨的運行環(huán)境和執(zhí)行線程
  • 不能操作頁面 DOM幕屹。但可以通過事件機制來處理
  • 事件驅(qū)動型服務(wù)線程

為什么要求網(wǎng)站必須是HTTPS的,大概是因為service worker權(quán)限太大能攔截所有頁面的請求吧级遭,如果http的網(wǎng)站安裝service worker很容易被攻擊

瀏覽器支持情況

image.png

生命周期

image.png

當用戶首次導航至 URL 時望拖,服務(wù)器會返回響應(yīng)的網(wǎng)頁。

  • 第1步:當你調(diào)用 register() 函數(shù)時挫鸽, Service Worker 開始下載说敏。
  • 第2步:在注冊過程中,瀏覽器會下載丢郊、解析并執(zhí)行 Service Worker ()盔沫。如果在此步驟中出現(xiàn)任何錯誤医咨,register() 返回的 promise 都會執(zhí)行 reject 操作,并且 Service Worker 會被廢棄架诞。
  • 第3步:一旦 Service Worker 成功執(zhí)行了腋逆,install 事件就會激活
  • 第4步:安裝完成,Service Worker 便會激活侈贷,并控制在其范圍內(nèi)的一切。如果生命周期中的所有事件都成功了等脂,Service Worker 便已準備就緒俏蛮,隨時可以使用了!

chrome://serviceworker-internals 來了解當前瀏覽器中所有已安裝Service Worker的詳細情況

3.2.2 HTTP緩存與service worker緩存

  • HTTP緩存

Web 服務(wù)器可以使用 Expires 首部來通知 Web 客戶端上遥,它可以使用資源的當前副本搏屑,直到指定的“過期時間”。反過來粉楚,瀏覽器可以緩存此資源辣恋,并且只有在有效期滿后才會再次檢查新版本。
使用 HTTP 緩存意味著你要依賴服務(wù)器來告訴你何時緩存資源和何時過期模软。

  • service worker緩存

Service Workers 的強大在于它們攔截 HTTP 請求的能力
進入任何傳入的 HTTP 請求伟骨,并決定想要如何響應(yīng)。在你的 Service Worker 中燃异,可以編寫邏輯來決定想要緩存的資源携狭,以及需要滿足什么條件和資源需要緩存多久。一切盡歸你掌控回俐!

3.2.3 實現(xiàn)離線緩存

index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Hello Caching World!</title>
  </head>
  <body>
    <!-- Image -->
    <img src="/images/hello.png" />                 
    <!-- JavaScript -->
    <script async src="/js/script.js"></script>     
    <script>
      // 注冊 service worker
      if ('serviceWorker' in navigator) {           
        navigator.serviceWorker.register('/service-worker.js', {scope: '/'}).then(function (registration) {
          // 注冊成功
          console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function (err) {                   
          // 注冊失敗 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

注:Service Worker 的注冊路徑?jīng)Q定了其 scope 默認作用頁面的范圍逛腿。
如果 service-worker.js 是在 /sw/ 頁面路徑下,這使得該 Service Worker 默認只會收到 頁面/sw/ 路徑下的 fetch 事件仅颇。
如果存放在網(wǎng)站的根路徑下单默,則將會收到該網(wǎng)站的所有 fetch 事件。
如果希望改變它的作用域忘瓦,可在第二個參數(shù)設(shè)置 scope 范圍搁廓。示例中將其改為了根目錄,即對整個站點生效政冻。

service-worker.js

var cacheName = 'helloWorld';     // 緩存的名稱  
// install 事件枚抵,它發(fā)生在瀏覽器安裝并注冊 Service Worker 時        
self.addEventListener('install', event => { 
/* event.waitUtil 用于在安裝成功之前執(zhí)行一些預裝邏輯
 但是建議只做一些輕量級和非常重要資源的緩存,減少安裝失敗的概率
 安裝成功后 ServiceWorker 狀態(tài)會從 installing 變?yōu)?installed */
  event.waitUntil(
    caches.open(cacheName)                  
    .then(cache => cache.addAll([    // 如果所有的文件都成功緩存了明场,便會安裝完成汽摹。如果任何文件下載失敗了,那么安裝過程也會隨之失敗苦锨。        
      '/js/script.js',
      '/images/hello.png'
    ]))
  );
});
  
/**
為 fetch 事件添加一個事件監(jiān)聽器逼泣。接下來趴泌,使用 caches.match() 函數(shù)來檢查傳入的請求 URL 是否匹配當前緩存中存在的任何內(nèi)容。如果存在的話拉庶,返回緩存的資源嗜憔。
如果資源并不存在于緩存當中,通過網(wǎng)絡(luò)來獲取資源氏仗,并將獲取到的資源添加到緩存中吉捶。
*/
self.addEventListener('fetch', function (event) {
  event.respondWith(
    caches.match(event.request)                  
    .then(function (response) {
      if (response) {                            
        return response;                         
      }
      var requestToCache = event.request.clone();  //          
      return fetch(requestToCache).then(                   
        function (response) {
          if (!response || response.status !== 200) {      
            return response;
          }
          var responseToCache = response.clone();          
          caches.open(cacheName)                           
            .then(function (cache) {
              cache.put(requestToCache, responseToCache);  
            });
          return response;             
    })
  );
});

注:為什么用request.clone()和response.clone()
需要這么做是因為request和response是一個流,它只能消耗一次皆尔。因為我們已經(jīng)通過緩存消耗了一次呐舔,然后發(fā)起 HTTP 請求還要再消耗一次,所以我們需要在此時克隆請求
Clone the request—a request is a stream and can only be consumed once.

3.2.4 調(diào)試相關(guān)

chrome瀏覽器打開https://googlechrome.github.io/samples/service-worker/basic/index.html慷蠕,這是一個實現(xiàn)了service worker離線緩存功能的網(wǎng)站珊拼,打開調(diào)試工具

image.png

介紹一個圖中的1.和2.

  1. 勾選可以模擬網(wǎng)站離線情況,勾選后network會有一個黃色警告圖標流炕,該網(wǎng)站已經(jīng)離線澎现。此時刷新頁面,頁面仍然能夠正常顯示
  2. 當前service worker的scope每辟。它能夠攔截https://googlechrome.github.i...下的請求剑辫,同樣也能夠攔截https://googlechrome.github.i.../.html下的請求

調(diào)試面板具體代表的什么參看https://x5.tencent.com/tbs/guide/serviceworker.html的第三部分

3.3 serice worker實現(xiàn)消息推送

image.png
  • 步驟一、提示用戶并獲得他們的訂閱詳細信息
  • 步驟二渠欺、將這些詳細信息保存在服務(wù)器上
  • 步驟三揭斧、在需要時發(fā)送任何消息

不同瀏覽器需要用不同的推送消息服務(wù)器。以 Chrome 上使用 Google Cloud Messaging<GCM> 作為推送服務(wù)為例峻堰,第一步是注冊 applicationServerKey(通過 GCM 注冊獲取)讹开,并在頁面上進行訂閱或發(fā)起訂閱。每一個會話會有一個獨立的端點(endpoint)捐名,訂閱對象的屬性(PushSubscription.endpoint) 即為端點值旦万。將端點發(fā)送給服務(wù)器后,服務(wù)器用這一值來發(fā)送消息給會話的激活的 Service Worker (通過 GCM 與瀏覽器客戶端溝通)镶蹋。

步驟一和步驟二
index.html

<!DOCTYPE html>
<html>
  <head>
    <meta charset="UTF-8">
    <title>Progressive Times</title>
    <link rel="manifest" href="/manifest.json">                                      
  </head>
  <body>
    <script>
      var endpoint;
      var key;
      var authSecret;
      var vapidPublicKey = 'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY';
      // 方法很復雜成艘,但是可以不用具體看,知識用來轉(zhuǎn)化vapidPublicKey用
      function urlBase64ToUint8Array(base64String) {                                  
        const padding = '='.repeat((4 - base64String.length % 4) % 4);
        const base64 = (base64String + padding)
          .replace(/\-/g, '+')
          .replace(/_/g, '/');
        const rawData = window.atob(base64);
        const outputArray = new Uint8Array(rawData.length);
        for (let i = 0; i < rawData.length; ++i) {
          outputArray[i] = rawData.charCodeAt(i);
        }
        return outputArray;
      }
      if ('serviceWorker' in navigator) {
        navigator.serviceWorker.register('sw.js').then(function (registration) {
          return registration.pushManager.getSubscription()                            
            .then(function (subscription) {
              if (subscription) {                                                      
                return;
              }
              return registration.pushManager.subscribe({                              
                  userVisibleOnly: true,
                  applicationServerKey: urlBase64ToUint8Array(vapidPublicKey)
                })
                .then(function (subscription) {
                  var rawKey = subscription.getKey ? subscription.getKey('p256dh') : '';
                  key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
                  var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                  authSecret = rawAuthSecret ?
                    btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
                  endpoint = subscription.endpoint;
                  return fetch('./register', {                                         
                    method: 'post',
                    headers: new Headers({
                      'content-type': 'application/json'
                    }),
                    body: JSON.stringify({
                      endpoint: subscription.endpoint,
                      key: key,
                      authSecret: authSecret,
                    }),
                  });
                });
            });
        }).catch(function (err) {
          // 注冊失敗 :(
          console.log('ServiceWorker registration failed: ', err);
        });
      }
    </script>
  </body>
</html>

步驟三 服務(wù)器發(fā)送消息給service worker

app.js

const webpush = require('web-push');                 
const express = require('express');
var bodyParser = require('body-parser');
const app = express();
webpush.setVapidDetails(                             
  'mailto:contact@deanhume.com',
  'BAyb_WgaR0L0pODaR7wWkxJi__tWbM1MPBymyRDFEGjtDCWeRYS9EF7yGoCHLdHJi6hikYdg4MuYaK0XoD0qnoY',
  'p6YVD7t8HkABoez1CvVJ5bl7BnEdKUu5bSyVjyxMBh0'
);
app.post('/register', function (req, res) {           
  var endpoint = req.body.endpoint;
  saveRegistrationDetails(endpoint, key, authSecret); 
  const pushSubscription = {                          
    endpoint: req.body.endpoint,
    keys: {
      auth: req.body.authSecret,
      p256dh: req.body.key
    }
  };
  var body = 'Thank you for registering';
  var iconUrl = 'https://example.com/images/homescreen.png';
  // 發(fā)送 Web 推送消息
  webpush.sendNotification(pushSubscription,          
      JSON.stringify({
        msg: body,
        url: 'http://localhost:3111/',
        icon: iconUrl
      }))
    .then(result => res.sendStatus(201))
    .catch(err => {
      console.log(err);
    });
});
app.listen(3111, function () {
  console.log('Web push app listening on port 3111!')
});

service worker監(jiān)聽push事件贺归,將通知詳情推送給用戶

service-worker.js

self.addEventListener('push', function (event) {
 // 檢查服務(wù)端是否發(fā)來了任何有效載荷數(shù)據(jù)
  var payload = event.data ? JSON.parse(event.data.text()) : 'no payload';
  var title = 'Progressive Times';
  event.waitUntil(
    // 使用提供的信息來顯示 Web 推送通知
    self.registration.showNotification(title, {                           
      body: payload.msg,
      url: payload.url,
      icon: payload.icon
    })
  );
});

擴展知識:service worker的更新

總結(jié)

PWA的優(yōu)勢

  • 可以將app的快捷方式放置到桌面上淆两,全屏運行,與原生app無異

  • 能夠在各種網(wǎng)絡(luò)環(huán)境下使用拂酣,包括網(wǎng)絡(luò)差和斷網(wǎng)條件下秋冰,不會顯示undefind

  • 推送消息的能力

  • 其本質(zhì)是一個網(wǎng)頁,沒有原生app的各種啟動條件婶熬,快速響應(yīng)用戶指令

    PWA存在的問題

  • 支持率不高:現(xiàn)在ios手機端不支持pwa剑勾,IE也暫時不支持

  • Chrome在中國桌面版占有率還是不錯的埃撵,安卓移動端上的占有率卻很低

  • 各大廠商還未明確支持pwa

  • 依賴的GCM服務(wù)在國內(nèi)無法使用

  • 微信小程序的競爭

盡管有上述的一些缺點,PWA技術(shù)仍然有很多可以使用的點虽另。

  • service worker技術(shù)實現(xiàn)離線緩存暂刘,可以將一些不經(jīng)常更改的靜態(tài)文件放到緩存中,提升用戶體驗捂刺。
  • service worker實現(xiàn)消息推送谣拣,使用瀏覽器推送功能,吸引用戶
  • 漸進式開發(fā)族展,盡管一些瀏覽器暫時不支持芝发,可以利用上述技術(shù)給使用支持瀏覽器的用戶帶來更好的體驗。

參考文檔

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末苛谷,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子格郁,更是在濱河造成了極大的恐慌腹殿,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,734評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件例书,死亡現(xiàn)場離奇詭異锣尉,居然都是意外死亡,警方通過查閱死者的電腦和手機决采,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,931評論 3 394
  • 文/潘曉璐 我一進店門自沧,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人树瞭,你說我怎么就攤上這事拇厢。” “怎么了晒喷?”我有些...
    開封第一講書人閱讀 164,133評論 0 354
  • 文/不壞的土叔 我叫張陵孝偎,是天一觀的道長。 經(jīng)常有香客問我凉敲,道長衣盾,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,532評論 1 293
  • 正文 為了忘掉前任爷抓,我火速辦了婚禮势决,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘蓝撇。我一直安慰自己果复,他們只是感情好,可當我...
    茶點故事閱讀 67,585評論 6 392
  • 文/花漫 我一把揭開白布渤昌。 她就那樣靜靜地躺著据悔,像睡著了一般传透。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上极颓,一...
    開封第一講書人閱讀 51,462評論 1 302
  • 那天朱盐,我揣著相機與錄音,去河邊找鬼菠隆。 笑死兵琳,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的骇径。 我是一名探鬼主播躯肌,決...
    沈念sama閱讀 40,262評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼破衔!你這毒婦竟也來了清女?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,153評論 0 276
  • 序言:老撾萬榮一對情侶失蹤晰筛,失蹤者是張志新(化名)和其女友劉穎嫡丙,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體读第,經(jīng)...
    沈念sama閱讀 45,587評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡曙博,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,792評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了怜瞒。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片父泳。...
    茶點故事閱讀 39,919評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖吴汪,靈堂內(nèi)的尸體忽然破棺而出惠窄,到底是詐尸還是另有隱情,我是刑警寧澤漾橙,帶...
    沈念sama閱讀 35,635評論 5 345
  • 正文 年R本政府宣布睬捶,位于F島的核電站,受9級特大地震影響近刘,放射性物質(zhì)發(fā)生泄漏擒贸。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,237評論 3 329
  • 文/蒙蒙 一觉渴、第九天 我趴在偏房一處隱蔽的房頂上張望介劫。 院中可真熱鬧,春花似錦案淋、人聲如沸座韵。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,855評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽誉碴。三九已至宦棺,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間黔帕,已是汗流浹背代咸。 一陣腳步聲響...
    開封第一講書人閱讀 32,983評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留成黄,地道東北人呐芥。 一個月前我還...
    沈念sama閱讀 48,048評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像奋岁,于是被迫代替她去往敵國和親思瘟。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,864評論 2 354

推薦閱讀更多精彩內(nèi)容