如何构建通用存储中间层

javascript/jquery

浏览数:297

2018-11-21

AD:资源代下载服务

零、问题的由来

开门见山地说,这篇文章【又】是一篇安利软文~,安利的对象就是 tua-storage

顾名思义,这就是一款存储数据的工具。

用 tua-storage 好处大大的有么?

那必须滴~,下面开始我的表演~

  • 多端统一 api
  • 支持数据同步
  • 数据过期逻辑
  • 自动清理过期数据
  • 支持永久保存
  • 支持批量操作

一、多端统一 api

日常开发中,在不同的平台下由于有不同的存储层接口,所以往往导致相同逻辑的同一份代码要写几份儿。

例如,小程序中保存数据要使用【异步】的 wx.setStoragewx.getStorage 或对应的同步方法;

而在 web 端使用 localStorage 的话,则是【同步】的 setItemgetItem 等方法;

在 React-Native 的场景下,使用的又是 AsyncStorage 中【异步】的 setItemgetItem

1.1.异步方法

然而,经过 tua-storage 的二次封装,以上两个方法统一变成了:

  • save: 异步保存
  • load: 异步读取

此外还有一些其他方法:

  • clear: 异步清除(删除多个)
  • remove: 异步删除(删除单个)
  • getInfo: 异步获取信息(如 keys

详情参阅这里的文档

1.2.同步方法

在某些场景下正好需要调用同步方法的话,咋办咧?

与 Node.js 的 api 风格差不多,在上述异步方法后面加上 Sync 就是对应的同步方法:

  • saveSync
  • loadSync
  • clearSync
  • removeSync
  • getInfoSync

那么在 AsyncStorage 的场景下,压根就没有同步方法时调用以上方法会怎么样呢?

嗯,你猜得没错,会直接报错…

1.3.区分场景

如何区分不同的场景呢?

在初始化的时候传递 storageEngine 即可:

import TuaStorage from 'tua-storage'

const tuaStorage = new TuaStorage({
    // 小程序
    storageEngine: wx,

    // web
    storageEngine: localStorage,

    // React-Native
    storageEngine: AsyncStorage,

    // Node.js
    storageEngine: {},
})

注意:传递的是【对象】,而非字符串!

二、支持数据同步

对于一个二次封装多端存储层的库来说,保证多端 api 的统一仅仅是常规操作而已。

tua-storage 的另一大亮点就是数据同步功能。

想想平时我们是怎么使用存储层的

  • 读取一个数据
  • 正好存储层里有这个数据

    • 返回数据(皆大欢喜,happy ending~)
  • 假如存储层里没这个数据

    • 手动调用各种方法去同步这个数据
    • 手动存到存储层中,以便下次读取

各位有没有看出其中麻烦的地方在哪儿?数据同步部分的复杂度全留给了业务侧。

让我们回归这件事的【初心】:我仅仅需要获取这个数据!我不管它是来自存储层、来自接口数据、还是来自其他什么地方…

2.1.数据同步函数

因此 tua-storage 在读取数据时很贴心地提供了一个 syncFn 参数,作为数据同步的函数,当请求的数据不存在或已过期时自动调用该函数。并且数据同步后默认会保存下来,这样下次再请求时存储层中就有数据了。

syncParams 的使用场景是接口需要传参时,这些参数会传给 syncFn

tuaStorage.load({
    key: 'some data',
    syncFn: ({ a }) => axios('some api url' + a),
    // 以下参数会传到 syncFn 中
    syncParams: { a: 'a' },
})

这么一来,存储层就和接口层对接起来了。业务侧再也不用手动调用 api 获取数据。

2.2.合并分散配置

每次读取数据时如果都要手动传同步函数,实际编码时还是很麻烦…

不急,吃口药~

tua-storage 在初始化时能够传递一个叫做 syncFnMap 参数。顾名思义,这是一个将 keysyncFn 映射起来的对象。

const tuaStorage = new TuaStorage({
    // ...
    syncFnMap: {
        'data one': () => axios('data one api'),
        'data two': () => axios('data two api'),
        // ...
    },
})

// 不用手动传 syncFn,默认匹配 syncFnMap 中的对应函数
tuaStorage.load({ key: 'data one' })

2.3.自动生成配置

其实手动编写每个 api 请求函数也是很繁琐的,要是有个根据配置自动生成请求函数的库就好了~

诶~,巧了么不是~。各位开发者老爷们了解一下同样跨平台的 tua-api ~?

tua-storage 搭配 tua-api 之后会变成这样

import TuaStorage from 'tua-storage'
import { getSyncFnMapByApis } from 'tua-api'

// 本地写好的各种接口配置
import * as apis from '@/apis'

const tuaStorage = new TuaStorage({
    syncFnMap: getSyncFnMapByApis(apis),
})

三、数据过期逻辑

一般各个平台的存储层都没有数据过期这一逻辑。但在使用 tua-storage 时默认每个数据都有过期时间这一属性。

3.1.默认过期时间

默认为 30 秒,可以在初始化时配置默认超时时间。

import TuaStorage from 'tua-storage'

const tuaStorage = new TuaStorage({
    // 改为 60 秒
    defaultExpires: 60,
})

// 返回一个 Promise
tuaStorage
    .save({
        key: 'data key',
        data: { foo: 'bar' },

        // 这里传递的过期时间优先级更高
        expires: 90,
    })
    .then(console.log)
    .catch(console.error)


// 保存到 storage 中的数据大概长这样
// key 之前会加上初始化传入的默认前缀
{
    'TUA_STORAGE_PREFIX: data key': {
        expires: 90,
        rawData: { foo: 'bar' },
    },
}

3.2.数据保存前缀

为了保证存在 storage 中的数据名称不冲突,以及实现版本控制,tua-storage 默认有一个存储前缀:storageKeyPrefix

默认值为 TUA_STORAGE_PREFIX: ,所以在上一小节中保存的数据会有一个奇怪的前缀。

保证名称不冲突很好理解,如何实现版本控制呢?

3.3.白名单机制

clear 函数能够接受一个白名单数组(因为内部是通过 indexOf 来判断的,所以不必填写完整的 key 值)。

import TuaStorage from 'tua-storage'

const tuaStorage = new TuaStorage({ ... })

tuaStorage.clear(['key'])
    .then(console.log)
    .catch(console.error)

// 假设现在 storage 中有以下数据
{
    'foo': {},
    'bar': {},
    'foo-key': {},
    'bar-key': {},
}

// 清除后剩下的数据是
{
    'foo-key': {},
    'bar-key': {},
}

所以在调用 clear 时,在白名单中传入新的存储前缀,即可实现删除上一版本数据的功能。

import TuaStorage from 'tua-storage'

// 上一版本的前缀
const prefix1 = 'STORAGE_PREFIX_V1.0: '

// 这一版本的前缀
const prefix2 = 'STORAGE_PREFIX_V1.1: '

const tuaStorage = new TuaStorage({
    // 将默认前缀切换成新版本的
    storageKeyPrefix: prefix2,
})

// 开始清除上个版本的数据
tuaStorage.clear([ prefix2 ])
    .then(console.log)
    .catch(console.error)

更多默认配置参阅这里的文档

四、自动清理过期数据

默认在启动时会进行一次过期数据清理(可以关闭),之后每过一段时间会再次清理。

什么样的数据会被清理呢?

4.1.清理逻辑

首先当然是清理已到过期时间的数据,即有一个属性为 expires 的数据,且当前时间已超过了该时间。

一旦遇到不满足格式的数据(非对象、没有 expires 属性)则跳过,这样就不会误清除其他程序保存的数据。

4.2.清理时间间隔

在初始化时可传入 autoClearTime 修改默认自动清理时间间隔。

默认为一分钟,注意是以秒为单位。

五、支持永久保存

在某些场景下,可能不方便写过期时间,这时默认可以传递 expires: null,标记该数据永不过期。

不喜欢用 null 标记?

大丈夫~,初始化时传递 neverExpireMark 即可修改为你喜欢的别的标记。

import TuaStorage from 'tua-storage'

const tuaStorage = new TuaStorage({
    neverExpireMark: 'never',
})

// 永不过期
tuaStorage.save({
    key: 'some key',
    data: 'some data',
    expires: 'never',
})

六、支持批量操作

假设现在有一组数据需要保存或读取,常规操作就是使用 Promise.all 发起多个操作。

import TuaStorage from 'tua-storage'

const tuaStorage = new TuaStorage({ ... })

const dataToBeSaved = [
    { key: 'key one', data: 'some data' },
    { key: 'key two', data: 'some data' },
]

// 异步
const result = dataToBeSaved
    .map(tuaStorage.save.bind(tuaStorage))
    .then(Promise.all.bind(Promise))

// 同步
const result = dataToBeSaved
    .map(tuaStorage.saveSync.bind(tuaStorage))

讲道理这样写还是挺烦的…所以 tua-storage 的各个 api 还支持直接传入数组:

// 异步
tuaStorage.save(dataToBeSaved)
    .then(console.log)
    .catch(console.log)

// 同步
tuaStorage.saveSync(dataToBeSaved)

七、小结

还在为 web 端、小程序端、React-Native 端、node 端业务侧代码使用不一样的方式调用存储层烦恼么?还在为手动数据同步,保存数据,处理过期逻辑而烦躁么?各位开发者老爷们不妨试一试 tua-storage,(挤需体验三番钟,里造会干我一样,爱象介款工具)。

灵感来源

inspired by react-native-storage

原文地址:https://segmentfault.com/a/1190000017080687