【PWA学习与实践】(9)生产环境中PWA实践的问题与解决方案

《PWA学习与实践》系列文章已整理至gitbook - PWA学习手册,文字内容已同步至learning-pwa-ebook。转载请注明做者与出处。javascript

本文是《PWA学习与实践》系列的第九篇文章。css

PWA做为时下最火热的技术概念之一,对提高Web应用的安全、性能和体验有着很大的意义,很是值得咱们去了解与学习。对PWA感兴趣的朋友欢迎关注《PWA学习与实践》系列文章。前端

引言

在前八篇文章中,我已经介绍了一些PWA中的常见技术与使用方式。虽然咱们已经学习了不少相关知识,可是,仍是有不少问题在实践时才会暴露出来。这篇文章是一篇TroubleShooting,总结了我近期在PWA实践过程当中遇到了一些问题,以及这些问题的解决方案。但愿能帮助一些遇到相似问题的朋友。java

1. Service Worker Scope

注意Service Worker注册时的做用范围(scope)android

1.1. 遇到的问题

我在页面/home下注册了Service Worker:ios

navigator.serviceWorker.register('/static/home/js/sw.js')
复制代码

经过在.then()中调用console.log()能够发现Service Worker其实注册成功了,可是在页面中却不生效。这是为何呢?git

1.2. 产生的缘由

我在前几篇介绍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.comhttps://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脚本所处的路径及其子路径下。显然,我上面的代码触碰到了这个规则。那怎么办呢?

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('/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

相关文章
相关标签/搜索