实现离线访问
service worker
如果已经访问过这个博客的话,再断网访问一次会发现文章依旧可以看。这就是缓存的力量。 本地缓存有
- http缓存
- web storage api
- service worker cache api 我这里使用的是service worker的cache api。
service worker
(简称为sw)可以当作一个在后台运行的进程,它不会阻塞浏览器进程,也不能访问浏览器的api(DOM
,localstorage
等),并且在你关闭页面甚至是浏览器时,它也可以继续运行,PWA的大部分能力都是通过sw
来实现的。sw
只能在https
域下或者localhost
域下运行。
使用sw
注册sw
首先需要在主js文件中注册sw
// index.js或者main.js
// 注册service worker,service worker脚本文件为sw.js
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('./sw.js').then(() => {
console.log('Service Worker 注册成功')
})
}
sw的生命周期
installing --> installed --> activating --> activated --> redundant sw
有一个全局变量,名为self
,相当于平时用的window
对象。self
引用当前这个sw
。 如果要监听这些事件的话,可以这样做:
self.addEventListener('activated', (event) => {
console.log('激活了')
})
缓存静态资源
// sw.js
// 缓存名
const CACHE_NAME = 'blog-cache-v1'
// 需要缓存的资源
const STATIC_ASSETS = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/main.js',
'/images/logo.png',
'/offline.html'
]
// 安装 Service Worker完成后进行缓存
self.addEventListener('install', (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => {
console.log('Opened cache')
return cache.addAll(STATIC_ASSETS)
})
)
})
Cache API
为Request
/Response
对提供了持久性存储。它提供了添加和删除Request
/Response
对的方法,以及查找匹配给定Request
的缓存Response
的方法。缓存在主应用线程和service worker
中都可用:所以一个线程可以添加一个响应,另一个线程可以检索它。 STATIC_ASSETS是需要缓存的静态资源,CACHE_NAME是给这份缓存取的名字。当sw安装完成后,就会使用将需要缓存的静态资源缓存下来。 这里只是实现了缓存,还需要让浏览器使用这份缓存。
使用缓存的资源
打开开发者工具,点进网络那一栏,然后刷新一下。会发现请求这些资源也只是get请求,只要当get这些资源时,改为从缓存get就可以了。 sw提供了拦截请求的功能,可以通过监听fetch事件来拦截。
// sw.js
self.addEventListener('fetch',(event)=>{
event.respondWith(
caches.match(event.request).then((cache)=>{
return cache||fetch(event.request)
}).catch(event){
return fetch(event.request)
}
)
})
这样就拦截了请求,然后在caches里找有没有缓存好的response,如果有,那就返回缓存好的response;如果没有的话,那就让它继续请求。 这样子的话就可以使用install的时候缓存好的资源了。
更新缓存
使用缓存完成了,但是还存在一个问题,除非手动清除缓存或者注销sw.js,不会再更新缓存了。
function cleanOldCaches() {
return caches.keys().then((cacheNames) => {
return Promise.all(
cacheNames.map((cacheName) => {
if (cacheName !== CACHE_NAME) {
console.log('Deleting old cache:', cacheName)
return caches.delete(cacheName)
}
})
)
})
}
function updateCachePeriodically() {
return caches.open(CACHE_NAME).then((cache) => {
return cache.keys().then((requests) => {
return Promise.all(
requests.map((request) => {
return cache.match(request).then((response) => {
if (response && response.headers.get('date')) {
const cacheDate = new Date(response.headers.get('date'))
if ((Date.now() - cacheDate.getTime()) > CACHE_PERIOD) {
return cache.delete(request)
}
}
})
})
)
})
})
}
// 激活 Service Worker
self.addEventListener('activate', (event) => {
event.waitUntil(
Promise.all([
cleanOldCaches(),
updateCachePeriodically()
])
)
})
cleanOldCaches用于删除旧的缓存以便新的缓存加进来,updateCachePeriodically用于删除过期的缓存。
对fetch缓存的进一步改造
在之前关于fetch的处理会发现,这里只对声明的静态文件进行了缓存,其实还可以更精细地控制缓存。
async function networkFirst(request) {
try {
const networkResponse = await fetch(request)
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME)
await cache.put(request, networkResponse.clone())
}
return networkResponse
}
catch (error) {
const cachedResponse = await caches.match(request)
if (cachedResponse) {
return cachedResponse
}
return new Response('Network error happened', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
})
}
}
async function cacheFirstWithRefresh(request) {
const cachedResponse = await caches.match(request)
const fetchPromise = fetch(request).then(async (networkResponse) => {
if (networkResponse.ok) {
const cache = await caches.open(CACHE_NAME)
await cache.put(request, networkResponse.clone())
}
return networkResponse
}).catch((error) => {
console.error('Fetching failed:', error)
return cachedResponse || new Response('Network error happened', {
status: 408,
headers: { 'Content-Type': 'text/plain' },
})
})
return cachedResponse || fetchPromise
}
self.addEventListener('fetch', (event) => {
const url = new URL(event.request.url)
if (event.request.mode === 'navigate' || url.pathname === '/') {
event.respondWith(networkFirst(event.request))
}
else {
event.respondWith(cacheFirstWithRefresh(event.request))
}
})
这里有两种策略,一种是网络优先,一种是带刷新的缓存优先。条件判断了是否为导航请求(点击链接或输入URL等)或者向根目录请求,如果是,那么网络优先;如果不是,那么缓存优先。
- 网络优先:优先从网络请求里读,如果网络请求失败再尝试从缓存里读。
- 缓存优先:优先从缓存里读,如果缓存里没有再尝试网络请求并更新缓存。
- 带刷新的缓存优先:优先从缓存读,即使缓存有也发送网络请求并更新缓存。 这份代码是本博客的sw.js截取出来,因为是博客,所以我希望文章是最新的。原本是对html的get请求使用网络优先,另一个只是缓存优先。但是我发现本博客没有请求获取html,而是通过js渲染页面的,最后改成了根路径网络优先,其他的为带刷新的缓存优先。