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

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

PWA作为时下最火热的技术概念之一,对提升Web应用的安全、性能和体验有着很大的意义,非常值得我们去了解与学习。

本系列文章《PWA学习与实践》会逐步拆解PWA背后的各项技术,通过实例代码来讲解这些技术的应用方式。也正是因为PWA中技术点众多、知识细碎,因此我在学习过程中,进行了整理,并产出了《PWA学习与实践》系列文章,希望能带大家全面了解PWA中的各项技术。对PWA感兴趣的朋友欢迎关注。

引言

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

1. Service Worker Scope

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

1.1. 遇到的问题

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

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

通过在.then()中调用console.log()可以发现Service Worker其实注册成功了,但是在页面中却不生效。这是为什么呢?

1.2. 产生的原因

我在前几篇介绍Service Worker的文章中没有过多强调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规定了Service Worker的作用(URL)范围。例如,一个注册在https://www.sample.com/list路径下的Service Worker,其作用的范围只能是它本身与它的子路径:

  • 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('/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的静态资源并未被缓存下来。

image

2.2. 产生的原因

切换到Console可以看到类似如下的报错信息:

image

前端同学对这个问题非常熟悉:跨域问题

为了使我们的页面能够顺利加载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数据,例如个人中心里的列表信息。而列表中包含了较多的图片。为了节省了用户的存储空间,对于图片资源我并未选择缓存。

这导致了一个问题:离线情况下,虽然用户能正常看到列表信息,但是其中的图片部分都是类似下面这种“图裂了”的情况,体验不太好。

image

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

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,847评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,208评论 1 292
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,587评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,942评论 0 205
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,332评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,587评论 1 218
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,853评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,568评论 0 198
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,273评论 1 242
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,542评论 2 246
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,033评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,373评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,031评论 3 236
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,073评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,830评论 0 195
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,628评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,537评论 2 269

推荐阅读更多精彩内容