Service Worker 从入门到出门

16,472 阅读14分钟

前言

正常的开场本应该是“什么是Service Worker”,但Service Worker往往会和PWA被一同提起。无论屏幕前的你是有丰富PWA开发经验的大佬,还是从没有听过这个概念的小佬,本文都有义务讲清楚Service Worker和PWA的关系,在此假设大家仅仅是一个对Service Worker感兴趣的前端工程师,并没有丰富的相关经验。
另外,本文力求抓住重点,理清思路,并不是一篇偏技术流的文章,如果您只是想了解某些API怎么用,或者遇到了什么问题需要解决,那本文将浪费您5-20分钟的时间,点个赞之后就快快关掉吧~

从PWA说起

PWA全称Progressive Web Apps,直译过来是“渐进式网络应用程序”,看到这个翻译,大多数人应该是不接受的,因为我们并不能从字面上理解PWA是什么。以下是维基百科对PWA的定义:

渐进式网络应用程序(英语:Progressive Web Apps,简称:PWA)是一种普通网页或网站架构起来的网络应用程序,但它可以以传统应用程序或原生移动应用程序的形式展示给用户。

这也是一个正确但不充分的定义,不能很好描述PWA的真实特性。

经过一段时间的整理,在此表达一下本文对PWA的理解:
Web和App都懂,但Progressive是几个意思?说起“Progressive 渐进式”,想必大家或多或少听过一些关于Web应用“平稳退化、渐进增强”的设计理念,由于浏览器对于Web标准的跟进会有不同程度的滞后(更有甚者不但不跟进还要乱搞),很多优秀的新特性老旧浏览器并不支持,所以开发者有时会采取渐进式的策略,充分利用新特性,为支持新特性的浏览器提供更完善的功能和更好的体验( 让一部分人先富起来?)。PWA之P,大约就是这个意思。
众所周知,Web应用和Native应用原本井水不犯河水,二者有着各自的应用场景和优势。但随着浮夸的移动互联网时代的到来,贪婪的人类想要取长补短,兼顾二者的优点,乐此不疲地发明了一坨又一坨血统不纯的烂尾混合开发技术,在此不一一列举,大家都懂。PWA是为了达到同样目的的另一种尝试,它绝不是革命性的技术,只是传统Web应用向Native应用的又一次疯狂试探,也只是一次不大不小的进化而已。但区别于混合开发技术,PWA是血统纯正的Web技术的自然延伸,背后有相关Web标准支撑。

这一次,PWA相对于传统Web应用,主要在以下几个方面变得更强:

  • 观感方面:在手机上,可以添加Web应用到桌面,并可提供类似于Native应用的沉浸式体验(翻译成人话就是可以隐藏浏览器的脑门)。这部分背后的技术是manifest,虽然可以给带来全新的体验,但不是本文重点,就此别过。
  • 性能方面:由于本文主角Service Worker具有拦截浏览器HTTP请求的超能力,搭配CacheStorage,PWA可以提升Web应用在网络条件不佳甚至离线时的用户体验和性能。
  • 其它方面:推送通知、后台同步等可选的高级功能,这些功能也是利用Service Worker来实现的。

简单总结:PWA是Web应用的自然进化,Service Worker是PWA的关键。

目前来看,本文已疑似离题,所以让我们赶快回到Service Worker

Service Worker是浏览器在后台独立于网页运行的、用JavaScript编写的脚本。
让我们来看看最小的Service Worker长什么样,以及怎么跑起来:(朋友们请不要看见代码块就自动略过,请相信我真的没有几行)

// 不起眼的一行if,除了防止报错之外,也无意间解释了PWA的P:
// 如果浏览器不支持Service Worker,那就当什么都没有发生过
if ('serviceWorker' in navigator) {
    window.addEventListener('load', function () {
        // 所以Service Worker只是一个挂在navigator对象上的HTML5 API而已
        navigator.serviceWorker.register('/service-worker.js').then(function (registration) {
            console.log('我注册成功了666');
        }, function (err) {
            console.log('我注册失败了');
        });
    });
}

以上代码,在load事件触发后,下载并注册了service-worker.js这个文件,Service Worker的逻辑,就写在这里:

// service-worker.js
// 虽然可以在里边为所欲为地写任何js代码,或者也可以什么都不写,
// 都不妨碍这是一个Service Worker,但还是举一个微小的例子:
self.addEventListener('fetch', function (event) {
    if (/\.png$/.test(event.request.url)) {
        event.respondWith(fetch('/images/支付宝收款码.png'));
    }
});

