PWA超簡單入門

DoubleDimos發表於2019-03-04

PWA簡單入門教程

什麼是PWA

MDN官網對PWA有很明確的定義

Progressive web apps (PWAs) take traditional web sites/applications — with all of the advantages the web brings — and add a number of features that give them many of the user experience advantages of native apps. 
複製程式碼

簡單地翻譯過來就是:PWA像傳統的web app一樣,但是它更優秀。

PWA是Google推出的技術,你可以在這裡找到更為詳細的資料。或者前往MDN檢視文件。

PWA準備知識

PWA是全新的內容,它不僅僅是全新的API那麼簡單,更為重要的是,它引入了一系列全新的標準和語法作為基礎。在學習PWA之前,你需要保證你已經熟練使用以下的內容:

  1. ES6標準語法
  2. Promise標準,這是最為重要的知識點,如果你還不熟或者沒聽說過,那麼你得好好思考一下了
  3. fetch,全新的獲取資源的API,它包括Request、Response、Header和Stream
  4. WebWorker,JavaScript解決單執行緒的方案
  5. Cache API(快取API)

PWA的很容易犯錯的地方

PWA線上上部署的時候,請確保是在HTTPS下面,而非HTTP。當然,為了便於開發,瀏覽器支援localhost上面部署。

PWA完成快取後,很多時候你會發現程式碼無法變動,或者沒有按照預期的那樣自動更新worker,這時候不妨在清除快取試試。

PWA並非支援所有瀏覽器,事實上,很少瀏覽器預設支援PWA。這方面Chrome和FireFox做得比較好,因此本文采用Chrome作為開發工具。

一些準備

首先,讓我們先來一些簡單的準備工作。PWA需要一個伺服器,我們使用Koa簡單地搭建一個伺服器。
首先,建立開發目錄,並初始化一些檔案:

mkdir pwa-test
cd pwa-test
touch index.html
touch index.js
touch sw.js

// 伺服器指令碼
touch server.js

// 初始化nodejs專案目錄,一路回車
npm init
複製程式碼

接著安裝一些開發使用的包

npm install koa koa-static --save-dev
//或者
yarn add koa koa-static --dev
複製程式碼

接著,我們在server.js裡面寫入下面這段簡單的程式碼:

const Koa = require(`koa`);
const Static = require(`koa-static`);
const path = require(`path`);  

const app = new Koa();
const staticPath = `./`;

app.use(Static(path.resolve(__dirname, staticPath)));

app.listen(8080);
複製程式碼

然後在index.html裡面寫入簡單的一些內容

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>PWA TEST</title>
</head>
<body>
    <script src="./index.js"></script>
</body>
</html>
複製程式碼

開始PWA之旅

service worker

PWA最重要的一個部分,service worker。它和傳統的Worker相似但又不同。操作Service Worker的方法很簡單,只需要簡單的register一下。我們接下來介紹一下具體使用。

首先是檢測是否支援service worker

// index.js
if (`serviceWorker` in navigator) {
    navigator.serviceWorker.register(`/sw.js`).then(reg => {
        console.log(`service worker registed!`);
    }).reject(err => {
        console.log(`Opooos, something wrong happend!`);
    })
}

window.onload = function() {
    document.body.append(`PWA!`)
}
複製程式碼

這段程式碼使用了Promise,當你register一個檔案的時候,它會返回一個Promise。值得注意的是,register接收的檔案並非相對於當前檔案所在路勁的路勁,而是根路徑的相對路徑。換句話來說,如果你的pwa應用在https://www.example.com/pwa上執行,那麼你register的sw.js並非是./sw.js,而應該是/pwa/sw.js

現在我們已經註冊了一個service Worker,但是它還沒有任何內容。接著我們在sw.js寫入以下程式碼:

// sw.js
self.addEventListener(`install`, function (e) {
    e.waitUntil(
        caches.open(`v1`).then(cache => {
            return cache.addAll([
                `/index.js`,
                `/index.html`,
                `/`
            ]);
        })
    );
});

self.addEventListener(`fetch`, function (event) {
    event.respondWith(
        caches.match(event.request)
        .then(function (response) {
            // 檢測是否已經快取過
            if (response) {
                return response;
            }

            var fetchRequest = event.request.clone();

            return fetch(fetchRequest).then(
                function (response) {
                    // 檢測請求是否有效
                    if (!response || response.status !== 200 || response.type !== `basic`) {
                        return response;
                    }

                    var responseToCache = response.clone();

                    caches.open(`v1`)
                        .then(function (cache) {
                            cache.put(event.request, responseToCache);
                        });

                    return response;
                }
            );
        })
    );
});
複製程式碼

self在Worker裡面相當於Global,這裡我們註冊了兩個事件:installfetch。這兩個事件分別對應的是service Worker安裝以及下載檔案的時候時候呼叫。

首先是install,當你register一個檔案之後,install會被呼叫,它意味著這個檔案要被安裝到service Worker中去了。install事件的event裡面有個waitUntil的函式,它接收一個Promise作為引數。它保證了在傳入的Promise執行完之後才完成安裝。

