[译] 理解 Service Workers

理解 Service Workers

什么是 Service Workers?他们可以作什么,怎样使你的 web app 表现得更好?本文旨在回答这些问题,以及如何使用 Ember.js 框架来实现他们。css

目录

背景

在互联网早期时代,几乎没人会考虑用户处于离线状态时该如何呈现一个 web 页面,只会考虑在线状态。html

Connected!
Connected!

链接上了!这帮家伙在这里!永远别想离开。前端

可是,随着移动互联网的到来以及网络在世界其余地区的普及,良莠不齐的网络质量在用户使用的现代网络中已经愈来愈广泛。node

所以,网站在离线状态时候的表现,以便用户不受网络可用性的限制,已变得很是有价值。react

AppCache 最初是做为 HTML5 规范的一部分引入,用以解决离线 web 应用程序的问题。它包含以 Cache Manifest 配置文件为中心的HTML和JS的组合,配置文件以声明式语言来编写。 android

AppCache 最终被发现是 不实用的和充满陷阱的。所以它已被废弃,被 Service Workers 有效的取代。ios

Service workers 提供了一个更具前瞻性的离线应用解决方案,经过更加程序化的语言书写规则替代 AppCache 的声明式书写方式。git

Service Workers 在浏览器后台进程中持续的执行其代码。它是事件驱动的,这意味着在 Service Worker 的做用域范围内触发的事件会驱动其行为。github

这篇文章剩下的部分将对 Service Worker 的每一个事件阶段作个简要的说明,可是在开始使用 Service Workers 以前,你首先须要在你的 web app 中执行代码来注册 Service Worker 。web

注册

下面的代码说明了怎样在你的客户端浏览器中注册你的 Service Worker,这是经过在你的 web app 前端代码的某一处执行 register 方法调用来实现的:

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 事件处理程序。

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

它也将设置你的 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 生命周期中触发实现对应的事件处理程序了。

安装事件

当你的 Service Worker 首次注册的时,或者你的 Service Worker 文件(/sw.js)在以后的任什么时候间被更新时(浏览器会自动检测这些更改),install 事件都将被触发。

对于那些你想在你的 Service Worker 初始化时执行的逻辑,install 事件是很是有用的,它能够执行一些一次性的操做,贯穿在整个 Service Worker 应用程序的生命周期中。一个常见的例子是在 install 阶段加载缓存。

下面是一个在 install 事件处理程序阶段向缓存添加数据的例子。

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,并向每一个 URL 发起一个请求,而后将响应存储在其缓存中。它使用请求体做为每一个缓存值的键名。了解更多请参阅 addAll

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

Fetch事件

Fetch 事件是在每次网页发出请求的时候触发的,触发该事件的时候 Service Worker 可以 '拦截' 请求,并决定返回内容 ———— 是返回缓存的数据,仍是返回真实请求响应的数据。

下面的例子说明了缓存优先的策略:与请求匹配的任何缓存数据都将优先被返回,而不须要发送网络请求。只有当没有现有的缓存数据时才会发出网络请求。

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 会 resolve 一个 undefined 值。在这个例子里,咱们经过判断这个值来决定是返回这个值,仍是调用 fetch 发起一个网络请求并返回一个 promise。

event.respondWith 是一个 FetchEvent 对象中的特殊方法,用于将请求的响应发送回浏览器。它接收一个对响应(或网络错误)resolve 后的 Promise 对象做为参数。

缓存策略

Fetch 事件特别重要,由于它可以定义你的缓存策略。也就是说,你能够决定什么时候使用缓存数据,什么时候使用网络请求来的数据。

Service Worker 的好用之处在于它是一个用于拦截请求的低层 API,并容许你决定为其提供哪些响应。这容许咱们自由的提供咱们本身的缓存策略或者网络来源的内容。当你尝试实现一个最好的 Web App 的时候,有几种基本的缓存策略可使用。

Mozilla 基金会有一个 handy resource 的文档,其中有写几种不一样的缓存策略。还有 Jake Archibald 编写的 The Offline Cookbook 书中有概述几种类似的缓存策略等等。

在上文的一个例子中,咱们演示了一个基本的缓存优先的策略。如下是我发现的一个适用于我本身项目的示例:缓存和更新策略。这个方法首先让缓存响应,随后在后台发起对应的网络请求。来自后台请求的响应用于更新缓存中的数据,以便在下次访问时提供更新后的响应。

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 方法以容许在 Service Worker 上下文终止以前 resolve 一个异步Promise。这里会走一个网络请求,而后缓存其响应。一旦这个异步操做完成,waitUntil 将会 resolve,操做将会终止。

激活事件

激活事件是一个较少记录的事件,但当你须要更新 Service Worker 文件,执行清理或者维护以前版本的 Service Worker 的时候,它是很是重要的。

当你更新你的 Service Worker 文件(/sw.js)的时候,浏览器会检测到这些改变,它们在 Chrome 开发者工具中的展现以下图所示:

你的新 Service Worker 正在“等待激活”。

当实际网页关闭并从新打开的时候,浏览器将使用新的 Service Worker 替换旧的 Service Worker,而后在 install 事件触发以后,触发 activate 事件,若是你须要清理缓存或者对旧版本的 Service Worker 进行维护,激活事件可让你完美的作到这一点。

同步事件

Sync 事件容许延迟网络任务,直到用户链接上网络,它实现的功能一般被称为后台同步。这对于在离线模式下,确保用户启动的任何有网络依赖的任务,最终都将在网络再次可用时达到其预期目的,是很是有用的。

下面是一个后台同步实现的例子。你须要在前端 JavaScript 中注册一个 sync 事件,并在 Service Worker 中附带 sync 事件处理程序。

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

