【PWA學習與實踐】(9)生產環境中PWA實踐的問題與解決方案

《PWA學習與實踐》系列文章已整理至 gitbook - PWA學習手冊,文字內容已同步至 learning-pwa-ebook。轉載請註明做者與出處。

本文是《PWA學習與實踐》系列的第九篇文章。javascript

PWA做爲時下最火熱的技術概念之一,對提高Web應用的安全、性能和體驗有着很大的意義,很是值得咱們去了解與學習。對PWA感興趣的朋友歡迎關注《PWA學習與實踐》系列文章。css

引言

在前八篇文章中,我已經介紹了一些PWA中的常見技術與使用方式。雖然咱們已經學習了不少相關知識,可是,仍是有不少問題在實踐時纔會暴露出來。這篇文章是一篇TroubleShooting,總結了我近期在PWA實踐過程當中遇到了一些問題,以及這些問題的解決方案。但願能幫助一些遇到相似問題的朋友。前端

1. Service Worker Scope

注意Service Worker註冊時的做用範圍(scope)

1.1. 遇到的問題

我在頁面/home下注冊了Service Worker:java

navigator.serviceWorker.register('/static/home/js/sw.js')

經過在.then()中調用console.log()能夠發現Service Worker其實註冊成功了,可是在頁面中卻不生效。這是爲何呢?android

1.2. 產生的緣由

我在前幾篇介紹Service Worker的文章中沒有過多強調Scope的概念:ios

scope: A USVString representing a URL that defines a service worker's registration scope; what range of URLs a service worker can control. This is usually a relative URL. The default value is the URL you'd get if you resolved './' using the service worker script's location as the base.

Scope規定了Service Worker的做用(URL)範圍。例如,一個註冊在https://www.sample.com/list路徑下的Service Worker,其做用的範圍只能是它自己與它的子路徑:git

  • https://www.sample.com/list
  • https://www.sample.com/list/book
  • https://www.sample.com/list/book/comic

而在https://www.sample.comhttps://www.sample.com/book這些路徑下則是無效的。github

同時,scope的默認值爲./(注意,這裏全部的相對路徑不是相對於頁面,而是相對於sw.js腳本的)。所以,navigator.serviceWorker.register('/static/home/js/sw.js')代碼中的scope其實是/static/home/js,Service Worker也就註冊在了/static/home/js路徑下,顯然沒法在/home下生效。web

這種狀況很是常見:咱們會把sw.js這樣的文件放置在項目的靜態目錄下(例如文中的/static/home/js),而並不是頁面路徑下。顯然,要解決這個問題須要設置相應的scope。chrome

然而,另外一個問題出現了。若是你直接將scope設置爲/home

navigator.serviceWorker.register('/static/home/js/sw.js', {scope: '/home'})

在chrome控制檯會看到以下的錯誤提示:

Uncaught (in promise) DOMException: Failed to register a ServiceWorker: The path of the provided scope ('/home') is not under the max scope allowed ('/static/home/js/'). 
Adjust the scope, move the Service Worker script, or use the Service-Worker-Allowed HTTP header to allow the scope.

StackOverflow上對此的解釋是:

Service workers can only intercept requests originating in the scope of the current directory that the service worker script is located in and its subdirectories.

簡單來講,Service Worker只容許註冊在Service Worker腳本所處的路徑及其子路徑下。顯然,我上面的代碼觸碰到了這個規則。那怎麼辦呢?

1.3. 解決方案

解決這個問題的方式主要有兩種。

方法一:修改路由,讓sw.js的訪問路徑處於合適的位置

router.get('/sw.js', function (req, res) {
    res.sendFile(path.join(__dirname, '../../static/kspay-home/static/js/sw/', 'sw.js'));
});

以上是一個express中簡單的路由。經過路由設置,咱們將Service Worker腳本路徑置於根目錄下,這樣就能夠設置scope爲/home而不會違反其規則了:

navigator.serviceWorker
    .register('/static/home/js/sw.js', {
        scope: '/home'
    })

方法二:添加Service-Worker-Allowed響應頭