waitUntil裡面,我們使用了caches。這是一個全域性變數,我們使用open開啟一個快取,我們假設這個快取庫叫做v1,如果沒有,它會自動建立。

caches.open(`v1`)同樣返回一個Promise,它的回撥函式接收一個cache,也就是對應的快取庫。接著,我們使用addAll新增了/index.js/index.html這兩個檔案。記住,cache上的操作應該返回,不然waitUntil接收不到什麼時候完成安裝的指令。

另一個事件fetch是做什麼用的呢?它會’攔截‘網頁的fetch請求。這樣一來,我們就可以攔截網頁的部分或者全部fetch請求,然後看看這些請求所請求的檔案在我們的快取裡有沒有,有的話就直接從快取裡拿,不用下載了。這也是PWA最重要的功能之一。

self.addEventListener(`fetch`, function (event) {
    event.respondWith(
        caches.match(event.request)
        .then(function (response) {

            if (response) {
                return response;
            }

            var fetchRequest = event.request.clone();

            return fetch(fetchRequest).then(
                function (response) {

                    if (!response || response.status !== 200 || response.type !== `basic`) {
                        return response;
                    }

                    var responseToCache = response.clone();

                    caches.open(`v1`)
                        .then(function (cache) {
                            cache.put(event.request, responseToCache);
                        });

                    return response;
                }
            );
        })
    );
});
複製程式碼

首先,當產生fetch請求時,fetch事件被呼叫。fetch事件的event裡面同樣有一個特殊的屬性,那就是request。和nodejs裡面的那個request類似,它代表了一個請求。

我們首先在快取裡查詢這個request以前存過沒有,呼叫了match函式,它返回一個Promise,這個Promise成功時呼叫一個response作為引數的回撥函式。如果在快取裡找到了請求對應的檔案,那麼response不為undefined,那麼直接返回就行了。

但是問題來了,我們怎麼告訴主執行緒:“這個檔案下載過,我的cache裡面有,直接來拿就行了”。event裡面還有個方法,是event.respondWith的方法。它接受一個Promise作為引數,這個Promise的成功回撥應該是一個response

如果沒有找到,那麼就需要從伺服器下載下來,當然,我們同樣也希望把這個檔案加入快取,免得每次都下載。

我們使用fetch而非ajax的重要原因是,fetch天然的支援RequestResponse,並且是使用的Promise。這和service Worker是一套的。

fetch接收一個request作為請求,並且在回撥函式裡面返回這個請求的response

值得注意的是,requestresponse都是流,和nodejs的Stream類似。因此如果我們直接把event.request傳給fetch,那麼request就不能複用了。於是我們複製一個request來請求,這樣就能複用了。當請求成功的時候(fetch的請求成功並不等於是得到了資料,和ajax不同,只有當出現錯誤導致這次請求失敗的時候,才不是成功的。其餘情況,就算請求到的是404,也算成功),我們判斷下response有沒有收到東西,並且是200成功的,另外不能是跨域獲得的(也就是response.type == `basic`)。

好了,現在從伺服器得到了這個響應了,我們只需要把它加入我們的快取裡面就行了。這裡我們呼叫了cache.put來快取這個請求的響應。

最後,一定不要忘記,返回這個response!並且為了複用,返回的應該是clone後的!因為event.respondWith需要告訴主執行緒:“這個請求我們已經拿到了(不管是從快取中拿到的還是從伺服器拿到的),你接受這個響應就行了!”

萬事俱備

我們現在都準備好了,讓我們看看能不能執行。開啟終端,執行伺服器指令碼

nodejs ./server.js
複製程式碼

開啟localhost:8080,不出意外你應該能看見PWA!

現在,我們停止伺服器指令碼,再次重新整理頁面。頁面上應該同樣會顯示PWA!

一些問題

  1. 怎麼更新service Worker呢?

    很簡單,你只需要更改sw.js就行了,它會在下次聯網時主動檢測並進行比對,如果不同,那麼會重新安裝。

  2. 怎麼更新快取後的指令碼或檔案呢?

    這需要你自己手動的檢測了,你可以開發一個檢測更新的介面,然後手動的再次請求並更新部分檔案

  3. 有沒有什麼成功的案例可以借鑑呢?

    有,用chrome開啟Vue中文官網,會提示你加入桌面,同意之後,你就會發現pwa的神奇之處了。

  4. 我的PWA怎麼手機上不支援?

    你需要安裝chrome或者Firefox,並且授予它一些基本的許可權,比如允許建立桌面圖示

  5. 看起來pwa沒有什麼啊,不就是允許離線訪問了嗎?

    No!pwa可不止這些內容哦!pwa甚至可以呼叫一些原生APP才可以呼叫的介面,也可以像原生的APP那樣推送訊息哦!

  6. 太棒了,我怎麼繼續學習呢!

    你有兩個選擇,MDN上有文件,但是簡體中文沒有,只有繁體中文。PWA官網,需要科學上網。