PWA系列
PWA系列 - Workbox插件InjectManifest
Service Worker 是什么
- SW 运行中 Web 浏览器和 Web 服务器之间,即是
中间代理
- SW 运行在 独立线程中,
不能操作DOM,可访问原生应用功能
- SW 的工作方式是,先安装(
install
) -> 启动(activate
) ->控制页面
- SW 可拦截http请求(
fetch
) - SW 很重要的功能是缓存(
Cache
),完全独立于 HTTP 缓存的缓存机制,浏览器http 缓存受 HTTP 标头中指定的缓存指令影响,SW 的Cache可自主编程 - SW 为了安全,只能通过
HTTPS 或 localhost
使用
新建一个 Service Worker
注册 sw.js 文件
监听页面load完成后,注册sw.js即可,register方法有两个参数
- 第一个是sw.js路径
- 第二个指明sw的作用范围,sw的范围默认是由页面位置决定的,如路径是 /static/sw.js ,那么范围就是/static和其子目录的html能被/static/sw.js控制。可设置对象
{scope: './'}
, sw.js只能控制scope范围内的页面。 - 注册的sw.js最好就放到根目录下,使得sw.js能控制所有页面
- scope范围是指定sw.js能控制的页面,但是http请求拦截却是可以拦截任意范围的,包括跨域的请求
window.addEventListener("load", function () {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js");
}
});
sw.js 中监听事件
- sw类似一个app应用,需要安装,启动打开;因为是中间代理,所以可以拦截http请求
- sw运行在独立线程中,self变量是全局对象
- sw默认第一次安装且启动后,不会立即拥有控制当前页面的权限,而是下次打开页面时才能控制页面,所以最好是启动后,立即让sw拥有控制权
- install事件会在register后执行,install只会第一次触发,sw.js更新后会触发,一把做预请求资源到缓存
- activate事件会在install后执行,install只会第一次触发,sw.js更新后会触发,一般做新旧缓存资源清理
- sw会重新安装,情况一般是三种,navigator.serviceWorker.register("/sw3.js", { scope: "./updata" }),
sw.js地址变了
,scope变了
,sw.js内容变了
self.addEventListener("install", async (event) => {
console.log("开始安装", event);
event.waitUntil(self.skipWaiting())
});
self.addEventListener("activate", async (event) => {
console.log("安装完成,开始启动", event);
event.waitUntil(self.clients.claim())
});
self.addEventListener("fetch", async (event) => {
console.log("运行中,拦截请求", event.request);
const url = new URL(event.request.url);
if (url.origin == location.origin && url.pathname == '/dog.svg') {
event.respondWith(caches.match('/cat.svg'));
}
});
sw.js与html通信
postMessage
html js中注册监听 与 发送消息
// 收到消息
navigator.serviceWorker.addEventListener("message",function (event) {
console.log("收到消息sw->html", event.data);
}
);
// 发送消息
navigator.serviceWorker.controller.postMessage({
action: "the action event",
});
sw.js 中注册监听 与 发送消息
self.addEventListener("message", function (event) {
// 收到消息
console.log("收到消息html->sw", event.data);
// 发送消息
this.self.clients.matchAll().then(function (clients) {
clients.forEach((client) => {
client.postMessage({
action: "response message",
});
});
});
});
fetch 拦截
- 利用fetch可以拦截的特性,html中发送api请求,sw拦截到后计算返回结果达到通信目的
比如: 需要让sw处理某些计算,可以发送一个请求logo.png(注意:这个logo.png需要真实存在),sw.js中拦截到后,处理某些业务再返回
html js中发送请求
fetch("/logo.png?action=redirect").then(async (res) => {
res = await res.json();
console.log(res);
});
sw.js 中拦截
self.addEventListener("fetch", async (event) => {
console.log("运行中,拦截请求", event.request);
const url = new URL(event.request.url);
if (
url.pathname == "/logo.png" &&
url.searchParams.get("action") == "redirect"
) {
// 拦截到后,处理业务再event.respondWith返回
request = new Request(
"http://wthrcdn.etouch.cn/weather_mini?citykey=101280101"
);
event.respondWith(fetch(request));
} else {
event.respondWith(fetch(event.request));
}
});
扩展延伸说明
install 事件的 event.waitUntil
event.waitUntil
是回调函数,接收Promise
对象,event.waitUntil(self.skipWaiting())
意思就是不等待,这句话也可不写,下面的setTimeout
等待10秒后,才会执行后续activate事件,如果waitUntil
得到reject,那么安装失败
self.addEventListener("install", async (event) => {
console.log("开始安装", event);
event.waitUntil(
new Promise((resolve) => {
setTimeout(resolve, 10000);
})
);
});
install 事件的 cache 做预请求
- sw最佳实际是与缓存结合,预请求资源写在install中,讲这些js和css预请求存在MyFancyCacheName_v1的缓存名中,
cache.addAll
返回Promise,等都请求到后表示install完成,才继续执行后续activate事件 - 预请求的文件会存放在service worker存储中,页面请求时,在常见http请求缓存时,文件来源是 Memory Cache, 而此时是 Service Worker
self.addEventListener("install", async (event) => {
const cacheKey = 'MyFancyCacheName_v1';
event.waitUntil(caches.open(cacheKey).then((cache) => {
return cache.addAll([
'/css/global.bc7b80b7.css',
'/css/home.fe5d0b23.css',
'/js/home.d3cc4ba4.js',
'/js/jquery.43ca4933.js'
]);
}));
});
activate 事件中立即接管控制页面
event.waitUntil(self.clients.claim())
写在activate事件事件中,意思是sw启动后,立即接管页面控制,因为默认是下一次页面打开后,sw能接管页面控制权限
self.addEventListener("activate", async (event) => {
console.log("安装完成,开始启动", event);
event.waitUntil(self.clients.claim())
});
activate 事件中更新清理缓存
sw.js修改后,浏览器会请求并重新install和activate,如果sw.js更新后,浏览器获得新sw,此时新sw安装后启动,但是默认并没有获得页面控制,页面还是被旧sw控制的,知道下次打开页面,除非执行self.clients.claim()
立即获得控制
self.addEventListener('activate', (event) => {
// 更改缓存名,原来是MyFancyCacheName_v1
const cacheAllowList = ['MyFancyCacheName_v2'];
// 获取所有缓存名
event.waitUntil(caches.keys().then((keys) => {
// 删除旧缓存
return Promise.all(keys.map((key) => {
if (!cacheAllowList.includes(key)) {
return caches.delete(key);
}
}));
}));
});
fetch的POST获取参数
html js中发送POST请求
fetch("/logo.png", {
method: "POST",
mode: "cors",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({
action: "dosomejob",
}),
}).then(async (res) => {
res = await res.json();
console.log(res);
});
sw.js中获取参数,但是event.respondWith(fetch(request))不可用了
self.addEventListener("fetch", async (event) => {
console.log("运行中,拦截请求", event.request);
const url = new URL(event.request.url);
if (url.pathname == "/logo.png" && event.request.method == "POST") {
const body = await event.request.clone().text();
console.log('body',body)
}
});
fetch拦截请求,做缓存策略
要理解到这里不是http缓存,是低等级的缓存依靠header头,而且js可编程的缓存,是高级的自定义的缓存
这里介绍五种常用缓存策略
:
Cache only (仅缓存)
所有资源都先在install
中预先请求,不然后续也不会缓存,而且资源永远不会更新,除非sw.js更新后重新安装
- 这种方式对于js、css、image等资源文件是很好的,因为现在的资源文件都打有hashcode,有更新会重新预缓存
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url);
const isPrecachedRequest = precachedAssets.includes(url.pathname);
// 判断是否有缓存
if (isPrecachedRequest) {
// 从缓存中取
event.respondWith(caches.open(cacheName).then((cache) => {
return cache.match(event.request.url);
}));
} else {
// 没有就从请求网络
return;
}
});
Network only (仅网络)
所有资源都不缓存,都从网络取
self.addEventListener('fetch', (event) => {
return;
});
Cache first (缓存优先)
- 先检查缓存中有没有,如果有就立即返回
- 如果缓存没有,就网络请求,并设置缓存
const cacheName = 'MyFancyCacheName_v1';
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(caches.open(cacheName).then((cache) => {
return cache.match(event.request.url).then((cachedResponse) => {
// 有就返回
if (cachedResponse) {
return cachedResponse;
}
// 没有就请求
return fetch(event.request).then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone());
return fetchedResponse;
});
});
}));
} else {
return;
}
});
Network first (网络优先)
- 每次都从网络加载数据,并设置到缓存
- 如果网络请求失败,就用上次的缓存数据
const cacheName = 'MyFancyCacheName_v1';
self.addEventListener('fetch', (event) => {
if (event.request.mode === 'navigate') {
event.respondWith(caches.open(cacheName).then((cache) => {
// 每次都网络请求
return fetch(event.request.url).then((fetchedResponse) => {
cache.put(event.request, fetchedResponse.clone());
return fetchedResponse;
}).catch(() => {
// 如果加载失败
return cache.match(event.request.url);
});
}));
} else {
return;
}
});
Stale-while-revalidate (过时验证缓存)
- 每次直接返回缓存数据,同时发起网络请求,将网络数据更新本地缓存
- 后续每次请求都相当于最近一次最新数据
const cacheName = 'MyFancyCacheName_v1';
self.addEventListener('fetch', (event) => {
if (event.request.destination === 'image') {
event.respondWith(caches.open(cacheName).then((cache) => {
return cache.match(event.request).then((cachedResponse) => {
const fetchedResponse = fetch(event.request).then((networkResponse) => {
cache.put(event.request, networkResponse.clone());
return networkResponse;
});
return cachedResponse || fetchedResponse;
});
}));
} else {
return;
}
});