PWA學習筆記

PWA(Progressive Web Apps)學習筆記

什麼是PWA

PWA = 普通的網站 + manifest + Service Workersjavascript

manifest文件包含網站相關的信息,包括圖標,背景屏幕,顏色和默認方向。css

Service Workers爲網站提供了更好的體驗(漸進加強),容許將網站添加到設備的主屏幕,離線緩存。html

PWA應該具有的特性:java

  • 響應式的 - 它適應較小的屏幕尺寸
  • 鏈接無關 - 因爲 Service Worker 緩存,它能夠離線工做
  • 應用式的交互 - 它使用應用外殼架構進行構建
  • 始終保持最新 - 感謝 Service Worker 的更新過程
  • 安全的 - 它經過 HTTPS 進行工做
  • 可發現的 - 搜索引擎能夠找到它
  • 可安裝的 - 使用清單文件
  • 可連接的 - 能夠簡單的經過 URL 來共享

何爲Service Workers

Service Workers由JavaScript編寫,運行在瀏覽器後臺,基於事件驅動。若是用戶瀏覽器不支持Service Workers的話,並不會形成影響,網站還能夠做爲普通網站進行瀏覽,所以作到了「漸進加強」。git

經過Service Workers,能夠緩存 UI 外殼(用戶界面所必需的最小化的 HTML、CSS 和 JavaScript),動態內容在UI外殼加載後再加載,爲用戶提供相似原生app的體驗。github

  • Service Workers運行在本身的全局腳本上下文中
  • 不綁定到具體的網頁
  • 沒法修改網頁中的元素,由於它沒法訪問 DOM
  • 只能使用 HTTPS(localhost本地開發除外)
  • Service Workser運行在不一樣的線程中,不會被阻塞

Service Workers生命週期

從生命週期圖中能夠看出,當第一次加載頁面時,並不會有激活的 Service Worker 來控制頁面。只有當 Service Worker 安裝完成而且用戶刷新了頁面或跳轉至網站的其餘頁面,Service Worker 纔會激活並開始攔截請求。
若是須要在第一次加載時,就但願Service Workers激活並開始攔截請求,能夠經過以下方式當即激活Service Workers。web

self.addEventListener('install', function(event) {
  //使 Service Worker 解僱當前活動的worker, 而且一旦進入等待階段就會激活自身,觸發activate事件
  event.waitUntil(self.skipWaiting());
});

結合self.clients.claim() 一塊兒使用,以確保底層 Service Worker 的更新當即生效。npm

self.addEventListener('activate', function(event) {
  e.waitUntil(
        caches.keys().then(function(keyList) {
            return Promise.all(keyList.map(function(key) {
                if (key !== cacheName) {
                    console.log('[ServiceWorker] Removing old cache', key);
                    return caches.delete(key);
                }
            }));
        })
    );
    return self.clients.claim(); //確保底層 Service Worker 的更新當即生效
});

Service Workers緩存

var cacheKey = "first-pwa";  //緩存的key,能夠添加多個不一樣的緩存

var cacheList = [   //須要緩存的文件列表
    '/',
    'index.html',
    'icon.png',
    'main.css'
];

//在安裝過程當中緩存已知的資源
self.addEventListener('install', event => {  //監聽install事件
    event.waitUntil(  //install完成後
        caches.open(cacheKey)  //打開cache
            .then(cache => cache.addAll(cacheList))  //將須要緩存的文件加入cache列表
            .then(() => self.skipWaiting())  //使 Service Worker 解僱當前活動的worker,
                                            // 而且一旦進入等待階段就會激活自身,觸發activate事件
                                            //無需等待用戶跳轉或刷新頁面
    );
});


//攔截fetch請求
self.addEventListener('fetch', event => {
    event.respondWith(
        caches.match(event.request).then(response => { //若是請求的資源在緩存中
            if (response != null) return response;  //返回緩存資源

            //經過網絡獲取資源,並緩存
            var requestToCache = event.request.clone(); //克隆當前請求
            return fetch(requestToCache.url).then(response => {
                if (!response || response.status !== 200) {
                    return response;  //返回錯誤的響應
                }
                var responseToCache = response.clone(); //克隆響應
                caches.open(cacheKey)
                    .then(cache => {
                        cache.put(requestToCache, responseToCache);  //將響應添加到緩存中
                    });
                return response;  //返回響應
            });
        })
    );
});

屬於Service Workers做用域範圍內的全部http請求都將觸發fetch事件,包括html、css、js、圖片等。json

攔截包含save-data的http請求頭部的實例

若是用戶在瀏覽器中啓用了節省數據的功能,瀏覽器在每一個http請求頭部中會加入save-data請求頭。api

this.addEventListener('fetch', function (event) {
 
  if(event.request.headers.get('save-data')){
    // 咱們想要節省數據,因此限制了圖標和字體
    if (event.request.url.includes('fonts.googleapis.com')) {
        // 不返回任何內容
        event.respondWith(new Promise(resolve => resolve(new Response('', {
            status: 417,
            statusText: 'Ignore fonts to save data.'
            })))
        );
    }
  }
});

如何保證Service Workers能獲取到最新的文件

  • 更新存儲緩存的名稱。
  • 緩存破壞,每次發佈時更新文件的名稱,如增長一個版本號等。

Web應用清單(mainifest.json)

mainifest.json須要在網頁head標籤中引用

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Lato">
    <link rel="stylesheet" href="main.css">
    <link rel="manifest" href="manifest.json"/>
    <title>PWA</title>