受到身后TL大哥桌面上扫码提需求支付宝二维码的启发,以上代码,可以拦截网页上所有png图片的请求,返回你的支付宝收款码图片,只要用户够多,总会有人给你打钱的。
代码中的self是第一个匪夷所思的地方,看起来是一个未定义的变量,但稍加思索我们就可以意识到这是一个关键字,类似于window或global,代表该Service Worker自身,所以想要玩转Service Worker,是需要学习它的API的。
我们暂且不去仔细看那些需要背的API,只需要记得这段代码做了什么,它像一个middleware一样,拦截并处理了HTTP请求,此时的Service Worker,可以理解为一个客户端代理。由于代码是人为编写的,开启了无限可能。另外,最可怕的就是流氓会武术了,所以Service Worker要求HTTPS,注意那个"S",但为了开发调试方便,localhost除外。

简单总结:

  • 我们需要手动编写service-worker.js文件。
  • 我们需要在网页中下载并注册service-worker.js文件。
  • Service Worker具有超能力,可以拦截并处理HTTP请求。

Service Worker的生命周期

按照一篇文章的一贯节奏,到这个该深入的阶段,阅读体验往往突然变差,为了避免这种情况,不得不提的Service Worker生命周期话题,推荐大家去其它地方阅读,这种文章已经很多了,本文作者认为,此处可以写但没必要,并且没有信心写得比它们好,写了反而有抄袭凑字之嫌,所以在此仅仅贴出MDN的图,并简单总结(微笑)。

image

简单总结:

  • Service Worker生命周期:安装中、安装后、激活中、激活后、我废了。(有点像组件的生命周期不是吗?)
  • 首次导航到网站时,会下载、解析并执行Service Worker文件,触发install事件,尝试安装Service Worker,如果install事件回调函数中的操作都执行成功,标志Service Worker安装成功,此时进入waiting状态,注意这时的Service Worker只是准备好了而已,并没有生效,当用户二次进入网站时,才会激活Service Worker,此时会触发activate事件,标志Service Worker正式启动,开始响应fetch、post、sync等事件。

Service Worker的主要事件

  • install:顾名思义,Service Worker安装时触发,通常在这个时机缓存文件。
  • activate:顾名思义,Service Worker激活时触发,通常在这个时机做一些重置的操作,例如处理旧版本Service Worker的缓存。
  • fetch:顾名思义,浏览器发起HTTP请求时触发,通常在这个事件的回调函数中匹配缓存,是最常用的事件。
  • push:顾名思义,和推送通知功能相关,没有相关需求的话可以不去了解。
  • sync:顾名思义,和后台同步功能相关,没有相关需求的话可以不去了解。

Service Worker的应用

当我们掌握了Service Worker的基础,就可以尝试着应用了,在此着重介绍一些关乎网页性能方面的应用,不会粘贴代码,而主要谈谈思路和方法。

1.缓存静态资源

Service Worker的一大应用是可以利用CacheStorage API来缓存js、css、字体、图片等静态文件。我们可以在Service Worker的install阶段,指定需要缓存的具体文件,在fetch事件的回调函数中,检查请求的url,如果匹配了已缓存的资源,则不再从服务端获取,以此达到提升网页性能的目的。常用的构建PWA的App Shell架构,就是利用这种方式实现的。
需要注意的是,性能的提升是相对于完全没有缓存的情况来讲的,而浏览器本身有着相对完善的HTTP缓存机制。所以使用Service Worker缓存,并不能使我们已经相对完善的架构有立竿见影的性能提升,Service Worker缓存真正有意义的地方在于,利用它可以更精准地、以编码方式控制缓存,如何缓存、缓存什么、如何更新缓存,完全取决于代码如何写,所以这提供了很大的自由度,但同时也带来维护成本。它只是换了一种缓存方式,而不是从无到有的突破。

2.离线体验

上一部分我们只是缓存了js、css、字体、图片等静态资源,但如果我们将首页index.html也缓存呢?那结果是我们的网页甚至可以支持离线浏览。
听起来很棒是吧?请坐下,这里有一个巨大的问题:假定我们的主页是index.html,里边注册了service-worker.js,service-worker.js中缓存了index.html以达到离线浏览的目的,那么问题来了,在我们下次上线,死活是不会生效的,因为用户访问的总是缓存过的index.html,是不是很尴尬?我们需要更新service-worker.js来重新缓存index.html,虽然网上也有一些方案解决这种问题,但似乎异常纠结,让我们难免怀疑离线浏览是否有意义。
下面的做法似乎更好一些:既然我们现在具有了离线缓存文件和拦截HTTP请求的能力,那我们可以在Service Worker安装时,缓存一个offline.html,类似于404页面。离线状态下,我们访问index.html文件的请求是会失败的,在这个时机,我们返回offline.html文件展示给用户,至于具体展示些什么,取决于具体需求,甚至可以像chrome离线时那样,做一个小游戏来调戏没有网络的用户。

