likes
comments
collection
share

stokado拥抱localForage,轻松管理Web Storage和IndexedDB

作者站长头像
站长
· 阅读数 6

前情提要

故事起源于上个月在工作中用到了 localForage,发现用起来挺方便的,API 跟 Web Storage(sessionStorage 和 localStorage) 是一致的。巧了,我实现的开源库 stokado 正是通过 proxy 来代理这些 storage API 的,那么是不是可以 1 + 1 > 2

心动不如行动,vscode 启动。

stokado拥抱localForage,轻松管理Web Storage和IndexedDB

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 实现的,我的实现方式启发于 vueuseuseStorage ,修改了部分类型的 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(×)

stokado拥抱localForage,轻松管理Web Storage和IndexedDB

如图所示,兼容性不太行,尤其是移动端。

3. localStorage(×)

localStorage 可以说是众多方式中,页面通信兼容性最好的,实现也简单,可以通过监听 storageEvent 进行实时更新,简直无敌。

stokado拥抱localForage,轻松管理Web Storage和IndexedDB

但是,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 原生的 getsetdeleteProperty。因为要兼容 localForage,改成调用对应的 getItemsetItemremoveItem等方法来统一协调 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')

你以为结束了吗?

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

因为 监听订阅 的缘故,在进行 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)

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

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拥抱localForage,轻松管理Web Storage和IndexedDB

最后

希望大家可以尝试使用 stokado ,欢迎提供改进建议,顺手点个 🌟 最好不过。

兄弟姐妹,我想要 300 starstokado拥抱localForage,轻松管理Web Storage和IndexedDB

stokado拥抱localForage,轻松管理Web Storage和IndexedDB

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

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

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

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