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中,首次加载的主要事件变化是两个:

installedsw已经安装成功,通常会在这个时候主动缓存资源,而且installed事件在一个sw中,只会触发一次activatesw 已经正常激活,准备好去处理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)
}
}))
}))
}) |