前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解 Service Workers

理解 Service Workers

作者头像
疯狂的技术宅
发布2019-03-27 15:22:56
1.7K0
发布2019-03-27 15:22:56
举报
文章被收录于专栏:京程一灯京程一灯

Service Workers 是什么?它们能做什么,它如何让您的 web 应用更好的表现?本文旨在回答这些问题,以及如何使用 Ember.js 框架来实现 Service Worker。

背景

在 Web 早期,对于用户离线时网页应该怎样表现,基本没有方案。用户应该 "总是" 在线的。

连接上了!大伙都在这!别离开。

但是,由于移动互联网的到来,和世界上其它领域的发展,参差不齐的互联网连接在现代网络用户中已经越来越普遍了。

因此,网站在离线时如何展现,将变得非常重要,以使用户不至于被网络可用性所限制。

AppCache 起初作为 HTML5 的特性被用作离线 web 应用的一种解决方案。 它包含以一个围绕"缓存清单"的 HTML 和 JS 的组合,一份以声明式语言编写的配置文件。

AppCache 最终被认为 "笨拙且充满陷阱"。它已经被弃用了,并被 Service Workers 所取代。

Service workers 提供了一个对离线问题更先进的解决方案。通过更严格、更程序化实现,替换了 AppCache 那种声明式的方式。

Service Workers 是在 web 浏览器所包含的持久的后台进程中执行代码的一种方式。其中的代码是事件驱动的,意味着在 Service Worker 范围内触发的事件驱动着它的行为。

文章剩余的部分将会对这些事件做一个简短的解释。但是要开始使用 Service Workers,还需要先在前置 web 应用里注册 Service Worker。

注册

下面的代码说明了怎样在客户端浏览器里 注册 Service Worker。通过在前置 web 应用中调用 register 方法即可完成注册:

代码语言:javascript
复制
if (navigator.serviceWorker) {  
  navigator.serviceWorker.register('/sw.js')
    .then(registration => {
      console.log('congrats. scope is: ', registration.scope);
    })
    .catch(error => {
      console.log('sorry', error);
    });
}

这会告诉浏览器从哪里寻找 Service Worker 的安装代码。浏览器将 ( /sw.js) 保存并作为正在访问的域下的 Service Worker,这个文件将包含您定义的 Service Worker 中所有的事件处理。

已注册的 Service Worker 在 Chrome 开发者工具栏中

它同时也会设置您的 Service Worker 的 范围 。文件名 /sw.js 表示 Service Worker 的范围是 URL 的根路径(或 http://localhost:3000),这意味着根路径下发出的请求都能过通过触发的事件被 Service Worker 捕捉,而 /js/sw.js 这样的文件名将只能捕获到 http://localhost:3000/js 下的请求。

另外,您可以通过传递第二个参数给 register 方法,明确的设置 Service Worker 的范围:

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

事件处理

现在,您的 Service Worker 已经注册成功,是时候处理 Service Worker 生命周期里触发的事件了。

Install 事件

Install 事件会在 Service Worker 第一次注册的时候触发,也会在 Service Worker 文件 ( /sw.js) 更新的时候触发(浏览器会自动检测文件变化)。

Install 事件非常有用,它可以让您在 Service Worker 初始化的时候执行逻辑。比如一个一次性的操作,为 Service Worker 的工作做好准备;普遍的例子是从安装步骤中载入缓存。

这里是一个在 Install 事件中添加数据至缓存的示例:

代码语言:javascript
复制
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 列表中的每一个文件,然后在各自的缓存中保存响应,它使用请求的 body 作为每一个缓存数据的 key。查阅 addAll 文档了解更多。

通过 Chrome 开发者工具栏查看缓存数据

Fetch 事件

fetch 事件在每次网页发送请求的时候触发。当事件触发的时候,Service Worker 可以 '拦截' 请求并决定返回结果 - 可能是缓存数据,或者是真实的网络请求响应。

下面的例子演示了一个 '缓存优先' 策略:任何与请求匹配的缓存数据会优先返回,不通过网络请求;只有在没有匹配的缓存数据的时候,才会发出网络请求。

代码语言:javascript
复制
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);
});