scope的規範有時候過於嚴格了。所以,瀏覽器也提供了一種方式來使咱們能夠越過這種限制。方法就是設置Service-Worker-Allowed響應頭。

以express中的靜態服務中間件serve-static爲例,進行相應配置

options: {
    maxAge: 0,
    setHeaders: function (res, path, stat) {
        // 添加Service-Worker-Allowed,擴展service worker的scope
        if (/\/sw\/.+\.js/.test(path)) {
            res.set({
                'Content-Type': 'application/javascript',
                'Service-Worker-Allowed': '/home'
            });
        }
    }
}

2. CORS

跨域資源的緩存報錯

2.1. 遇到的問題

《【PWA學習與實踐】(3) 讓你的WebApp離線可用》中我介紹瞭如何用Service Worker進行緩存以實現離線功能。其中,爲了提升體驗,咱們會在Service Worker安裝時緩存靜態文件,實現這一功能的部分代碼以下:

// 監聽install事件,安裝完成後,進行文件緩存
self.addEventListener('install', e => {
    var cacheOpenPromise = caches.open(cacheName).then(function (cache) {
        return cache.addAll(cacheFiles);
    });
    e.waitUntil(cacheOpenPromise);
});

cacheFiles就是須要緩存的靜態文件列表。然而Service Worker運行後,在application tab中發現cacheFiles的靜態資源並未被緩存下來。

2.2. 產生的緣由

切換到Console能夠看到相似以下的報錯信息:

前端同窗對這個問題很是熟悉:跨域問題

爲了使咱們的頁面可以順利加載CDN等外站資源,瀏覽器在scriptlinkimg等標籤上放鬆了跨域限制。這使得咱們在頁面中經過script標籤來加載javascript腳本是不會致使跨域問題的(經典的jsonp就是以此爲基礎實現的)。

然而在Service Worker中使用cache.addAll()則會經過相似fetch請求的方式來獲取資源(相似在頁面中使用XHR請求外站腳本),是會受到跨域資源策略限制而沒法緩存到本地的。

在實際生產環境中,爲了縮短請求的響應時間與、減輕服務器壓力,一般咱們都會將javascript、css、image這些靜態資源經過CDN進行分發,或者將其放置在一些獨立的靜態服務集羣中。因此線上的靜態資源基本都是「跨站資源」。

2.3. 解決方案

該問題其實不算是Service Worker中的特定問題,解決方式和處理通常的跨域問題相似,能夠設置Access-Control-Allow-Origin響應頭來解決。

  • 若是使用CDN,能夠在CDN服務中進行配置。通常的CDN服務是會支持配置HTTP響應頭的;
  • 若是使用本身搭建的靜態服務器集羣,能夠對服務器進行相應配置。這裏有一個倉庫包含ngix、apache、iis等經常使用服務器的配置,能夠參考。

3. iOS standalone 模式

iOS standalone模式下的特殊處理

3.1. 遇到的問題

今年年初Apple宣佈在iOS safari 11.3中支持Service Worker,這對PWA的推廣起到了重要的做用,讓咱們能夠「跨平臺」來實現PWA技術。

雖然,iOS safari不支持manifest配置來實現添加到桌面,可是我在《【PWA學習與實踐】(2) 使用Manifest,讓你的WebApp更「Native」》中介紹瞭如何用safari自有的meta標籤來實現standalone模式。

不過,問題就出在了standalone模式上。拋開iOS safari standalone模式現有的一些其餘小bug(包括狀態欄的顯示、白屏、重複添加等),iOS safari standalone模式有一個沒法迴避的重大問題。其源於iOS與android的一個重要區別:

iOS沒有後退鍵,而通常android機都有。

在iOS上使用standalone模式添加的應用,因爲沒有瀏覽器的工具欄,因此沒法進行後退。例如我打開首頁,而後點擊首頁課程列表中的一門課程後,瀏覽器跳轉到課程頁,因爲iOS沒有後退鍵,因此你沒法再回到首頁,除非殺死「應用」從新啓動。