image

3.其它

Service Worker只是提供了一些厉害的功能,但如何应用完全取决于开发者,如果脑洞大开,完全有可能提供令人耳目一新的体验。在此仅仅举一个小例子:
我们知道,网页中图片是很消耗带宽资源的,用户等待网站加载,很多时候都是在等图片,而大多数放在CDN上的图片,都支持添加后缀参数获取不同分辨率照片的功能。假设我们有办法知道一个用户的网络条件的好坏(至于如何判断一个用户的网络条件,是另外一件事,可以让用户选择,也可用技术手段解决),把用户分级,暂且分为两级:网速快的和网速慢的。我们把网速级别信息放到HTTP请求的header中(或其它你想得到的合适的地方),当发起图片请求的时候,我们有机会拿到用户的网络级别,如果是网速快的用户,我们通过后缀参数返回CDN上高分辨率的图片,反之相反。
这样的结果是网速快的用户可以看到更清晰的照片,而网速慢的用户虽然看到的照片清晰度差,但可以更早地看到照片,不必经过漫长的等待。

注意事项

以上的讨论,都是纸上谈兵,当我们真的要在生产环境实际应用的时候,仔细想想,就会发现事情不是那么简单,在此列举一些需要注意和考虑的地方。

  • 首先,我们要做的是买好后悔药。因为无论如何,Service Worker是在千奇百怪的客户端做一些危险的操作,如果出现问题(肯定会出现问题),往往不是bug,而是事故级别。那样的话,我们恐怕需要在另一家公司才能继续试验我们可爱的Service Worker了。想的到的办法是,在类似配置中心的地方,或服务端,提供开关接口,请求页面前调用接口,如果发现事情不妙,就关闭开关,触发销毁Service Worker的代码。
  • 如果项目经过了迭代,那么在项目测试的时候,QA同学的浏览器环境和真实用户的浏览器环境是不同的,至少是覆盖不全的。Service Worker的存在,本质上把原本就有状态的客户端变成了状态更加复杂的客户端。所以可能导致一些bug无法在测试阶段被发现,而且目测这些bug极难复现、调试、解决和检验。

应用场景

接下来让我们来谈谈Service Worker的应用场景,或者说什么样的情况才需要上Service Worker,这很好理解,但很重要,我们不能拿着锤子,看什么都像钉子。

  • 网站功能趋于稳定:频繁迭代的网站似乎不方便加Service Worker。
  • 网站需要拥有大量用户:管理后台、OA系统等场景似乎不是很有必要加Service Worker。
  • 网站真的在追求用户体验:Bug多多、脸不好看的网站似乎不是很有必要加Service Worker。
  • 网站用户体验关乎用户留存:12306似乎完全不需要加Service Worker。
  • 等等等等。。。
    image

简单总结:Service Worker的初衷是极致优化用户体验,是用来锦上添花的,技术只是技术,但实际应用前,应考虑成本和收益。

一些废话

本文简单谈了谈PWA和Service Worker的关系、Service Worker的基础、Service Worker的生命周期和事件、Service Worker的简单应用,以及实际应用中的注意事项和场景,只是一篇入门级的科普文章,大家可根据自己感兴趣的具体方面,去深入了解。鉴于本文作者是个才疏学浅的喷子,文章难免有所疏漏甚至误导,欢迎指正。

另外在码字过程中,有一些感想在此和大家分享:

  • Service Worker和PWA已经出现大约4年左右了,但似乎一直是叫好不叫座的状态,也看不到什么爆发的趋势,其原因可能是多方面的,但最大的原因,可能是没有巨大的需求来推动,真正有潜力的技术,一定是你觉得不学都混不下去的那种。
  • Web应用和Native应用的本质区别,是编译产物放在服务端还是客户端。Web应用的静态资源需要从服务端获取,是造成了Web应用体验差的一大原因。浏览器HTTP缓存,和Service Worker的缓存,都是折中的方案,至少解决不了首屏加载慢的问题。
  • 眼看着5G时代就要到来,Web应用的性能问题,是否还是问题?Service Worker可能带来的那几百毫秒级的性能提升,是否还有意义?当大多数人的网络条件上了新的台阶之后,可能网页将不再卡那么几秒,APP也可以瞬间下载完成,那时我们真的还会折腾缓存吗?网页和APP还都会存在吗?

事已至此,本文已离题无误,至于Service Worker,用来吹牛实在是再合适不过了,如果真要大规模应用,大家可能需要结合具体情况综合考虑。但无论如何,作为一项技术储备还是有必要的。不说了,外卖凉了。那你还愣着干嘛?快赶快点赞评论加收藏啊!

image

关于我们

快狗打车前端团队专注前端技术分享,定期推送高质量文章,欢迎关注点赞。