FetchEvent 对象中的 request 变量包含请求的 body,它被用来查找响应匹配的缓存数据。

cache.match 将会尝试去查找请求对应的缓存数据。如果没有找到,promise 将会返回 undefined。我们会首先检查是否有缓存数据,如果没有,就调用 fetch 方法发送网络请求,并返回 promise 。

event.respondWith 是 FetchEvent 特有的方法,我们用它返回一个响应给浏览器请求。它接受一个 Promise,用来返回响应数据(或网络错误)。

缓存策略

Fetch 事件尤其重要,因为您的 缓存策略 都需要在此定义。比如:什么时候该使用缓存数据,什么时候又该使用网络数据。

Service Workers 的魅力在于,它属于底层 API,可以拦截请求并让您决定怎样返回响应。这让我们可以自由的实施自己的策略,来获取缓存数据或网络内容。当您尝试为自己的 Web 应用实现最佳缓存时,您可以使用几种基本的缓存策略。

Mozilla 有一个 方便的资源 记录了几种不同的缓存策略。另外,Jake Archibald 编写的一篇 离线手册 也介绍了同样的缓存策略,以及更多。

在上面的例子中,我们演示了一个基本的 缓存优先 策略。下面是在我自己的项目工程中适用的一个例子:缓存更新 策略。这个方法将优先响应缓存数据,随后在后台发送一个网络请求;后台请求的响应被用来更新缓存数据,因此,在接下来的请求中,更新后的缓存数据能够被访问到。

代码语言:javascript
复制
self.addEventListener('fetch', event => {  
  const { request } = event;

  event.respondWith(caches.open(CACHE_NAME)
    .then(cache => cache.match(request))
    .then(matching => matching || fetch(request)));

  event.waitUntil(caches.open(CACHE_NAME)
    .then(cache => fetch(request)
      .then(response => cache.put(request, response))));
});

event.respondWith 用来提供请求响应。我们从缓存中获取匹配的数据,如果不存在,我们将会从网络上获取。

接着我们调用 event.waitUntil,允许一个异步的 Promise 在 Service Worker 上下文结束之前作出决策,然后缓存响应。一旦这个异步操作完成, waitUntil 将返回并终止操作。

Activate 事件

Activate 事件少有文档说明。但是在你更新 Service Worker 文件并需要执行清理的时候,或在维护前一版本的 Service Worker 的时候,它将显得尤为重要。

当您更新 Service Worker 文件 ( /sw.js) 时,浏览器会在开发者工具中显示文件变化:

新的 Service Worker 是 '等待激活' 状态。

当网页关闭,然后重新打开,浏览器会将旧的 Service Worker 替换成新的,然后触发 Activate 事件,在 Install 事件之后。如果你需要清除缓存、或维护老版本的 Service Worker,Activate 事件是最好的时机。

Sync 事件

Sync 事件可以让网络任务延时,直到用户连通;该功能的实现通常被称为 后台同步。对于确保在离线模式下,用户启动的任何与网络有关的任务,最终将在网络可用时重新工作,这是非常有帮助的。

这里是一个关于 后台同步 的实现看起来的样子。您需要在前置 JS 中编写注册同步事件的代码,同时在 Service Worker 中处理同步事件:

代码语言:javascript
复制
// app.js
navigator.serviceWorker.ready  
  .then(registration => {
    document.getElementById('submit').addEventListener('click', () => {
      registration.sync.register('submit').then(() => {
        console.log('sync registered!');
      });
    });
  });

在这里,我们给一个按钮绑定了点击事件。点击按钮时,会调用 ServiceWorkerRegistration 对象上的 sync.register 方法。

基本上,任何需要确保连接网络的操作,不管是即时操作还是网络离线后最终恢复的情况,都需要作为 sync 事件注册。

例如提交评论、或者是获取用户数据之类的操作,都需要在 Service Worker 的事件处理中定义:

代码语言:javascript
复制
// sw.js
self.addEventListener('sync', event => {  
  if (event.tag === 'submit') {
    console.log('sync!');
  }
});