3.2. 產生的緣由

正如上面所提到的,因爲iOS沒有後退鍵,而standalone模式會隱藏瀏覽器工具條和導航條,所以,在iOS中使用保存到桌面的WebApp,就像是一次不能回頭的旅行……

3.3. 解決方案

顯然,這種體驗是沒法接受的。目前我採用的解決方案很是簡單,在打開頁面時進行判斷,若是是iOS中的standalone模式,則在頁面右上角顯示一個「返回」小圖標。點擊圖標返回上一個頁面。

iOS中有一個專門的屬性來判斷是否爲standalone模式:

if ('standalone' in window.navigator && window.navigator.standalone) {
    // standalone模式進行特殊處理,例如展現返回按鈕
    backBtn.show();
}

使用history API便可實現按鈕的後退功能:

backBtn.addEventListener('click', function () {
    window.history.back();
});

4. 圖片策略

解決PWA離線資源中非緩存圖片資源的展現

4.1. 遇到的問題

在實際使用中,爲了知足必定的離線功能,我緩存了一些變化頻率極小的API數據,例如我的中內心的列表信息。而列表中包含了較多的圖片。爲了節省了用戶的存儲空間,對於圖片資源我並未選擇緩存。

這致使了一個問題:離線狀況下,雖然用戶能正常看到列表信息,可是其中的圖片部分都是相似下面這種「圖裂了」的狀況,體驗不太好。

4.2. 產生的緣由

緣由上面已經解釋了,離線狀態下沒法請求到圖片資源,因此在一些瀏覽器中就會表現出這種「圖掛了」的狀態。

4.3. 解決方案

解決這個體驗問題的大體思路以下:

  1. 首先,須要在本地緩存佔位圖資源
  2. 其次,在獲取圖片時判斷是否出現錯誤
  3. 最後,在錯誤時使用佔位圖進行替換

因爲只是緩存佔位圖,而佔位圖通常較爲固定,只會有有限的幾種尺寸樣式,所以不會產生太多緩存空間的佔用。佔位圖的緩存徹底能夠在緩存靜態資源時一塊兒進行。

而圖片獲取出錯(多是網絡緣由,也多是URL錯誤)時,進行佔位圖的替換有兩種簡單的方式:

方法一:在fetch事件中監聽圖片資源,出錯時使用佔位圖

self.addEventListener('fetch', e => {
    if (/\.png|jpeg|jpg|gif/i.test(e.request.url)) {
        e.respondWith(
            fetch(e.request).then(response => {
                return response;
            }).catch(err => {
                // 請求錯誤時使用佔位圖
                return caches.match(placeholderPic).then(cache => cache);
            })
        );
        return;
    }

方法二:經過img標籤的onerror屬性來請求佔位圖

先將img標籤改成

<img class="list-cover"
    src="//your.sample.com/1234.png"
    alt="{{ item.desc }}"
    onerror="javascript:this.src='https://your.sample.com/placeholder.png'"/>

onerror屬性中指定的方法會在圖片加載錯誤時替換src;同時咱們將Service Worker中的代碼進行調整:

self.addEventListener('fetch', e => {
    if (/\.png|jpeg|jpg|gif/i.test(e.request.url)) {
        e.respondWith(
            fetch(e.request).then(response => {
                return response;
            // 觸發onerror後,img會再次請求圖片placeholder.png
            // 因爲無網絡鏈接,此fetch依然會出錯
            }).catch(err => {
                // 因爲咱們事先緩存了placeholder.png,這裏會返回緩存結果
                return caches.match(e.request).then(cache => cache);
            })
        );
        return;
    }

5. 寫在最後

本文總結了一些我在進行PWA升級實踐中遇到的問題,但願對遇到相似問題的朋友可以有一些啓發或幫助。

在下一篇文章中,我會回到PWA相關技術,介紹Resource Hint,以及如何使用Resource Hint來提升頁面的加載性能,提高用戶體驗。

《PWA學習與實踐》系列

參考資料

Service Worker Scope

CORS

iOS standalone

相關文章
相關標籤/搜索