查看原文
其他

【第1025期】理解Service Worker

2017-08-13 安秦 前端早读课

前言

昨天推送完看了会综艺节目就顺便去环岛路跑步,上一次跑应该是去年的事情了。今日早读文章由百度外卖前端@安秦翻译分享。

正文从这开始~

译者注

PWA是最近前端最火热的一个概念之一,Service Worker为PWA赋能离线可用性以及push消息。

原文最后讲了在Ember.js框架上的实践介绍,因为原作者是Ember.js的拥趸,但鉴于国内React、Vue、Angular居多(不要引起战争,害怕.jpg),译文就省略那部分内容了。

Service Worker是啥?能用来干啥?又如何能提升你的web应用的体验?本文就是来回答这些问题的。

背景

在那个网络还很年轻的时代,很少有人去想一个网页在用户断网的情况下应该有什么样的表现。你就应该一直是在线的。

然而在移动互联网的时代,不稳定的网络链接在现代网络变得原来越常见。鉴于此,允许网站自己决定离线时的行为变得弥足珍贵,这样用户就不会被网络状态局限。

最初,H5标准中推出了应用缓存作为离线web应用的解决方案。它以一个缓存清单为中心将HTML与JS组合起来,这清单是一个用声明式语法编写的配置文件。

但是最终,大家发现应用缓存存在太多的坑。此后,稍有人再赞同使用它,取而代之的是Service Worker。

Service Worker带来的离线可用解决方案更加符合未来发展趋势。它摒弃了应用缓存声明式的设计,改用一种更加命令式或者说程序性的设计方案。

Service Worker是一种在浏览器环境当中于一个持久的背景进程当中执行代码的方法。被执行的代码时事件驱动的,也就是说驱动一个Service Worker的行为的,是在其中产生的事件。

本文后续就是简要地介绍一下那些事件。然而想要开始利用Service Worker,你需要先实现你的web应用明面上的功能,然后再于其中注册Service Worker。

注册

下方的代码描绘了如何在浏览器客户端当中注册你的Service Worker。这是通过在web应用的某处调用 register 实现的:

if (navigator.serviceWorker) {  
 navigator.serviceWorker.register('/sw.js')
   .then(registration => {
     console.log('恭喜。作用范围: ', registration.scope);
   })
   .catch(error => {
     console.log('抱歉', error);
   });
}

这就告知浏览器从哪里能找到你实现的Service Worker。浏览器会找到 /sw.js 文件,然后保存在当前被访问的域名的名下。该文件包含各类事件的处理逻辑,整体定义你的Service Worker 的行为。


在Chrome开发者工具中查看一个被注册的Service Worker


