Skip to content

本地缓存状态(useStorageMap)

一种更好的管理本地缓存状态解决方案

背景介绍

在项目开发中,通常需要保存一些不太重要的状态信息,例如用户是否已查看提示或已阅读某条消息,以便在下次进入应用时不再弹出提醒。通常,我们会将这些信息存储在 localStorage 中。

回想一下,我们会如何编写这种逻辑代码?大致是这样的:

vue
<script setup>
const showMessage = ref(uni.getStorageSync('viewed_message') || true)

const onCloseMessage = () => {
  showMessage.value = false

  uni.setStorageSync('viewed_message', false)
}
</script>

<template>
  ...
  <view v-if="showMessage" class="message" @click="onCloseMessage">
    message...
  </view>
</template>
<script setup>
const showMessage = ref(uni.getStorageSync('viewed_message') || true)

const onCloseMessage = () => {
  showMessage.value = false

  uni.setStorageSync('viewed_message', false)
}
</script>

<template>
  ...
  <view v-if="showMessage" class="message" @click="onCloseMessage">
    message...
  </view>
</template>

还有另外一种情况,我们需要记录的状态不仅仅是某个单一的状态,而是一系列的状态。举例来说,有一个应用,允许用户在不同的地区之间切换。初次进入某个地区时,需要显示欢迎信息,再次切换回相同地区时,就不再需要显示欢迎。

另一个例子是某个活动中,用户可以进行多次抽奖,有可能多次中奖。当用户有未读的中奖记录时,需要提醒,后面就不再需要提醒,但如果后续还有其他中奖记录,需要再次提醒。

在这种情况下,我们的代码就会变成类似这样:

js
const area = '广东'
const showMessage = ref(uni.getStorageSync(`viewed_welcome_${area}`) || false)
const area = '广东'
const showMessage = ref(uni.getStorageSync(`viewed_welcome_${area}`) || false)

为了更好地管理这些状态,通常会将一系列相关的状态放在同一个列表中。这种方法可以降低 localStorage 中的 key 数量,同时也有助于代码的维护。

js
const viewWelcomeList = uni.getStorageSync(`viewed_welcome_list`) || []
const area = '广东'
const showMessage = ref(viewWelcomeList.includes(area))

const onCloseMessage = () => {
  showMessage.value = false

  viewWelcomeList.push(area)

  // 还需避免数据重复
  uni.setStorageSync('viewed_welcome_list', [...new Set(viewWelcomeList)])
}
const viewWelcomeList = uni.getStorageSync(`viewed_welcome_list`) || []
const area = '广东'
const showMessage = ref(viewWelcomeList.includes(area))

const onCloseMessage = () => {
  showMessage.value = false

  viewWelcomeList.push(area)

  // 还需避免数据重复
  uni.setStorageSync('viewed_welcome_list', [...new Set(viewWelcomeList)])
}

但这样仍然非常麻烦,你应该能够想象,随着这种代码的增多,读取和更新状态都会变得非常难维护。

因此,useStorageMap 就应运而生,它能够很方便地帮助我们解决此类问题。

基本使用

读取和更新

对于前面那个地区切换的例子,我们只需要这么做:

js
import {useStorageMap} from '@ifanrx/uni-mp'

const area = '广东'
const viewAreaWelcome = useStorageMap('view_area_welcome')

const showMessage = computed(() => !viewAreaWelcome.get(area))

const onCloseMessage = () => {
  viewAreaWelcome.set(area)
}
import {useStorageMap} from '@ifanrx/uni-mp'

const area = '广东'
const viewAreaWelcome = useStorageMap('view_area_welcome')

const showMessage = computed(() => !viewAreaWelcome.get(area))

const onCloseMessage = () => {
  viewAreaWelcome.set(area)
}

如此一来,当我们调用 viewAreaWelcome.set 的时候,showMessage 就会自动更新。

如果情况再复杂点,需要精确到省市区,可以改成这样:

js
import {useStorageMap} from '@ifanrx/uni-mp'

// useStorageMap 内部会将数组合并成字符串作为每条记录的 key 
const area = ['广东', '广州', '海珠'] 
const viewAreaWelcome = useStorageMap('view_area_welcome')

const showMessage = computed(() => !viewAreaWelcome.get(area))

const onCloseMessage = () => {
  viewAreaWelcome.set(area)
}
import {useStorageMap} from '@ifanrx/uni-mp'

// useStorageMap 内部会将数组合并成字符串作为每条记录的 key 
const area = ['广东', '广州', '海珠'] 
const viewAreaWelcome = useStorageMap('view_area_welcome')

const showMessage = computed(() => !viewAreaWelcome.get(area))

const onCloseMessage = () => {
  viewAreaWelcome.set(area)
}

传入 value

如果需要记录更多状态,可以在 set 方法传入 value

js
const onCloseMessage = () => {
  viewAreaWelcome.set(area, {
    value: {
      viewed: true,
      ...其它信息,
    },
  })
}
const onCloseMessage = () => {
  viewAreaWelcome.set(area, {
    value: {
      viewed: true,
      ...其它信息,
    },
  })
}

过期时间

如果需要设定时间范围,比如一天之内只显示一次,我们也可以指定过期时间:

