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 语法糖,序列化,监听订阅,过期设置等功能。

语法糖

javascript 复制代码
import { createProxyStorage } from 'stokado'

const storage = createProxyStorage(localStorage)

storage.test = 'hello stokado'

storage.test // 'hello stokado'

delete storage.test

当然,原生的 localStorage 和 sessionStorage 对象本身就支持对象写法,这里更多是简化 类 storage 对象的 getItem 和 setItem 等操作。

序列化

ini 复制代码
// 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 实现的,我的实现方式启发于 vueuseuseStorage ,修改了部分类型的 read 和 write。

监听订阅

vbnet 复制代码
storage.on(key, callback)

storage.once(key, callback)

storage.off([[key], callback])

vue 经典面试题:如何实现简单的监听订阅?

设置过期

scss 复制代码
storage.setExpires(key, expires)

storage.getExpires(key)

storage.removeExpires(key)

这也是大家通常遇到的问题,掘金也有很多相关文章。

一次性取值

scss 复制代码
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中发送消息:

ini 复制代码
var channel = new BroadcastChannel('myChannel');
channel.postMessage('Hello from page A!');

在页面B中接收消息:

ini 复制代码
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 原生的 getsetdeleteProperty。因为要兼容 localForage,改成调用对应的 getItemsetItemremoveItem等方法来统一协调 storage 对象。

当然,这是小问题,只要改调用方式就行,问题在于 localForage 的 API 都是异步的,所以需要 async/await 配合使用。

javascript 复制代码
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')

你以为结束了吗?

当然不,前面说了是与 监听订阅 相关的问题,所以正主还没出场呢。请接着往下看。

因为 监听订阅 的缘故,在进行 setItemremoveItem 之前需要先调用 getItem 获取旧值。即 setItem 方法内部实际逻辑是 getItem(oldValue) then setItem(newValue)

此时,有个问题就是在调用 setItem 后,马上调用 getItem,实际上运行时序为:getItem(oldValue) then setItem(newValue); getItem(oldValue)

是的,第二个 getItem 也拿到了 oldValue,因为 setItem 是在第一个 getItem 执行后才塞到 EventLoop 中,而第二个 getItem 是跟着第一个 getItem 加入到 EventLoop 队列中的,所以第二个 getItemsetItem 前面执行。

而期待的结果是:getItem(oldValue) then setItem(newValue) then getItem(newValue)

我的解决方法是以下逻辑:

scss 复制代码
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

你的认可,是我不断前进的动力。

另外,为自己打个广告,求个职:

  1. 全日制本科,4年+前端经验
  2. 热爱开源,有多个开源库输出,github地址
  3. 熟练运用 Vue 相关技术栈进行开发,对框架原理、运行机制有深入了解,可独立完成组件封装和 hooks 开发
  4. 喜欢探索学习新技术,有良好的文档编写和代码书写规范

详细简历可私信,深圳内推的朋友瞅瞅我有没有机会!

相关推荐
WeiXiao_Hyy38 分钟前
成为 Top 1% 的工程师
java·开发语言·javascript·经验分享·后端
吃杠碰小鸡1 小时前
高中数学-数列-导数证明
前端·数学·算法
kingwebo'sZone1 小时前
C#使用Aspose.Words把 word转成图片
前端·c#·word
xjt_09011 小时前
基于 Vue 3 构建企业级 Web Components 组件库
前端·javascript·vue.js
我是伪码农2 小时前
Vue 2.3
前端·javascript·vue.js
夜郎king2 小时前
HTML5 SVG 实现日出日落动画与实时天气可视化
前端·html5·svg 日出日落
辰风沐阳2 小时前
JavaScript 的宏任务和微任务
javascript
夏幻灵3 小时前
HTML5里最常用的十大标签
前端·html·html5
冰暮流星3 小时前
javascript之二重循环练习
开发语言·javascript·数据库
Mr Xu_3 小时前
Vue 3 中 watch 的使用详解:监听响应式数据变化的利器
前端·javascript·vue.js