stokado拥抱localForage,轻松管理Web Storage和IndexedDB
前情提要
故事起源于上个月在工作中用到了 localForage
,发现用起来挺方便的,API 跟 Web Storage
(sessionStorage 和 localStorage) 是一致的。巧了,我实现的开源库 stokado 正是通过 proxy
来代理这些 storage API 的,那么是不是可以 1 + 1 > 2?
心动不如行动,vscode 启动。

stokado
首先,允许我简单介绍一下 stokado。
stokado, 借助 proxy 代理 storage,拦截 getItem,setItem 等方法,实现 getter/setter 语法糖,序列化,监听订阅,过期设置等功能。
语法糖
import { createProxyStorage } from 'stokado'
const storage = createProxyStorage(localStorage)
storage.test = 'hello stokado'
storage.test // 'hello stokado'
delete storage.test
当然,原生的 localStorage 和 sessionStorage 对象本身就支持对象写法,这里更多是简化 类 storage
对象的 getItem 和 setItem 等操作。
序列化
// number
storage.test = 0
storage.test === 0
// boolean
storage.test = false
storage.test === false
// object
storage.test = { hello: 'world' }
storage.test.hello === 'stokado'
// array
storage.test = ['hello']
storage.test.push('stokado')
storage.test.length // 2
// Date
storage.test = new Date('2000-01-01T00:00:00.000Z')
storage.test.getTime() === 946684800000
// function
storage.test = function () {
return 'hello stokado!'
}
storage.test() === 'hello stokado!'
localForage
也是支持序列化的,它是通过 JSON.stringify
实现的,我的实现方式启发于 vueuse
的 useStorage ,修改了部分类型的 read 和 write。
监听订阅
storage.on(key, callback)
storage.once(key, callback)
storage.off([[key], callback])
vue 经典面试题:如何实现简单的监听订阅?
设置过期
storage.setExpires(key, expires)
storage.getExpires(key)
storage.removeExpires(key)
一次性取值
storage.setDisposable(key)
这算是我的一个创新点,主要是用于想通过 localStorage 通信时,在另一页面取值后,还要进行删除,比较繁琐,所以设置一次性,即可取值后进行自动删除操作。
重构
此次重构主要有两个问题,都是与 监听订阅 功能相关。先从简单的说起:
跨页面同步
众所周知,localStorage 是可以跨页面读取的,所以 stokado v2 以前的版本是对 storageEvent
进行监听,从而触发各自页面的 storage.on
事件,但是对 localForage
兼容后, IndexedDB 是没有 storageEvent
事件的,也就是说:在A页面修改了 IndexedDB,B页面无法收到同步通知。
所以需要发送同步通信,经典面试题又来了:如何实现跨页面通信?
1. postMessage(×)
需要获取通信页面对应的 window 对象,这样调用 postMessage
,才会触发 message
事件。
2. SharedWorker(×)
如图所示,兼容性不太行,尤其是移动端。
3. localStorage(×)
localStorage 可以说是众多方式中,页面通信兼容性最好的,实现也简单,可以通过监听 storageEvent
进行实时更新,简直无敌。
但是,stokado 本来就是代理 storage 对象,通过 localStorage 通信,会污染它本身对象,从而容易触发其他意想不到的情况,所以 ×。
4. BroadcastChannel(√)
使用简单!只需订阅同一命名频道,即可在同一通道进行通信。
在页面A中发送消息:
var channel = new BroadcastChannel('myChannel');
channel.postMessage('Hello from page A!');
在页面B中接收消息:
var channel = new BroadcastChannel('myChannel');
channel.onmessage = function(event) {
console.log(event.data); // 输出: "Hello from page A!"
};
虽说兼容性没有 localStorage 好,但是基本上与 proxy
一致,也就是不支持 ie。:)
而 stokado 的核心本就是 proxy
,所以 BroadcastChannel
算是最佳选择了。
注意:在调用 createProxyStorage
时,需要传入 storageName 作为通道命名。
Async 异步时序
v2 之前的版本是在 proxy
代理拦截后,通过 Reflect
映射调用 storage 原生的 get
、set
、deleteProperty
。因为要兼容 localForage
,改成调用对应的 getItem
、setItem
、removeItem
等方法来统一协调 storage 对象。
当然,这是小问题,只要改调用方式就行,问题在于 localForage
的 API 都是异步的,所以需要 async/await
配合使用。
import { createProxyStorage } from 'stokado'
import localForage from 'localforage'
const local = createProxyStorage(localForage, 'localForage')
await (local.test = 'hello localForage')
// or
await local.setItem('test', 'hello localForage')
你以为结束了吗?
当然不,前面说了是与 监听订阅 相关的问题,所以正主还没出场呢。请接着往下看。
因为 监听订阅 的缘故,在进行 setItem
或 removeItem
之前需要先调用 getItem
获取旧值。即 setItem
方法内部实际逻辑是 getItem(oldValue) then setItem(newValue)
。
此时,有个问题就是在调用 setItem
后,马上调用 getItem
,实际上运行时序为:getItem(oldValue) then setItem(newValue); getItem(oldValue)
。
是的,第二个 getItem
也拿到了 oldValue,因为 setItem
是在第一个 getItem
执行后才塞到 EventLoop
中,而第二个 getItem
是跟着第一个 getItem
加入到 EventLoop
队列中的,所以第二个 getItem
在 setItem
前面执行。
而期待的结果是:getItem(oldValue) then setItem(newValue) then getItem(newValue)
。
我的解决方法是以下逻辑:
let prevPromise = Promise.resolve()
function pThen(getter: Function, callback: Function) {
const maybePromise = getter()
if (isPromise(maybePromise)) {
prevPromise = prevPromise.then(() => getter()).then((res) => {
prevPromise = Promise.resolve()
return callback(res)
})
return prevPromise
}
else {
return callback(maybePromise)
}
}
pThen(() => getItem(storage, property), () => {
storage.setItem(property, newValue)
})
pThen(() => getItem(storage, property), (res) => {
console.log(res)
})
通过 Promise
链式解决,核心思想为:prevPromise.then(() => getItem).then(() => setItem)
,再把结果重新赋值给 prevPromise,那么下一个进来的 getItem
,就会跟在 setItem
后面,完整链式为 Promise.resolve().then(() => getItem).then(() => setItem).then(() => getItem).then(() => callback)
。
但我觉得这不是最优解,期待在评论区见到大佬出手给个建议。
最后
希望大家可以尝试使用 stokado ,欢迎提供改进建议,顺手点个 🌟 最好不过。
兄弟姐妹,我想要 300 star,
你的认可,是我不断前进的动力。
另外,为自己打个广告,求个职:
- 全日制本科,4年+前端经验
- 热爱开源,有多个开源库输出,github地址
- 熟练运用 Vue 相关技术栈进行开发,对框架原理、运行机制有深入了解,可独立完成组件封装和 hooks 开发
- 喜欢探索学习新技术,有良好的文档编写和代码书写规范
详细简历可私信,深圳内推的朋友瞅瞅我有没有机会!
转载自:https://juejin.cn/post/7357546247848869926