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-simple-environment/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
即可。
$ npm install -g anywhere
$ anywhere 7890 # http://localhost:7890/
编写一个index.html
文件和sw.js
文件,以及引入相关的资源文件,目录结构如下,可以参考https://github.com/WindrunnerMax/webpack-simple-environment/tree/simple--service-worker
,当然直接clone
下来运行一个静态文件服务器就可以直接使用了。
simple--service-worker
├── static
│ ├── avatar.png
│ └── cache.js
├── index.html
└── sw.js
在html
中引入相关文件即可,主要是为了借助浏览器环境,而关注的位置是js
。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Service Worker</title>
<style type="text/css">
.avatar{
50px;
height: 50px;
border-radius: 50px;
}
</style>
</head>
<body>
<img class="avatar" src="./static/avatar.png">
<script type="text/javascript">
navigator.serviceWorker
.register("sw.js")
.then(() => {
console.info("注册成功");
})
.catch(() => {
console.error("注册失败");
});
</script>
<script src="./static/cache.js"></script>
</body>
</html>
使用Service worker
的第一步,就是告诉浏览器,需要注册一个Service worker
脚本,在这里我们直接将其写到了index.html
文件中了。默认情况下,Service worker
只对根目录/
生效,如果要改变生效范围可以在register
时加入第二个参数{ scope: "/xxx"}
,也可以直接在注册的时候就指定路径/xxx/sw.js
。
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
中会显示一个小小的齿轮图标,很好辨认。
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
也就是版本的控制,之前的版本就要清理等,另外还查看了一下目前的相关缓存。
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
去请求新的。另外如果有需要的话我们不需要在事件响应时进行匹配 可以直接将所有发起过的请求缓存。
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-simple-environment/tree/simple--service-worker
中clone
下来后运行这个示例。
<!-- index.html -->
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Service Worker</title>
<style type="text/css">
.avatar{
50px;
height: 50px;
border-radius: 50px;
}
</style>
</head>
<body>
<img class="avatar" src="./static/avatar.png">
<script type="text/javascript">
navigator.serviceWorker
.register("sw.js")
.then(() => {
console.info("注册成功");
})
.catch(() => {
console.error("注册失败");
});
</script>
<script src="./static/cache.js"></script>
</body>
</html>
// 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;
// });
// });
}
})
);
}
});
// 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