</head>
<body>
<h1>Hello PWA!</h1>
<script type="text/javascript">
    if (navigator.serviceWorker != null) {
        navigator.serviceWorker.register('sw.js').then(registration => {
            console.log('ServiceWorker registration successful with scope: ', registration.scope);
        }).catch(function (err) {
            console.log('ServiceWorker registration failed: ', err);
        });
    } else {
        //serviceWorker is not supported
    }
</script>
</body>
</html>

manifest.json中包含的字段主要包括:

{
  "name": "First PWA",
  "short_name": "pwa",
  "display": "standalone",
  "start_url": "/index.html",
  "theme_color": "#FFDF00",
  "background_color": "#FFDF00",
  "orientation": "landscape",
  "scope": "/",
  "icons": [
    {
      "src": "icon.png",
      "sizes": "144x144",
      "type": "image/png"
    }
  ]
}
  • name:當用戶被提示安裝應用時出現的文本。
  • short_name:當應用安裝後出如今用戶主屏幕上的文本。
  • display:顯示模式,默認爲browser。包括fullscreen、standalone、minimal-ui 或 browser 。

    • fullscreen:應用佔用整個可用的顯示區域。
    • standalone:應用以看起來像一個獨立的原生應用。此模式下,用戶代理將排除諸如 URL 欄等標準瀏覽器 UI 元素,但能夠包括諸如狀態欄和系統返回按鈕的其餘系統 UI 元素。
    • minimal-ui:此模式相似於 fullscreen,但爲終端用戶提供了可訪問的最小 UI 元素集合,例如,後退按鈕、前進按鈕、重載按鈕以及查看網頁地址的一些方式。
    • browser:使用操做系統內置的標準瀏覽器來打開 Web 應用。
  • start_url:應用啓動時的第一個頁面。
  • theme_color:能夠對瀏覽器的地址欄進行着色,以符合網站的主色調。
  • background_color:啓動時的背景色。
  • orientation: 屏幕方向。
  • icons:當應用被添加到設備主屏幕時所顯示的圖標。

參考 https://developer.mozilla.org/en-US/docs/Web/Manifest

監聽添加到主屏幕事件

//監聽添加到主屏幕事件
    window.addEventListener('beforeinstallprompt', function (event) {
        // //取消添加
        // e.preventDefault();
        // return false;

        event.userChoice.then(function (result) {
            console.log(result.outcome);
            if (result.outcome == 'dismissed') {

            } else {

            }
        });
    });

推送通知

目前FireFox、Chrome、Edge 已經支持 Push API。推送的過程主要分爲三個步驟:

  • 客戶端訂閱
  • 發送須要推送的消息到push service
  • push service推送到客戶端

客戶端訂閱

在訂閱前,須要先生成VAPID, VAPID是「自主應用服務器標識」 ( Voluntary Application Server Identification ) 的簡稱。它是一個規範,定義了應用服務器和推送服務之間的握手。

1.客戶端訂閱消息,此時瀏覽器會詢問用戶是否容許消息推送通知。
2.從瀏覽器獲取PushSubscription對象,其中包含了客戶端的信息,能夠理解爲標示設備的id。

var vapidPublicKey = 'BF0eSi4ANvVKr017Gr_Xzb-bN9l8-c3qRUHqVU6C-vFy_i3xgrKDY-13BPF5BVx93IVObJwnwrt5vjX-ltM6Uuo';

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;
    }

    function subscribeForPushNotification(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') : '';
                        var key = rawKey ? btoa(String.fromCharCode.apply(null, new Uint8Array(rawKey))) : '';
                        var rawAuthSecret = subscription.getKey ? subscription.getKey('auth') : '';
                        var authSecret = rawAuthSecret ?
                            btoa(String.fromCharCode.apply(null, new Uint8Array(rawAuthSecret))) : '';
                        var endpoint = subscription.endpoint;
                        return fetch('http://localhost:3001/api/register', {
                            method: 'post',
                            headers: new Headers({
                                'content-type': 'application/json'
                            }),
                            body: JSON.stringify({
                                endpoint: subscription.endpoint,
                                key: key,
                                authSecret: authSecret,
                            }),
                        });
                    });
            });
    }

3.將PushSubscription發送到服務端保存。

服務端示例:

this.post('/register', 'register', async (req, res, next) => {
            try {
                let {endpoint, authSecret, key} = req.body;
                let subscriber = {
                    endpoint,
                    keys: {
                        auth: authSecret,
                        p256dh: key
                    }
                };
                subscribers.push(subscriber);
                res.apiSuccess({});
            }
            catch (err) {
                next(err);
            }
        });

發送消息到push service

經過Web Push協議將須要推送的消息發送到push service。

使用web-push的發送示例:

this.post('/send', 'send', async (req, res, next) => {
            try {
                let message = req.body.message;
                for (let subscriber of subscribers) {
                    webpush.sendNotification(
                        subscriber,
                        JSON.stringify({
                            msg:message,
                            url:'http://localhost:3001',
                            icon:''
                        })
                    );
                }
                res.apiSuccess({});
            }
            catch (err) {
                next(err);
            }
        });

push service推送到客戶端

當push service收到消息後,會將消息保存起來,直到目標設備上線後將消息推送到客戶端,或者消息超時再也不發送。

Servicer Worker toolbox

sw-toolbox

一些工具

參考

https://github.com/SangKa/PWA-Book-CN

https://developers.google.com/web/fundamentals/push-notifications/how-push-works

https://codelabs.developers.google.com/codelabs/your-first-pwapp/#0

相關文章
相關標籤/搜索