js
const onCloseMessage = () => {
  viewAreaWelcome.set(area, {
    value: {
      viewed: true,
      ...其它信息
    },
    // 绝对时间戳
    expiredAt: dayjs().add(1, 'day').unix(),

    // 或者用相对时间(毫秒)
    expiresIn: 7200
  })
}
const onCloseMessage = () => {
  viewAreaWelcome.set(area, {
    value: {
      viewed: true,
      ...其它信息
    },
    // 绝对时间戳
    expiredAt: dayjs().add(1, 'day').unix(),

    // 或者用相对时间(毫秒)
    expiresIn: 7200
  })
}

如果当前记录已过期,get 方法将自动清除数据并返回空。

TIP

使用 useStorageMap 主要在处理一系列数据时具有优势。如果你只需要存储单个数据,那么你应该使用另一个 API,即 storage。值得一提的是,storage 也支持设置过期时间。

常见问题

本地存储共享状态

在项目中,经常出现一种常见需求:某个弹窗已经显示过,不再需要弹出,而这个弹窗存在于多个页面中。对于这种情况,useStorageMap 并不适用。我们建议使用 storagepinia 处理。

以下是示例代码:

js
import {defineStore} from 'pinia'
import {ref} from 'vue'
import {storage} from '@ifanrx/uni-mp'

const key = 'preview_enable_test_shared'

export default defineStore('shared-storage', () => {
  const enable = ref(storage.getItem(key) || false)

  const onToggle = () => {
    enable.value = !enable.value

    storage.setItem(key, enable.value)
  }

  return {
    enable,
    onToggle,
  }
})
import {defineStore} from 'pinia'
import {ref} from 'vue'
import {storage} from '@ifanrx/uni-mp'

const key = 'preview_enable_test_shared'

export default defineStore('shared-storage', () => {
  const enable = ref(storage.getItem(key) || false)

  const onToggle = () => {
    enable.value = !enable.value

    storage.setItem(key, enable.value)
  }

  return {
    enable,
    onToggle,
  }
})
vue
<script setup>
import {router, storage} from '@ifanrx/uni-mp'
import {storeToRefs} from 'pinia'

import useSharedStorage from './use-shared-storage'

const sharedStorage = useSharedStorage()

const {enable: enableShared} = storeToRefs(sharedStorage)
const {onToggle: onToggleShared} = sharedStorage

const navToSub = () => {
  router.push('pages/mplib-storage/sub-page')
}
</script>

<template>
  <mp-nav-bar title="storage" />

  <view class="page">
    <uni-section title="跨页面共享存储状态(可监听)" type="line">
      <view class="example-body">
        <view>
          <switch :checked="enableShared" @change="onToggleShared" />
          开启通知
        </view>
        <button class="btn" @click="navToSub">进入子页面</button>
      </view>
    </uni-section>
  </view>
</template>

<style scoped>
.page {
  padding-bottom: 100rpx;
}

.example-body {
  padding: 10rpx;
}

.btn {
  margin-top: 20rpx;
}
</style>
<script setup>
import {router, storage} from '@ifanrx/uni-mp'
import {storeToRefs} from 'pinia'

import useSharedStorage from './use-shared-storage'

const sharedStorage = useSharedStorage()

const {enable: enableShared} = storeToRefs(sharedStorage)
const {onToggle: onToggleShared} = sharedStorage

const navToSub = () => {
  router.push('pages/mplib-storage/sub-page')
}
</script>

<template>
  <mp-nav-bar title="storage" />

  <view class="page">
    <uni-section title="跨页面共享存储状态(可监听)" type="line">
      <view class="example-body">
        <view>
          <switch :checked="enableShared" @change="onToggleShared" />
          开启通知
        </view>
        <button class="btn" @click="navToSub">进入子页面</button>
      </view>
    </uni-section>
  </view>
</template>

<style scoped>
.page {
  padding-bottom: 100rpx;
}

.example-body {
  padding: 10rpx;
}

.btn {
  margin-top: 20rpx;
}
</style>
vue
<script setup>
import {storeToRefs} from 'pinia'

import useSharedStorage from './use-shared-storage'

const sharedStorage = useSharedStorage()

const {enable: enableShared} = storeToRefs(sharedStorage)
const {onToggle: onToggleShared} = sharedStorage
</script>

<template>
  <mp-nav-bar title="storage" />

  <view class="page">
    <uni-section title="跨页面共享存储状态(可监听)" type="line">
      <view class="example-body">
        <view>
          <switch :checked="enableShared" @change="onToggleShared" />
          开启通知
        </view>
      </view>
    </uni-section>
  </view>
</template>

<style scoped>
.page {
  padding-bottom: 100rpx;
}

.example-body {
  padding: 10rpx;
}
</style>
<script setup>
import {storeToRefs} from 'pinia'

import useSharedStorage from './use-shared-storage'

const sharedStorage = useSharedStorage()

const {enable: enableShared} = storeToRefs(sharedStorage)
const {onToggle: onToggleShared} = sharedStorage
</script>

<template>
  <mp-nav-bar title="storage" />

  <view class="page">
    <uni-section title="跨页面共享存储状态(可监听)" type="line">
      <view class="example-body">
        <view>
          <switch :checked="enableShared" @change="onToggleShared" />
          开启通知
        </view>
      </view>
    </uni-section>
  </view>
</template>

<style scoped>
.page {
  padding-bottom: 100rpx;
}

.example-body {
  padding: 10rpx;
}
</style>

相关文档