《PWA學習與實踐》系列文章已整理至gitbook - PWA學習手冊,文字內容已同步至learning-pwa-ebook。轉載請註明做者與出處。javascript
本文是《PWA學習與實踐》系列的第九篇文章。css
PWA做爲時下最火熱的技術概念之一,對提高Web應用的安全、性能和體驗有着很大的意義,很是值得咱們去了解與學習。對PWA感興趣的朋友歡迎關注《PWA學習與實踐》系列文章。前端
在前八篇文章中,我已經介紹了一些PWA中的常見技術與使用方式。雖然咱們已經學習了不少相關知識,可是,仍是有不少問題在實踐時纔會暴露出來。這篇文章是一篇TroubleShooting,總結了我近期在PWA實踐過程當中遇到了一些問題,以及這些問題的解決方案。但願能幫助一些遇到相似問題的朋友。java
注意Service Worker註冊時的做用範圍(scope)android
我在頁面/home
下注冊了Service Worker:ios
navigator.serviceWorker.register('/static/home/js/sw.js')
複製代碼
經過在.then()
中調用console.log()
能夠發現Service Worker其實註冊成功了,可是在頁面中卻不生效。這是爲何呢?git
我在前幾篇介紹Service Worker的文章中沒有過多強調Scope的概念:github
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.web
Scope規定了Service Worker的做用(URL)範圍。例如,一個註冊在https://www.sample.com/list
路徑下的Service Worker,其做用的範圍只能是它自己與它的子路徑:chrome
https://www.sample.com/list
https://www.sample.com/list/book
https://www.sample.com/list/book/comic
而在https://www.sample.com
、https://www.sample.com/book
這些路徑下則是無效的。
同時,scope的默認值爲./
(注意,這裏全部的相對路徑不是相對於頁面,而是相對於sw.js腳本的)。所以,navigator.serviceWorker.register('/static/home/js/sw.js')
代碼中的scope其實是/static/home/js
,Service Worker也就註冊在了/static/home/js
路徑下,顯然沒法在/home
下生效。
這種狀況很是常見:咱們會把sw.js
這樣的文件放置在項目的靜態目錄下(例如文中的/static/home/js
),而並不是頁面路徑下。顯然,要解決這個問題須要設置相應的scope。
然而,另外一個問題出現了。若是你直接將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腳本所處的路徑及其子路徑下。顯然,我上面的代碼觸碰到了這個規則。那怎麼辦呢?
解決這個問題的方式主要有兩種。
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('/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'
});
}
}
}
複製代碼
跨域資源的緩存報錯
在《【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
的靜態資源並未被緩存下來。
切換到Console能夠看到相似以下的報錯信息:
前端同窗對這個問題很是熟悉:跨域問題。
爲了使咱們的頁面可以順利加載CDN等外站資源,瀏覽器在script
、link
、img
等標籤上放鬆了跨域限制。這使得咱們在頁面中經過script
標籤來加載javascript腳本是不會致使跨域問題的(經典的jsonp就是以此爲基礎實現的)。
然而在Service Worker中使用cache.addAll()
則會經過相似fetch請求的方式來獲取資源(相似在頁面中使用XHR請求外站腳本),是會受到跨域資源策略限制而沒法緩存到本地的。
在實際生產環境中,爲了縮短請求的響應時間與、減輕服務器壓力,一般咱們都會將javascript、css、image這些靜態資源經過CDN進行分發,或者將其放置在一些獨立的靜態服務集羣中。因此線上的靜態資源基本都是「跨站資源」。
該問題其實不算是Service Worker中的特定問題,解決方式和處理通常的跨域問題相似,能夠設置Access-Control-Allow-Origin
響應頭來解決。
iOS standalone模式下的特殊處理
今年年初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沒有後退鍵,因此你沒法再回到首頁,除非殺死「應用」從新啓動。
正如上面所提到的,因爲iOS沒有後退鍵,而standalone模式會隱藏瀏覽器工具條和導航條,所以,在iOS中使用保存到桌面的WebApp,就像是一次不能回頭的旅行……
顯然,這種體驗是沒法接受的。目前我採用的解決方案很是簡單,在打開頁面時進行判斷,若是是iOS中的standalone模式,則在頁面右上角顯示一個「返回」小圖標。點擊圖標返回上一個頁面。
iOS中有一個專門的屬性來判斷是否爲standalone模式:
if ('standalone' in window.navigator && window.navigator.standalone) {
// standalone模式進行特殊處理,例如展現返回按鈕
backBtn.show();
}
複製代碼
使用history API便可實現按鈕的後退功能:
backBtn.addEventListener('click', function () {
window.history.back();
});
複製代碼
解決PWA離線資源中非緩存圖片資源的展現
在實際使用中,爲了知足必定的離線功能,我緩存了一些變化頻率極小的API數據,例如我的中內心的列表信息。而列表中包含了較多的圖片。爲了節省了用戶的存儲空間,對於圖片資源我並未選擇緩存。
這致使了一個問題:離線狀況下,雖然用戶能正常看到列表信息,可是其中的圖片部分都是相似下面這種「圖裂了」的狀況,體驗不太好。
緣由上面已經解釋了,離線狀態下沒法請求到圖片資源,因此在一些瀏覽器中就會表現出這種「圖掛了」的狀態。
解決這個體驗問題的大體思路以下:
因爲只是緩存佔位圖,而佔位圖通常較爲固定,只會有有限的幾種尺寸樣式,所以不會產生太多緩存空間的佔用。佔位圖的緩存徹底能夠在緩存靜態資源時一塊兒進行。
而圖片獲取出錯(多是網絡緣由,也多是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;
}
複製代碼
本文總結了一些我在進行PWA升級實踐中遇到的問題,但願對遇到相似問題的朋友可以有一些啓發或幫助。
在下一篇文章中,我會回到PWA相關技術,介紹Resource Hint,以及如何使用Resource Hint來提升頁面的加載性能,提高用戶體驗。