在这里,咱们分配一个 click 事件给 button 元素,它将调用 ServiceWorkerRegistration 对象上的 sync.register 方法。

基本上,要确保任何操做均可以当即或最终在网络可用时到达网络,都须要被注册为 sync 事件。

在 Service Worker 的事件处理程序中,可能的操做像是发送一个评论,或者获取用户数据等等。

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

这里咱们监听一个 sync 事件,并检查 SyncEvent 对象上的 tag 属性属性是否匹配咱们指定给 click 事件的'submit'标签。

若是对应 'submit' 标签下的多个 sync 事件信息被注册,sync 事件处理程序将只执行一次。

所以,在这个例子中,若是用户离线,并点击了七次按钮,那么当网络恢复时,全部同步的注册事件将被合而且只触发一次。

在这种状况下,若是你想拆分同步事件给每一次点击,你能够注册多个具备惟一标记的同步事件。

何时同步事件被触发?

若是用户在线,则同步事件将会当即触发,并完成你定义的任何任务,而不会延时。

若是用户离线,则一旦从新得到网络链接,同步事件就会触发。

若是你像我同样,想在 Chrome 中尝试一下,必定要经过禁用 Wi-Fi 或者其余网络适配器来断开互联网链接。而在 Chrome 开发者工具中切换网络复选框不会触发 sync 事件。

想了解更多的信息,你能够阅读文档 this explainer document ,还有这篇文档 introduction to background syncs 。sync 事件如今在大部分浏览器当中并无实现(撰写本文时,只能在 Chrome 中使用),但势必在未来会发生变化,敬请期待。

通知推送

通知推送是 Service Workers 经过曝露其 push 以及浏览器实现的 Push API 来启用的功能。

当咱们讨论网络推送通知的时候,实际上会涉及两种对应的技术:通知和推送信息。

通知

通知是能够经过 Service Workers 实现的很是简单的功能:

// 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 => {
    // notification click event
});

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

你首先须要向用户发出许可才能启用网页的通知。从那时起,你能够切换通知,并处理某些事件,例如用户关闭一个通知的时候。

消息推送

推送消息涉及利用浏览器提供的 Push API 以及后端实现。这个要点能够单独抽出一篇文章详细讲解,可是其基本要点以下图所示:

Push API Diagram
Push API Diagram

这是一个稍微复杂的过程,超出了本文的范围。但若是你想了解更多,能够参考 introduction to push notifications 这篇文章 。

使用Ember.js实现

用 Ember.js 实现 Service Workers 的 APP 是很是容易的,凭借其脚手架工具 ember-cli 和其插件体系 Ember Add-ons 社区的支持,你能够以一种即插即拔的方式在你的 Web App 中增长 Service Worker。

这是由 DockYard 的人员提供的一系列插件 ember-service-worker 及其对应文档 here

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 and /service-worker-registration 这两个文件夹中。

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 的注册流程。

让咱们看看 ember-service-worker-index 该插件的 /service-worker 目录下的代码实现 (code here) ,符合上面所说的内容。

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 })
        );
    }
});复制代码

不去看具体的细节,咱们能够看到,这个代码基本实现了咱们以前讨论过的三个事件处理程序:install, activate and fetch

install 事件处理程序中,咱们调用 INDEX_HTML_URL对应的接口,获取数据,而后调用 cache.put 存储响应数据。

activate 阶段作了一些基本的清理缓存的操做。

fetch 事件处理程序中,咱们检查 request 是否知足几个条件(是不是 GET 请求,是否请求 HTML,是不是本地资源等等),只有知足一系列的条件,咱们才把对应的数据缓存返回。

注意咱们调用 cache.match方法 和 INDEX_HTML_URL 地址,来查找值,而不使用 request.url请求的 url。这意味着不管实际调用的 URL 请求是什么,咱们始终会根据相同的缓存密钥作对应的查找操做。

这是由于 Ember 的应用程序将始终使用 index.html 进行页面渲染。在应用程序的根路径下的任何 URL 请求都将以 index.html 的缓存版本结尾,Ember 应用程序一般会接管。这就是 ember-service-worker-index 来缓存index.html的目的。

一样的,ember-service-worker-asset-cache 该插件将缓存全部在 /assets 目录下能够找到的全部资源,文件,触发调用其 installfetch 事件处理函数。

有几个插件 several add-ons 也使用 ember-service-worker 该插件的结构,容许你自定义和微调对应的 Service Worker 的表现和缓存策略。

构建基于Ember和Service-Workers的App

首先,你须要下载 ember-cli,而后在命令行中执行下面的语句操做:

$ 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 插件没有解决你的问题,你能够参照这个文档 docs at the ember-service-worker website 建立你本身的插件。

结论

我但愿你可以对 Service Workers 和其底层架构有一个更深刻理解,以及怎样利用他们建立用户体验更好的Web App。

ember-service-worker 插件让你能在你的 Ember.js 应用程序中很容易地实现他们。若是你发现须要实现一个本身的 Service Worker 的逻辑,你能够很容易的建立本身的插件,来实现你须要的行为所对应的事件处理程序,这是我想在不久的未来解决的问题,敬请关注!

来自咱们的赞助商

若是你对基于 Ember.js 的全职工做感兴趣,Quartzy 正在招聘前端工程师!咱们帮助世界各地的科学家节省资金,使得他们更有效率的在实验室研究。点击这里申请吧。


掘金翻译计划 是一个翻译优质互联网技术文章的社区,文章来源为 掘金 上的英文分享文章。内容覆盖 AndroidiOSReact前端后端产品设计 等领域,想要查看更多优质译文请持续关注 掘金翻译计划官方微博知乎专栏

相关文章
相关标签/搜索