《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来提升页面的加载性能,提高用户体验。