我们在这监听同步事件,并检查 SyncEvent 对象上的 tag,来确定是否是给点击的 'submit' 事件。

如果 'submit' 标记的事件被注册了多次,同步事件处理只会被执行一次。

对于这个例子来说,如果用户离线,然后点击了 7 次按钮,当网络恢复的时候,所有的同步注册都会被合并,且同步事件只会触发一次。

在这个例子中,如果您想要分别同步每一次点击事件,需要给每个同步注册绑定唯一的标签。

Sync 事件什么时候触发?

如果用户在线,则 sync 事件会立即触发,并毫无延迟的完成您所定义的任务。

如果用户离线,sync 事件会在网络连接恢复的时候尽快触发。

如果您跟我一样,想要在 Chrome 中试试,请确保网络已完全断开。在 Chome DevTools 中通过切换 Network 选择框,并不会触发 sync 事件。

获取更多信息,您可以阅读这篇 说明文档,以及这个 后台同步介绍。sync 事件还未被广泛支持(在写这篇文章时,还只有 Chrome 支持),而且一定会经历很多变化,所以请保持关注。

推送通知

推送通知是 Service Worker 通过将 push 事件暴露给 Service Workers 的功能,另外 Push API 是由浏览器来实现的。

当讨论 Web 推送通知的时候,实际上涉及到俩种技术:'通知' 和 '推送消息'。

通知

通知功能在 Service Workers 中可以很简单的实现:

代码语言:javascript
复制
// 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!');
      });
  }
}
代码语言:javascript
复制
// sw.js
self.addEventListener('notificationclick', event => {  
  // notification click event
});

self.addEventListener('notificationclose', event => {  
  // notification closed event
});

首先,需要通过网页获得用户对通知的许可;之后就可以切换通知了,并能处理某些事件,例如:通知是否被用户关闭。

推送消息

推送消息涉及到调用由浏览器提供的 Push Api,再加上后端实现。Push Api 的实现需要通过单独一篇文章来讲解,但是基本概念如下图所示:

这个过程稍微有点复杂,而且不在本文的讨论范围。但是如果您想了解更多,这篇 推送通知介绍 会有所帮助。

使用 Ember.js 实现

在 Ember 应用中实现 Service Workers 难以置信的容易。得益于 ember-cli 和 Ember Add-ons 社区,您可以 “即插即用” 的方式在您的网页应用上实现 Service Workers。

这是 ember-service-worker 插件的功劳,该插件由 DockYard 团队成员提供(相关文档 在这)。

ember-service-worker 建立了一个模块化的架构,可以引入其它 ember-service-worker 系列插件,例如 ember-service-worker-index 或 ember-service-worker-asset-cache。这些插件实现了不同的行为和缓存策略,最终完成了 Service Worker 这个角色。

了解 ember-service-worker 约定

所有的 ember-service-worker- 插件都遵循一个约定,就是它们的核心逻辑储存在插件根目录下的两个文件夹中, /service-worker/service-worker-registration:

代码语言:javascript
复制
node_modules/ember-service-worker  
├── ...
├── package.json
├── service-worker
    └── index.js
└── service-worker-registration
    └── index.js

/service-worker 文件夹,是 Service Worker 主要实现的地方(要保存的东西在前面的 sw.js 有展示)。

/service-worker-registration 下的是您需要在前置代码中运行的逻辑,Service Worker 的注册会在这里进行。

我们通过 /service-worker 来查看 ember-service-worker-index (代码) 的实现,揭露它到底做了些啥:

代码语言:javascript
复制
import {  
  INDEX_HTML_PATH,
  VERSION,
  INDEX_EXCLUDE_SCOPE
} from 'ember-service-worker-index/service-worker/config';

import { urlMatchesAnyPattern } from 'ember-service-worker/service-worker/url-utils';  
import cleanupCaches from 'ember-service-worker/service-worker/cleanup-caches';

const CACHE_KEY_PREFIX = 'esw-index';  
const CACHE_NAME = `${CACHE_KEY_PREFIX}-${VERSION}`;

const INDEX_HTML_URL = new URL(INDEX_HTML_PATH, self.location).toString();

