msn-cache
是最近弄的一个小工具,用于处理memory
,storage
,netword
数据,msn
也就是这三个单词的首字母。获取缓存数据处理过程的优先级是memory
=> storage
=> network
。至于怎样降级获取数据的详细处理,可以往下看一下
msn-cache 处理过程
在降级处理过程,主要有两个难点:
- cache 的算法处理
- storage数据处理
cache 的算法处理
目前支持的算法有LRU
与FIFO
那么怎么验证这个算法是正确的?LRU
算法相对比较复杂,这边是通过在leetcode提交验证,若通过leetcode的验证,则表明通过。leetcode 题目地址,但是leetcode暂时不支持ts语法,因此要把ts转换为js,再粘贴到leetcode运行。LRU
的运行效率对比同语言还算不错。FIFO
算法在leetcode没有找到相关题目,暂时只能手工测试,后续考虑加上单元测试。
简单使用例子:
1 2 3 4 5 6 7 8 9 10 11 12 |
import MCache from 'msn-cache'
const mc = new MCache({
name: 'LRU', // FIFO
capacity: 2,
storage: 'sessionStorage'
})
// get value from cache
mc.get('key', () => { /* request function */ })
// put new key-value in cache
mc.put('key', value) |
在实例化的时候,只需要指定cache算法的名称则可,而不同算法的具体实现对于实例调用mc.get
,mc.put
都是透明的。
算法说明:
storage处理
在工具的使用,可以选用localStorage
,sessionStorage
两种。由于这两种的存储方法不能存储对象类型的数据。在put data in storage
的过程中,会执行JSON.stringify(data)
方法对数据进行转换;而从storage
提取数据的过程,会执行JSON.parse(string)
方法转换:
1 2 3 4 5 6 |
let storage = 'sessionStorage'
let data = {
foo: 'foo'
}
window[storage].setItem(JSON.stringify(data))
data = JSON.parse(window[storage].getItem()) |
因此,如果数据无法转换,则不能够使用,例如: JSON.parse(undefined)
,不能转换成功;不过程序目前已针对undefined
的情况进行处理。
storage的注意点
localStorage
相对sessionStorage
很好理解,只要用户不删除,是一直存在的;但有时候缓存的数据会失效;而msn-cache
从localStorage
能够拿到旧数据,就不会发出新的请求更新数据;针对这种情况,目前提供clearStorage
用于清除存在storage
的数据,用户可以选择需要的时候,对storage
数据进行清理,具体API可以看API文档。
sessionStorage 的“会话”适用范围
sessionStorage
的定义是,在会话过程中,一直存在;那么怎么定义“会话”?
用户在初始状态: A;sessionStorage数据为:foo: 'foo'
- 用户在页面A按下刷新,属于同一个会话,此时能够拿到
foo: 'foo'
- 用户点击页面的超链接B,该链接新开tab打开了,此时B页面,能够拿到
foo: 'foo'
;原有属于A的session storage数据也带过去了; - 在B页面,新增数据
bar: 'bar'
;此时切换到A页面tab,A页面不能拿到bar: 'bar'
- 此时,用户在A页面,按住
ctrl
,点击超链接C,该链接从新tab打开,C页面不能拿到foo: 'foo'
- 此时,用户主动新开tab,同样打开A页面,命名为A2,A2页面不能获取
foo: 'foo'
注意,页面之间跳转均属于同一个域名
由此,我们可以知道sessionStorage
的规则:
- 在同一个tab刷新,属于同一个session,storage数据不会清除
- 用户正常操作(指不受组合键等处理新页面)打开新页面,新页面会把源页面的storage数据带过去;但新页面的storage数据不会同步到源页面
- 用户主动新开tab或者按住ctrl键等方式新开tab,不属于同一个session,相当于重新初始化
因此用户需要根据上述规则,需要根据情况是否对数据进行清除或初始化。
network
说了这么久,好像都没有跟network有关系?实际上msn-cache
确实没有对network
做缓存,只是把network
请求当成最后一个找不到缓存的获取途径,使用的方法如下:
1 2 3 4 |
let mc = new MCache({/* ... */})
mc.get('key', () => {
return fetch('/api/foo')
}) |
mc.get
方法第二参数是一个方法,如果从storage
也获取不到数据,就会执行该方法,获取该方法返回的值,作为mc.get
的数据。从network数据缓存,我们延伸到service worker
。
service worker
service worker是什么?
是worker的一种,不能操作DOM,与主线程的通信是利用postMessage
方法;能够拦截请求,配合cache storage
,能够做到离线缓存。
在应用部署service worker,需要应用使用https
,若在本地开发使用localhost
则不需要
下面看一个简单的例子:页面中注册一个service worker,这个sw在安装成功之后,主动缓存/cat.svg
;当页面请求/dog.svg
的时候,返回/cat.svg
。caches
是指cacheStorage
,与localStorage
这些属于storage
类型。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
// 例子1:
// index.js
navigator.serviceWorker.register('/sw.js').then(function(registration) {
console.log('register successful.')
}, function(err) {
console.log('register failed.', err)
});
setTimeout(() => {
let img = new Image()
img.src = '/dog.svg'
img.onload = () => {
document.getElementById('img').src = '/dog.svg'
}
}, 3000) |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
// sw.js
self.addEventListener('install', event => {
// sw 已安装
console.log('sw installed')
event.waitUntil(caches.open('v1').then(cache => {
cache.add('/cat.svg')
}))
})
self.addEventListener('activate', event => {
// sw 已激活
console.log('sw activated')
})
// 拦截fetch事件
self.addEventListener('fetch', event => {
const url = new URL(event.request.url);
// 拦截dog.svg请求,对这个请求返回cat.svg
if (url.pathname == '/dog.svg') {
event.respondWith(caches.match('/cat.svg'));
}
}); |
clients.claim
从上述例子中,当初始值进入页面的时候,经过3秒之后(假定sw已经安装并激活完毕),会去请求/dog.svg
,但是会发现,这个时候返回的图片,还是/dog.svg
原图,并不是我们在fetch
事件拦截,预计返回的/cat.svg
;而当我们重新刷新页面的时候,请求/dog.svg
才按照预计返回/cat.svg
内容。
这是因为第一次进入页面,sw激活完毕的时候,并没有马上拿到应用的控制权,在请求/dog.svg
中,请求并没有被拦截到,这是sw默认的处理方式;而如果第一次sw加载即马上控制页面,则需要调用clients.claim()
方法。
1 2 3 4 5 6 |
self.addEventListener('activate', event => {
// 主动获取控制权
clients.claim()
// sw 已激活
console.log('sw activated')
}) |
注:我看到很多人添加 clients.claim() 作为样板文件,但我自己很少这么做。该事件只是在首次加载时非常重要,由于渐进式增强,即使没有 Service Worker,页面也能顺利运行。
service worker 生命周期
在主线程监听到sw的变化:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
navigator.serviceWorker.register('/sw.js').then(registration => {
var serviceWorker;
if (registration.installing) {
console.log('init state is installing')
serviceWorker = registration.installing;
} else if (registration.installed) {
serviceWorker = registration.installed;
console.log('init state is installed')
} else if (registration.waiting) {
serviceWorker = registration.waiting;
console.log('init state is waiting')
} else if (registration.active) {
serviceWorker = registration.active;
console.log('init state is active')
}
serviceWorker.addEventListener('statechange', function (e) {
console.log('状态变化为', e.target.state)
});
}).catch(err => {
console.log('register sw error', err)
}) |
在主线程中,监听首次加载sw的状态变化为:
而在sw中,首次加载的主要事件变化是两个:
installed
sw已经安装成功,通常会在这个时候主动缓存资源,而且installed
事件在一个sw中,只会触发一次activate
sw 已经正常激活,准备好去处理fetch
等事件
那么,如果有需要更新到新的sw,状态是怎么发生变化?例如sw.js
资源发生变化,然后主动刷新页面,新的sw中的install
事件会进行触发,而activate
事件不会马上触发;那么再次刷新呢?还是不会触发新的sw的activate
事件,而在主线程的log中,发现初始状态为waiting
;无论刷新多少次,都是这样子...
事实上,如果sw对应的文件发生变化,旧的worker还是会继续控制浏览器;同时新的worker会进行加载,加载完毕后(sw线程的install
事件触发)不会把原有的控制权抢过去,而是处于waiting
,这个时候拿到的缓存,也是旧worker对应的缓存;fetch
事件的拦截也是旧worker处理。
等待旧的worker控制的所有页面都被关闭(是的,你没看错,是控制的所有页面),待下次打开该页面的时候,新的worker才会接收新的控制权;这样子的处理逻辑,是保证只有一个worker控制资源。
图片来源:https://developers.google.com/web/fundamentals/primers/service-workers/lifecycle
那么,如果真的有需求是需要获取到新的worker的时候,马上让新的worker控制所有缓存数据,怎么处理?
skipWaiting
使用skipWaiting()
方法跳过等待;从上面的描述可以知道,新的worker安装完毕之后,就会进入等待状态,当所有由旧worker控制的页面退出则接受控制;这个阶段,新的worker一直处于waiting
状态;
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
const cacheVersion = 'v2'
const urlCache = [
'/cat.svg',
'/dog.svg',
'/cow.svg'
]
self.addEventListener('install', event => {
// 跳过等待,安装完毕之后马上由新的worker控制
self.skipWaiting()
// 缓存新版本数据
event.waitUntil(caches.open('v2')
.then(cache => cache.addAll(urlCache)))
})
self.addEventListener('activate', event => {
// 新的worker激活完成
event.waitUntil(caches.keys().then(keys => {
return Promise.all(keys.map(item => {
// 删除旧版本数据
if (cacheVersion !== item) {
return caches.delete(item)
}
}))
}))
}) |