# Service Worker的应用
`Service worker`本质上充当`Web`应用程序、浏览器与网络(可用时)之间的代理服务器,这个`API`旨在创建有效的离线体验,它会拦截网络请求并根据网络是否可用来采取适当的动作、更新来自服务器的的资源,它还提供入口以推送通知和访问后台同步`API`。
## 概述
`Service Worker`本质上也是浏览器缓存资源用的,只不过他不仅仅是`Cache`,也是通过`worker`的方式来进一步优化,其基于`h5`的`web worker`,所以不会阻碍当前`js`线程的执行,其最主要的工作原理,`1`是后台线程,是独立于当前网页线程,`2`是网络代理,在网页发起请求时代理拦截,来返回缓存的文件。简单来说`Service Worker`就是一个运行在后台的`Worker`线程,然后它会长期运行,充当一个服务,很适合那些不需要独立的资源数据或用户互动的功能,最常见用途就是拦截和处理网络请求,以下是一些细碎的描述:
* 基于`web worker`(一个独立于`JavaScript`主线程的独立线程,在里面执行需要消耗大量资源的操作不会堵塞主线程)。
* 在`web worker`的基础上增加了离线缓存的能力。
* 本质上充当`Web`应用程序(服务器)与浏览器之间的代理服务器(可以拦截全站的请求,并作出相应的动作`->`由开发者指定的动作)。
* 创建有效的离线体验(将一些不常更新的内容缓存在浏览器,提高访问体验)。
* 由事件驱动的,具有生命周期。
* 可以访问`cache`和`indexDB`。
* 支持推送。
* 可以让开发者自己控制管理缓存的内容以及版本。
`Service worker`还有一些其他的使用场景,以及`service worker`的标准能够用来做更多使`web`平台接近原生应用的事情:
* 后台数据同步。
* 响应来自其它源的资源请求。
* 集中接收计算成本高的数据更新,比如地理位置和陀螺仪信息,这样多个页面就可以利用同一组数据。
* 在客户端进行`CoffeeScript`、`LESS`、`CJS/AMD`等模块编译和依赖管理(用于开发目的)。
* 后台服务钩子。
* 自定义模板用于特定`URL`模式。性能增强,比如预取用户可能需要的资源,比如相册中的后面数张图片。
* 可以配合`App Manifest`和`Service Worker`来实现`PWA`的安装和离线等功能。
* 后台同步,启动一个`service worker`即使没有用户访问特定站点,也可以更新缓存。
* 响应推送,启动一个`service worker`向用户发送一条信息通知新的内容可用。
* 对时间或日期作出响应。
* 进入地理围栏(`LBS`的一种应用)。
## 示例
实现一个简单的`Service worker`应用示例,这个示例可以在断网的时候同样可以使用,相关的代码在`https://github.com/WindrunnerMax/webpack-env/tree/simple--service-worker`,在这里就是用原生的`Service Worker`写一个简单示例,直接写原生的`Service Worker`比较繁琐和复杂,所以可以借助一些库例如`Workbox`等,在使用`Service Worker`之前有一些注意事项:
* `Service worker`运行在`worker`上,也就表明其不能访问`DOM`。
* 其设计为完全异步,同步`API`(如`XHR`和`localStorage`)不能在`service worker`中使用。
* 出于安全考量,`Service workers`只能由`HTTPS`承载,`localhost`本地调试可以使用`http`。
* 在`Firefox`浏览器的用户隐私模式,`Service Worker`不可用。
* 其生命周期与页面无关(关联页面未关闭时,它也可以退出,没有关联页面时,它也可以启动)。
首先使用`Node`启动一个基础的`web`服务器,可以使用`anywhere`这个包,当然使用其他服务器都是可以的,执行完命令后访问`http://localhost:7890/`即可。另外写完相关代码后建议重启一下服务,之前我就遇到了无法缓存的问题,包括`disk cache`和`memory cache`,要重启服务才解决。还有要打开的链接为`localhost`,自动打开浏览器可能并不是`localhost`所以需要注意一下。如果要清理缓存的话,可以在浏览器控制台的`Application`项目中`Storage`点击`Clear site data`就能清理在网站中的所有缓存了。如果使用`express`或者`koa`等服务器环境,还可以尝试使用`Service Worker`来缓存数据请求,同样提供数据请求的`path`即可。
```shell
$ npm install -g anywhere
$ anywhere 7890 # http://localhost:7890/
```
编写一个`index.html`文件和`sw.js`文件,以及引入相关的资源文件,目录结构如下,可以参考`https://github.com/WindrunnerMax/webpack-env/tree/simple--service-worker`,当然直接`clone`下来运行一个静态文件服务器就可以直接使用了。
```
simple--service-worker
├── static
│ ├── avatar.png
│ └── cache.js
├── index.html
└── sw.js
```
在`html`中引入相关文件即可,主要是为了借助浏览器环境,而关注的位置是`js`。
```html
Service Worker
```
使用`Service worker`的第一步,就是告诉浏览器,需要注册一个`Service worker`脚本,在这里我们直接将其写到了`index.html`文件中了。默认情况下,`Service worker`只对根目录`/`生效,如果要改变生效范围可以在`register`时加入第二个参数`{ scope: "/xxx"}`,也可以直接在注册的时候就指定路径`/xxx/sw.js`。
```javascript
navigator.serviceWorker
.register("sw.js")
.then(() => {
console.info("注册成功")
}).catch(err => {
console.error("注册失败")
})
```
一旦登记成功,接下来都是`Service worker`脚本的工作,下面的代码都是写在`service worker`脚本里面的,登记后,就会触发`install`事件,`service worker`脚本需要监听这个事件。首先定义这个`cache`的名字,相当于是标识这一个缓存对象的键值,之后的`urlsToCache`数组是即将要缓存的数据,只要给定了相关的`path`,连数据请求也是同样能够缓存的,而不仅仅是资源文件,当然这边必须是`Get`的请求下使用,这是`Cache`这个`API`决定的。之后便是进行`install`,关于`event.waitUntil`可以理解为`new Promise`的作用,是要等待`serviceWorker`运行起来才继续后边的代码,其接受的实际参数只能是一个`Promise`。在`MDN`的解释是因为`oninstall`和`onactivate`完成前需要一些时间,`service worker`标准提供一个`waitUntil`方法,当`oninstall`或者`onactivate`触发时被调用,接受一个`promise`,在这个`promise`被成功`resolve`以前,功能性事件不会分发到`service worker`。之后便是从`caches`取出这个`CACHE_NAME`的`key`标识的`cache`,之后使用`cache.addAll`将数组中的`path`告诉`cache`,在第一次打开的时候,`Service worker`会自动去请求相关的数据并且缓存起来,使用`Service worker`去请求的数据,在`Chrome`控制台的`Network`中会显示一个小小的齿轮图标,很好辨认。
```javascript
const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];
this.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log("[Service Worker]", urlsToCache);
return cache.addAll(urlsToCache);
})
);
});
```
之后是`activated`阶段,如果是第一次加载`sw`,在安装后,会直接进入`activated`阶段,而如果`sw`进行更新,情况就会显得复杂一些,流程如下`:`首先老的`sw`为`A`,新的`sw`版本为`B, B`进入`install`阶段,而`A`还处于工作状态,所以`B`进入`waiting`阶段,只有等到`A`被`terminated`后,`B`才能正常替换`A`的工作。这个`terminated`的时机有如下几种方式,`1`、关闭浏览器一段时间。`2`、手动清除`Service Worker`。`3`、在`sw`安装时直接跳过`waiting`阶段。然后就进入了`activated`阶段,激活`sw`工作,`activated`阶段可以做很多有意义的事情,比如更新存储在`Cache`中的`key`和`value`。在下边的代码中,实现了不在白名单的`CACHE_NAME`就清理,可以在这里实现一个`version`也就是版本的控制,之前的版本就要清理等,另外还查看了一下目前的相关缓存。
```javascript
this.addEventListener("activate", event => {
// 不在白名单的`CACHE_NAME`就清理
const cacheWhitelist = ["service-worker-demo"];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
// 查看一下缓存
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
);
});
```
之后便是拦截请求的阶段了,该阶段是`sw`关键的一个阶段,用于拦截代理所有指定的请求,并进行对应的操作,所有的缓存部分,都是在该阶段。首先我们直接拦截掉所有的请求,在最前边的判断操作是为了防止所有的请求都被拦截从而都在`worker`里边发起请求,当然不进行判断也是可以使用的。然后对于请求如果匹配到了缓存,那么就直接从缓存中取得数据,否则就使用`fetch`去请求新的。另外如果有需要的话我们不需要在事件响应时进行匹配 可以直接将所有发起过的请求缓存。
```javascript
this.addEventListener("fetch", event => {
const url = new URL(event.request.url);
if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
event.respondWith(
caches.match(event.request).then(resp => {
if (resp) {
console.log("fetch ", event.request.url, "有缓存,从缓存中取");
return resp;
} else {
console.log("fetch ", event.request.url, "没有缓存,网络获取");
return fetch(event.request);
// // 如果有需要的话我们不需要在事件响应时进行匹配 可以直接将所有发起过的请求缓存
// return fetch(event.request).then(response => {
// return caches.open(CACHE_NAME).then(cache => {
// cache.put(event.request, response.clone());
// return response;
// });
// });
}
})
);
}
});
```
第一次打开时控制台的输出:
```
cache.js loaded
[Service Worker] (3) ['/', '/static/avatar.png', '/static/cache.js']
注册成功
(3) [Request, Request, Request]
```
第二次及之后打开的控制台输出:
```
fetch http://localhost:7811/static/avatar.png 有缓存,从缓存中取
fetch http://localhost:7811/static/cache.js 有缓存,从缓存中取
注册成功
cache.js loaded
```
至此我们就完成了一个简单的示例,在第二次打开页面的时候,我们可以将浏览器的网络连接断开,例如关闭文件服务器或者在控制台的`Network`中选择`Offline`,而我们也可以看到页面依旧正常加载,不需要网络服务,另外也可以在`Network`的相关的数据的`Size`列会出现`(ServiceWorker)`这个信息,说明资源是从`ServiceWorker`加载的缓存数据。可以在`https://github.com/WindrunnerMax/webpack-env/tree/simple--service-worker`中`clone`下来后运行这个示例。
```html
Service Worker
```
```javascript
// sw.js
const CACHE_NAME = "service-worker-demo";
const urlsToCache = ["/", "/static/avatar.png", "/static/cache.js"];
this.addEventListener("install", event => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => {
console.log("[Service Worker]", urlsToCache);
return cache.addAll(urlsToCache);
})
);
});
this.addEventListener("activate", event => {
// 不在白名单的`CACHE_NAME`就清理
const cacheWhitelist = ["service-worker-demo"];
event.waitUntil(
caches.keys().then(cacheNames => {
return Promise.all(
cacheNames.map(cacheName => {
if (cacheWhitelist.indexOf(cacheName) === -1) {
return caches.delete(cacheName);
}
})
);
})
);
// 查看一下缓存
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.keys().then(res => console.log(res)))
);
});
this.addEventListener("fetch", event => {
const url = new URL(event.request.url);
if (url.origin === location.origin && urlsToCache.indexOf(url.pathname) > -1) {
event.respondWith(
caches.match(event.request).then(resp => {
if (resp) {
console.log("fetch ", event.request.url, "有缓存,从缓存中取");
return resp;
} else {
console.log("fetch ", event.request.url, "没有缓存,网络获取");
return fetch(event.request);
// // 如果有需要的话我们不需要在事件响应时进行匹配 可以直接将所有发起过的请求缓存
// return fetch(event.request).then(response => {
// return caches.open(CACHE_NAME).then(cache => {
// cache.put(event.request, response.clone());
// return response;
// });
// });
}
})
);
}
});
```
```javascript
// cache.js
console.log("cache.js loaded");
// avatar.png
// [byte]png
```
## 每日一题
```
https://github.com/WindrunnerMax/EveryDay
```
## 参考
```
https://github.com/mdn/sw-test/
https://zhuanlan.zhihu.com/p/25459319
https://zhuanlan.zhihu.com/p/115243059
https://zhuanlan.zhihu.com/p/161204142
https://github.com/youngwind/service-worker-demo
https://mp.weixin.qq.com/s/3Ep5pJULvP7WHJvVJNDV-g
https://developer.mozilla.org/zh-CN/docs/Web/API/Cache
https://developer.mozilla.org/zh-CN/docs/Web/API/Service_Worker_API
https://www.bookstack.cn/read/webapi-tutorial/docs-service-worker.md
```