self.addEventListener('install', (event) => {  
  event.waitUntil(
    fetch(INDEX_HTML_URL, { credentials: 'include' }).then((response) => {
      return caches
        .open(CACHE_NAME)
        .then((cache) => cache.put(INDEX_HTML_URL, response));
    })
  );
});

self.addEventListener('activate', (event) => {  
  event.waitUntil(cleanupCaches(CACHE_KEY_PREFIX, CACHE_NAME));
});

self.addEventListener('fetch', (event) => {  
  let request = event.request;
  let isGETRequest = request.method === 'GET';
  let isHTMLRequest = request.headers.get('accept').indexOf('text/html') !== -1;
  let isLocal = new URL(request.url).origin === location.origin;
  let scopeExcluded = urlMatchesAnyPattern(request.url, INDEX_EXCLUDE_SCOPE);

  if (isGETRequest && isHTMLRequest && isLocal && !scopeExcluded) {
    event.respondWith(
      caches.match(INDEX_HTML_URL, { cacheName: CACHE_NAME })
    );
  }
});

抛开细节,可以看到这份代码基本实现了我们之前提到的三个事件的处理: installactivatefetch

install 事件的处理中,我们请求了 INDEX_HTML_URL,然后调用 cache.put 方法来缓存响应。

activate 事件中做了一些基本的清理工作。

fetch 事件的处理中,我们检查了 request 是否满足一些条件(是否是 GET 请求、是否请求的 HTML 内容;是否来源于当前路径等);如果满足这些条件,就返回缓存中的内容。

注意,我们使用 cache.match 方法和 INDEX_HTML_URL 查找数据,而不是 request.url,表示我们只通过 key 查找缓存,而不用关心当前的路径。

是因为, Ember 应用总是使用 index.html 渲染。在应用程序的根路径下的任何 URL 请求,都会以 index.html 的缓存版本结尾,Ember 应用通常在此接管。这就是 ember-service-worker-index 的目的 - 缓存 index.html

类似的,ember-service-worker-asset-cache 将通过自己的 installfetch 事件处理,来缓存 /assets 目录下的所有文件。

这里有 一些插件 使用了 ember-service-worker 架构,并允许您定制和微调 Service Worker 的行为和缓存策略。

构建 Ember 应用实现 Service Workers

首先,你需要安装 ember-cli。通过执行以下命令:

代码语言:javascript
复制
$ ember new new-app
$ cd new-app
$ ember install ember-service-worker
$ ember install ember-service-worker-index
$ ember install ember-service-worker-asset-cache

您的应用现在由 Service Workers 提供服务,并默认生成了 index.html/assets/**/* 缓存文件。

您可以通过 config/environment.js 调整 /assets 目录下哪些文件会被缓存。

如果发现已有的 ember-service-worker 插件不能很好的满足需求,您可以创建自己的插件,具体请查看 ember-service-worker 网站上的官方文档。

结论

希望您已经透彻的理解了 Service Workers 和它的底层架构,以及 web 应用该怎样利用它给用户代来更好的体验。

ember-service-worker 插件让 Ember.js web 应用很方便的实施 Service Worker;如果您需要有自己的 Service Worker 逻辑,自己编写一个插件来管理事件行为会更容易一些。这是我将来要解决的事情,敬请关注!

本文参与 腾讯云自媒体分享计划,分享自微信公众号。
原始发表:2017-10-15,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 京程一灯 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 背景
  • 注册
  • 事件处理
    • Install 事件
      • Fetch 事件
        • Activate 事件
          • Sync 事件
            • 推送通知
            • 使用 Ember.js 实现
              • 了解 ember-service-worker 约定
                • 构建 Ember 应用实现 Service Workers
                • 结论
                相关产品与服务
                云开发 CLI 工具
                云开发 CLI 工具(Cloudbase CLI Devtools,CCLID)是云开发官方指定的 CLI 工具,可以帮助开发者快速构建 Serverless 应用。CLI 工具提供能力包括文件储存的管理、云函数的部署、模板项目的创建、HTTP Service、静态网站托管等,您可以专注于编码,无需在平台中切换各类配置。
                领券
                问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档