上面的代码还会定义你的Service Worker的作用范围。文件路径 /sw.js 暗含默认情况下你的SW的作用范围是你的URL的根目录(比如 http://localhost:3000/ )。那么这样,你的SW里能通过监听事件获知所有在你的url根目录里发生的请求。一个如 /js/sw.js 这样的路径只会捕获到 http://localhost:3000/js 下的请求。

你也可以显式地定义SW的作用范围,只要给 register 函数传第二个参数:

navigator.serviceWorker.register('/sw.js', { scope: '/js' })

事件处理

现在你的Service Worker已经注册好了,就该轮到实现事件处理逻辑,监听Service Worker生命周期当中的各种事件。

Install事件

Install事件是在你的Service Worker第一次注册以及SW文件(/sw.js)发生变化的时候触发的(浏览器会自动鉴别是否发生改变了)。

利用安装事件,可以实现你的SW初始化逻辑,或者说通过只执行一次的命令来设定你的SW初始状态。一种常见的用法是在安装阶段提权准备好缓存。

以下就是是一个在安装阶段预先缓存资源的例子:

const CACHE_NAME = 'cache-v1';  
const urlsToCache = [  
 '/',
 '/js/main.js',
 '/css/style.css',
 '/img/bob-ross.jpg',
];

self.addEventListener('install', event => {  
 caches.open(CACHE_NAME)
   .then(cache => {
     return cache.addAll(urlsToCache);
   });
});

urlsToCache 所包含的就是我们想要提前缓存的url列表。

caches 是一个全局 CacheStorage 对象,用于管理浏览器缓存。我们通过调用 open 来获取一个可操作的具体 Cache 对象。

cache.addAll 接收一个url数组,对每一个进行请求,然后将响应结果存到缓存里。它以请求的详细信息为键来缓存每一个值。阅读 addAll 文档了解更多。


在Chrome开发者工具里查看缓存数据


Fetch事件

每当网页里产生一个网络请求,都会触发一个fetch事件。触发的时候,你的SW可以“拦截”请求并决定想要返回什么——是缓存的数据还是一个实际网络请求的结果。

以下的例子演示一个缓存优先策略:任何匹配请求的缓存数据都会优先发送,不会发出网络请求。只有当找不到存在的缓存数据时,才会产生一个网络请求。

self.addEventListener('fetch', event => {  
 const { request } = event;
 const findResponsePromise = caches.open(CACHE_NAME)
   .then(cache => cache.match(request))
   .then(response => {
     if (response) {
       return response;
     }

     return fetch(request);
   });

 event.respondWith(findResponsePromise);
});

request 存在于一个 FetchEvent 对象,包含请求的详情。它可以用来查找一个匹配的缓存响应结果。

cache.match 会尝试为一个请求寻找匹配的缓存值。如果没能找到,这个 promise 会得到 undefined 结果。我们会检查到这种情况,并且如果发生了,就调用一次 fetch 来产生网络请求。

event.respondWith 是一个 FetchEvent 对象里专门用于向浏览器发送响应结果的方法。它接受一个最终能解析成网络响应的 promise。

紧接着,调用 event.waitUntil 来在SW被终止前执行一个 Promise 异步流程。在这里我们先做一个网络请求然后再将其缓存。这个异步操作完成后,waitUntil才会解析完成,整个操作才可以终止。

Activate事件

这个事件的文档相较而言比较少,但对于你更新SW文件很有帮助,你可以在升级SW文件的时候针对之前的版本执行清理或其他维护操作。

当你更新你的SW文件(/sw.js),浏览器会检测到并在开发者工具中如下展示:


你的新Service Worker是“等待激活”状态


当实际的网页关掉并重新打开时,浏览器会将原先的Service Worker替换成新的,然后在 install 事件之后触发 activate 事件。如果你需要清理缓存或者针对原来的SW执行维护性操作,activate 事件就是做这些事情的绝佳时机。

Sync事件

Sync事件让你可以先将网络相关任务延迟到用户有网络的时候再执行。这个功能常被称作“背景同步”。这功能可以用于保证任何用户在离线的时候所产生对于网络有依赖的操作,最终可以在网络再次可用的时候抵达它们的目标。

一下是一个背景同步样例。你需要你的前台JS注册一个同步事件,同时在SW里实现sync事件监听处理:

// app.js
navigator.serviceWorker.ready  
 .then(registration => {
   document.getElementById('submit').addEventListener('click', () => {
     registration.sync.register('submit').then(() => {
       console.log('sync registered!');
     });
   });
 });

这里,我们指定在一个按钮的点击事件里,在一个全局的 ServiceWorkerRegistration 对象身上调用 sync.register。这里,我们指定在一个按钮的点击事件里,在一个全局的 ServiceWorkerRegistration 对象身上调用 sync.register。

简单地讲,任何你需要确保在有网络时立刻执行或者等到有网再执行的操作,都需要注册为一个sync事件。

这操作可以是发送一个评论,或者获取用户信息,在SW的事件监听器里会如下定义:

// sw.js
self.addEventListener('sync', event => {  
 if (event.tag === 'submit') {
   console.log('sync!');
 }
});

这里,我们监听一个 sync 事件,然后在 SyncEvent 对象上检查 tag 是否匹配我们在点击事件里所设定的 'submit'。

如果多个 tag 标记为 submit 的 sync事件被注册了,sync 事件处理器只会运行一次。

所以在这个例子里,如果用户离线了,然后点击按钮7次,当网络再次连上,所有的sync注册都会合而为一,sync事件只会触发一次。

如果你希望每一次点击都能触发 sync 事件,你就需要在注册的时候赋予它们不同的tag。

Sync事件是什么时候触发的?

如果用户的网络时联通的,那么sync事件会立刻触发并且立刻执行你所定义的任务。

而如果用户离线了,sync 事件会在网络恢复后第一时间触发。

如果你想我一样,想要在Chrome里体验这个功能,你需要真实地断开你的网络,禁用一下Wi-Fi或者关闭一下网络驱动器。只是在Chrome开发者工具的Network选项卡里模拟网络断开是不会触发 sync 事件的。

欲了解更多,可以阅读这篇解释文档,以及这篇对背景同步的介绍。不过要注意,sync事件还没有在浏览器中得到普及(在写下这篇文章的时候还只有Chrome支持),并且用法在未来还可能有变化,请保持关注。

Push消息

在Service Worker里,通过 push 事件以及浏览器的 Push API,可以实现push消息的功能。

在说道web push消息的时候,其实涉及到两个正在完善中的技术:消息提醒 与 信息推送。

消息提醒

用Service Worker实现消息提醒挺简单直接:

// app.js
// ask for permission
Notification.requestPermission(permission => {  
 console.log('permission:', permission);
});

// display notification
function displayNotification() {  
 if (Notification.permission == 'granted') {
   navigator.serviceWorker.getRegistration()
     .then(registration => {
       registration.showNotification('this is a notification!');
     });
 }
}

// sw.js
self.addEventListener('notificationclick', event => {  
 // 消息提醒被点击的事件
});

self.addEventListener('notificationclose', event => {  
 // 消息提醒被关闭的事件
});

你需要先向用户寻求让你的网页产生消息提醒的权限。之后,你就可以弹出提示信息,然后处理某些事件,比如用户把消息关掉的事件。

信息推送

信息推送涉及到利用浏览器提供的Push API以及后端的配合实现。要讲解如何使用Push API完全可以再写一篇文章,不过基本的套路如下:



这是个略微复杂难懂的过程,已经超出这篇文章的讨论范围。不过如果你很感兴趣,可以阅读这篇push消息介绍。

结语

希望你对Service Workers以及它的基础结构已经得到了更加清晰的理解,并且了解到web应用可以如何利用它来增强用户体验。

关于本文

译者:百度外卖FE@安秦
译文:https://zhuanlan.zhihu.com/p/28461857
作者:Adnan Chowdhury
原文:http://blog.88mph.io/2017/07/28/understanding-service-workers/

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存