如何構建通用儲存中間層

佯真愚發表於2018-11-20

零、問題的由來

開門見山地說,這篇文章【又】是一篇安利軟文~,安利的物件就是 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

相關文章