
本文是《PWA學(xué)習(xí)與實踐》系列的第九篇文章。
PWA作為時下最火熱的技術(shù)概念之一,對提升Web應(yīng)用的安全、性能和體驗有著很大的意義,非常值得我們?nèi)チ私馀c學(xué)習(xí)。
本系列文章《PWA學(xué)習(xí)與實踐》會逐步拆解PWA背后的各項技術(shù),通過實例代碼來講解這些技術(shù)的應(yīng)用方式。也正是因為PWA中技術(shù)點眾多、知識細(xì)碎,因此我在學(xué)習(xí)過程中,進行了整理,并產(chǎn)出了《PWA學(xué)習(xí)與實踐》系列文章,希望能帶大家全面了解PWA中的各項技術(shù)。對PWA感興趣的朋友歡迎關(guān)注。
引言
在前八篇文章中,我已經(jīng)介紹了一些PWA中的常見技術(shù)與使用方式。雖然我們已經(jīng)學(xué)習(xí)了很多相關(guān)知識,但是,還是有很多問題在實踐時才會暴露出來。這篇文章是一篇TroubleShooting,總結(jié)了我近期在PWA實踐過程中遇到了一些問題,以及這些問題的解決方案。希望能幫助一些遇到類似問題的朋友。
1. Service Worker Scope
注意Service Worker注冊時的作用范圍(scope)
1.1. 遇到的問題
我在頁面/home下注冊了Service Worker:
navigator.serviceWorker.register('/static/home/js/sw.js')
通過在.then()中調(diào)用console.log()可以發(fā)現(xiàn)Service Worker其實注冊成功了,但是在頁面中卻不生效。這是為什么呢?
1.2. 產(chǎn)生的原因
我在前幾篇介紹Service Worker的文章中沒有過多強調(diào)Scope的概念:
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規(guī)定了Service Worker的作用(URL)范圍。例如,一個注冊在https://www.sample.com/list路徑下的Service Worker,其作用的范圍只能是它本身與它的子路徑:
https://www.sample.com/listhttps://www.sample.com/list/bookhttps://www.sample.com/list/book/comic
而在https://www.sample.com、https://www.sample.com/book這些路徑下則是無效的。
同時,scope的默認(rèn)值為./(注意,這里所有的相對路徑不是相對于頁面,而是相對于sw.js腳本的)。因此,navigator.serviceWorker.register('/static/home/js/sw.js')代碼中的scope實際上是/static/home/js,Service Worker也就注冊在了/static/home/js路徑下,顯然無法在/home下生效。
這種情況非常常見:我們會把sw.js這樣的文件放置在項目的靜態(tài)目錄下(例如文中的/static/home/js),而并非頁面路徑下。顯然,要解決這個問題需要設(shè)置相應(yīng)的scope。
然而,另一個問題出現(xiàn)了。如果你直接將scope設(shè)置為/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腳本所處的路徑及其子路徑下。顯然,我上面的代碼觸碰到了這個規(guī)則。那怎么辦呢?
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中簡單的路由。通過路由設(shè)置,我們將Service Worker腳本路徑置于根目錄下,這樣就可以設(shè)置scope為/home而不會違反其規(guī)則了:
navigator.serviceWorker
.register('/static/home/js/sw.js', {
scope: '/home'
})
方法二:添加Service-Worker-Allowed響應(yīng)頭
scope的規(guī)范有時候過于嚴(yán)格了。因此,瀏覽器也提供了一種方式來使我們可以越過這種限制。方法就是設(shè)置Service-Worker-Allowed響應(yīng)頭。
以express中的靜態(tài)服務(wù)中間件serve-static為例,進行相應(yīng)配置:
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學(xué)習(xí)與實踐】(3) 讓你的WebApp離線可用》中我介紹了如何用Service Worker進行緩存以實現(xiàn)離線功能。其中,為了提高體驗,我們會在Service Worker安裝時緩存靜態(tài)文件,實現(xiàn)這一功能的部分代碼如下:
// 監(jiān)聽install事件,安裝完成后,進行文件緩存
self.addEventListener('install', e => {
var cacheOpenPromise = caches.open(cacheName).then(function (cache) {
return cache.addAll(cacheFiles);
});
e.waitUntil(cacheOpenPromise);
});
cacheFiles就是需要緩存的靜態(tài)文件列表。然而Service Worker運行后,在application tab中發(fā)現(xiàn)cacheFiles的靜態(tài)資源并未被緩存下來。
2.2. 產(chǎn)生的原因
切換到Console可以看到類似如下的報錯信息:
前端同學(xué)對這個問題非常熟悉:跨域問題。
為了使我們的頁面能夠順利加載CDN等外站資源,瀏覽器在script、link、img等標(biāo)簽上放松了跨域限制。這使得我們在頁面中通過script標(biāo)簽來加載javascript腳本是不會導(dǎo)致跨域問題的(經(jīng)典的jsonp就是以此為基礎(chǔ)實現(xiàn)的)。
然而在Service Worker中使用cache.addAll()則會通過類似fetch請求的方式來獲取資源(類似在頁面中使用XHR請求外站腳本),是會受到跨域資源策略限制而無法緩存到本地的。
在實際生產(chǎn)環(huán)境中,為了縮短請求的響應(yīng)時間與、減輕服務(wù)器壓力,通常我們都會將javascript、css、image這些靜態(tài)資源通過CDN進行分發(fā),或者將其放置在一些獨立的靜態(tài)服務(wù)集群中。所以線上的靜態(tài)資源基本都是“跨站資源”。
2.3. 解決方案
該問題其實不算是Service Worker中的特定問題,解決方式和處理一般的跨域問題類似,可以設(shè)置Access-Control-Allow-Origin響應(yīng)頭來解決。
- 如果使用CDN,可以在CDN服務(wù)中進行配置。一般的CDN服務(wù)是會支持配置HTTP響應(yīng)頭的;
- 如果使用自己搭建的靜態(tài)服務(wù)器集群,可以對服務(wù)器進行相應(yīng)配置。這里有一個倉庫包含ngix、apache、iis等常用服務(wù)器的配置,可以參考。
3. iOS standalone 模式
iOS standalone模式下的特殊處理
3.1. 遇到的問題
今年年初Apple宣布在iOS safari 11.3中支持Service Worker,這對PWA的推廣起到了重要的作用,讓我們可以“跨平臺”來實現(xiàn)PWA技術(shù)。
雖然,iOS safari不支持manifest配置來實現(xiàn)添加到桌面,但是我在《【PWA學(xué)習(xí)與實踐】(2) 使用Manifest,讓你的WebApp更“Native”》中介紹了如何用safari自有的meta標(biāo)簽來實現(xiàn)standalone模式。
不過,問題就出在了standalone模式上。拋開iOS safari standalone模式現(xiàn)有的一些其他小bug(包括狀態(tài)欄的顯示、白屏、重復(fù)添加等),iOS safari standalone模式有一個無法回避的重大問題。其源于iOS與android的一個重要區(qū)別:
iOS沒有后退鍵,而一般android機都有。
在iOS上使用standalone模式添加的應(yīng)用,由于沒有瀏覽器的工具欄,所以無法進行后退。例如我打開首頁,然后點擊首頁課程列表中的一門課程后,瀏覽器跳轉(zhuǎn)到課程頁,由于iOS沒有后退鍵,所以你無法再回到首頁,除非殺死“應(yīng)用”重新啟動。
3.2. 產(chǎn)生的原因
正如上面所提到的,由于iOS沒有后退鍵,而standalone模式會隱藏瀏覽器工具條和導(dǎo)航條,因此,在iOS中使用保存到桌面的WebApp,就像是一次不能回頭的旅行……
3.3. 解決方案
顯然,這種體驗是無法接受的。目前我采用的解決方案非常簡單,在打開頁面時進行判斷,如果是iOS中的standalone模式,則在頁面右上角顯示一個“返回”小圖標(biāo)。點擊圖標(biāo)返回上一個頁面。
iOS中有一個專門的屬性來判斷是否為standalone模式:
if ('standalone' in window.navigator && window.navigator.standalone) {
// standalone模式進行特殊處理,例如展示返回按鈕
backBtn.show();
}
使用history API即可實現(xiàn)按鈕的后退功能:
backBtn.addEventListener('click', function () {
window.history.back();
});
4. 圖片策略
解決PWA離線資源中非緩存圖片資源的展示
4.1. 遇到的問題
在實際使用中,為了滿足一定的離線功能,我緩存了一些變化頻率極小的API數(shù)據(jù),例如個人中心里的列表信息。而列表中包含了較多的圖片。為了節(jié)省了用戶的存儲空間,對于圖片資源我并未選擇緩存。
這導(dǎo)致了一個問題:離線情況下,雖然用戶能正??吹搅斜硇畔ⅲ瞧渲械膱D片部分都是類似下面這種“圖裂了”的情況,體驗不太好。
4.2. 產(chǎn)生的原因
原因上面已經(jīng)解釋了,離線狀態(tài)下無法請求到圖片資源,所以在一些瀏覽器中就會表現(xiàn)出這種“圖掛了”的狀態(tài)。
4.3. 解決方案
解決這個體驗問題的大致思路如下:
- 首先,需要在本地緩存占位圖資源
- 其次,在獲取圖片時判斷是否出現(xiàn)錯誤
- 最后,在錯誤時使用占位圖進行替換
由于只是緩存占位圖,而占位圖一般較為固定,只會有有限的幾種尺寸樣式,因此不會產(chǎn)生太多緩存空間的占用。占位圖的緩存完全可以在緩存靜態(tài)資源時一起進行。
而圖片獲取出錯(可能是網(wǎng)絡(luò)原因,也可能是URL錯誤)時,進行占位圖的替換有兩種簡單的方式:
方法一:在fetch事件中監(jiān)聽圖片資源,出錯時使用占位圖
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標(biāo)簽的onerror屬性來請求占位圖
先將img標(biāo)簽改為
<img class="list-cover"
src="http://your.sample.com/1234.png"
alt="{{ item.desc }}"
onerror="javascript:this.src='https://your.sample.com/placeholder.png'"/>
onerror屬性中指定的方法會在圖片加載錯誤時替換src;同時我們將Service Worker中的代碼進行調(diào)整:
self.addEventListener('fetch', e => {
if (/\.png|jpeg|jpg|gif/i.test(e.request.url)) {
e.respondWith(
fetch(e.request).then(response => {
return response;
// 觸發(fā)onerror后,img會再次請求圖片placeholder.png
// 由于無網(wǎng)絡(luò)連接,此fetch依然會出錯
}).catch(err => {
// 由于我們事先緩存了placeholder.png,這里會返回緩存結(jié)果
return caches.match(e.request).then(cache => cache);
})
);
return;
}
5. 寫在最后
本文總結(jié)了一些我在進行PWA升級實踐中遇到的問題,希望對遇到類似問題的朋友能夠有一些啟發(fā)或幫助。
在下一篇文章中,我會回到PWA相關(guān)技術(shù),介紹Resource Hint,以及如何使用Resource Hint來提高頁面的加載性能,提升用戶體驗。
《PWA學(xué)習(xí)與實踐》系列
- 第一篇:2018,開始你的PWA學(xué)習(xí)之旅
- 第二篇:10分鐘學(xué)會使用Manifest,讓你的WebApp更“Native”
- 第三篇:從今天起,讓你的WebApp離線可用
- 第四篇:TroubleShooting: 解決FireBase login驗證失敗問題
- 第五篇:與你的用戶保持聯(lián)系: Web Push功能
- 第六篇:How to Debug? 在chrome中調(diào)試你的PWA
- 第七篇:增強交互:使用Notification API來進行提醒
- 第八篇:使用Service Worker進行后臺數(shù)據(jù)同步
- 第九篇:PWA實踐中的問題與解決方案(本文)
- 第十篇:Resource Hint - 提升頁面加載性能與體驗(寫作中…)
參考資料
Service Worker Scope
- Service Worker Scope (MDN)
- Understanding Service Worker scope
- How exactly add “Service-Worker-Allowed” to register service worker scope in upper folder
CORS
- What limitations apply to opaque responses?
- Handle Third Party Requests
- CORS settings attributes
- Cross-Origin Resource Sharing (CORS)
- Git Repo: server configs