feat: 添加支持过期时间的存储缓存功能,优化语言加载逻辑
This commit is contained in:
10
auto-imports.d.ts
vendored
10
auto-imports.d.ts
vendored
@@ -10,6 +10,7 @@ declare global {
|
||||
const acceptHMRUpdate: typeof import('pinia').acceptHMRUpdate
|
||||
const asyncComputed: typeof import('@vueuse/core').asyncComputed
|
||||
const autoResetRef: typeof import('@vueuse/core').autoResetRef
|
||||
const clearExpiredCaches: typeof import('./src/composables/useStorageCache').clearExpiredCaches
|
||||
const computed: typeof import('vue').computed
|
||||
const computedAsync: typeof import('@vueuse/core').computedAsync
|
||||
const computedEager: typeof import('@vueuse/core').computedEager
|
||||
@@ -42,6 +43,7 @@ declare global {
|
||||
const formatAmountWithUnit: typeof import('./src/utils/helper').formatAmountWithUnit
|
||||
const formatBalance: typeof import('./src/utils/helper').formatBalance
|
||||
const getActivePinia: typeof import('pinia').getActivePinia
|
||||
const getCacheRemainingTime: typeof import('./src/composables/useStorageCache').getCacheRemainingTime
|
||||
const getCurrentInstance: typeof import('vue').getCurrentInstance
|
||||
const getCurrentScope: typeof import('vue').getCurrentScope
|
||||
const getCurrentWatcher: typeof import('vue').getCurrentWatcher
|
||||
@@ -49,6 +51,7 @@ declare global {
|
||||
const ignorableWatch: typeof import('@vueuse/core').ignorableWatch
|
||||
const inject: typeof import('vue').inject
|
||||
const injectLocal: typeof import('@vueuse/core').injectLocal
|
||||
const isCacheExpired: typeof import('./src/composables/useStorageCache').isCacheExpired
|
||||
const isDefined: typeof import('@vueuse/core').isDefined
|
||||
const isFunction: typeof import('./src/utils/is').isFunction
|
||||
const isPromise: typeof import('./src/utils/is').isPromise
|
||||
@@ -104,6 +107,7 @@ declare global {
|
||||
const refManualReset: typeof import('@vueuse/core').refManualReset
|
||||
const refThrottled: typeof import('@vueuse/core').refThrottled
|
||||
const refWithControl: typeof import('@vueuse/core').refWithControl
|
||||
const refreshCacheExpire: typeof import('./src/composables/useStorageCache').refreshCacheExpire
|
||||
const resolveComponent: typeof import('vue').resolveComponent
|
||||
const resolveRef: typeof import('@vueuse/core').resolveRef
|
||||
const setActivePinia: typeof import('pinia').setActivePinia
|
||||
@@ -272,6 +276,7 @@ declare global {
|
||||
const useStepper: typeof import('@vueuse/core').useStepper
|
||||
const useStorage: typeof import('@vueuse/core').useStorage
|
||||
const useStorageAsync: typeof import('@vueuse/core').useStorageAsync
|
||||
const useStorageCache: typeof import('./src/composables/useStorageCache').useStorageCache
|
||||
const useStyleTag: typeof import('@vueuse/core').useStyleTag
|
||||
const useSubscribeModal: typeof import('./src/composables/useSubscribeModal').useSubscribeModal
|
||||
const useSupported: typeof import('@vueuse/core').useSupported
|
||||
@@ -363,6 +368,7 @@ declare module 'vue' {
|
||||
readonly acceptHMRUpdate: UnwrapRef<typeof import('pinia')['acceptHMRUpdate']>
|
||||
readonly asyncComputed: UnwrapRef<typeof import('@vueuse/core')['asyncComputed']>
|
||||
readonly autoResetRef: UnwrapRef<typeof import('@vueuse/core')['autoResetRef']>
|
||||
readonly clearExpiredCaches: UnwrapRef<typeof import('./src/composables/useStorageCache')['clearExpiredCaches']>
|
||||
readonly computed: UnwrapRef<typeof import('vue')['computed']>
|
||||
readonly computedAsync: UnwrapRef<typeof import('@vueuse/core')['computedAsync']>
|
||||
readonly computedEager: UnwrapRef<typeof import('@vueuse/core')['computedEager']>
|
||||
@@ -394,6 +400,7 @@ declare module 'vue' {
|
||||
readonly formatAmount: UnwrapRef<typeof import('./src/utils/helper')['formatAmount']>
|
||||
readonly formatBalance: UnwrapRef<typeof import('./src/utils/helper')['formatBalance']>
|
||||
readonly getActivePinia: UnwrapRef<typeof import('pinia')['getActivePinia']>
|
||||
readonly getCacheRemainingTime: UnwrapRef<typeof import('./src/composables/useStorageCache')['getCacheRemainingTime']>
|
||||
readonly getCurrentInstance: UnwrapRef<typeof import('vue')['getCurrentInstance']>
|
||||
readonly getCurrentScope: UnwrapRef<typeof import('vue')['getCurrentScope']>
|
||||
readonly getCurrentWatcher: UnwrapRef<typeof import('vue')['getCurrentWatcher']>
|
||||
@@ -401,6 +408,7 @@ declare module 'vue' {
|
||||
readonly ignorableWatch: UnwrapRef<typeof import('@vueuse/core')['ignorableWatch']>
|
||||
readonly inject: UnwrapRef<typeof import('vue')['inject']>
|
||||
readonly injectLocal: UnwrapRef<typeof import('@vueuse/core')['injectLocal']>
|
||||
readonly isCacheExpired: UnwrapRef<typeof import('./src/composables/useStorageCache')['isCacheExpired']>
|
||||
readonly isDefined: UnwrapRef<typeof import('@vueuse/core')['isDefined']>
|
||||
readonly isFunction: UnwrapRef<typeof import('./src/utils/is')['isFunction']>
|
||||
readonly isPromise: UnwrapRef<typeof import('./src/utils/is')['isPromise']>
|
||||
@@ -456,6 +464,7 @@ declare module 'vue' {
|
||||
readonly refManualReset: UnwrapRef<typeof import('@vueuse/core')['refManualReset']>
|
||||
readonly refThrottled: UnwrapRef<typeof import('@vueuse/core')['refThrottled']>
|
||||
readonly refWithControl: UnwrapRef<typeof import('@vueuse/core')['refWithControl']>
|
||||
readonly refreshCacheExpire: UnwrapRef<typeof import('./src/composables/useStorageCache')['refreshCacheExpire']>
|
||||
readonly resolveComponent: UnwrapRef<typeof import('vue')['resolveComponent']>
|
||||
readonly resolveRef: UnwrapRef<typeof import('@vueuse/core')['resolveRef']>
|
||||
readonly setActivePinia: UnwrapRef<typeof import('pinia')['setActivePinia']>
|
||||
@@ -624,6 +633,7 @@ declare module 'vue' {
|
||||
readonly useStepper: UnwrapRef<typeof import('@vueuse/core')['useStepper']>
|
||||
readonly useStorage: UnwrapRef<typeof import('@vueuse/core')['useStorage']>
|
||||
readonly useStorageAsync: UnwrapRef<typeof import('@vueuse/core')['useStorageAsync']>
|
||||
readonly useStorageCache: UnwrapRef<typeof import('./src/composables/useStorageCache')['useStorageCache']>
|
||||
readonly useStyleTag: UnwrapRef<typeof import('@vueuse/core')['useStyleTag']>
|
||||
readonly useSupported: UnwrapRef<typeof import('@vueuse/core')['useSupported']>
|
||||
readonly useSwipe: UnwrapRef<typeof import('@vueuse/core')['useSwipe']>
|
||||
|
||||
@@ -43,7 +43,21 @@ export function useLanguage() {
|
||||
}
|
||||
|
||||
function loadRemoteLanguage() {
|
||||
const storageKey = `remote-lang-${language.value}`;
|
||||
const remoteLangJson = useStorageCache<string>(storageKey, "", { ttl: 24 * 60 * 60 }); // 缓存 1 天
|
||||
if (remoteLangJson.value) {
|
||||
try {
|
||||
const messages = JSON.parse(remoteLangJson.value);
|
||||
mergeLocaleMessage(locale.value, messages);
|
||||
return;
|
||||
}
|
||||
catch (e) {
|
||||
console.error("Failed to parse remote language JSON:", e);
|
||||
}
|
||||
}
|
||||
safeClient(client.api.error_messages({ lang: language.value }).get()).then((res) => {
|
||||
clearExpiredCaches([storageKey]);
|
||||
remoteLangJson.value = JSON.stringify(res.data.value);
|
||||
mergeLocaleMessage(locale.value, res.data.value);
|
||||
});
|
||||
}
|
||||
|
||||
248
src/composables/useStorageCache.ts
Normal file
248
src/composables/useStorageCache.ts
Normal file
@@ -0,0 +1,248 @@
|
||||
import type { RemovableRef, StorageLike } from "@vueuse/core";
|
||||
|
||||
interface CacheData<T> {
|
||||
value: T;
|
||||
expireTime?: number; // 过期时间戳(毫秒)
|
||||
}
|
||||
|
||||
interface UseStorageCacheOptions {
|
||||
/**
|
||||
* 缓存过期时间(秒)
|
||||
* @default undefined 永不过期
|
||||
*/
|
||||
ttl?: number;
|
||||
/**
|
||||
* 存储介质
|
||||
* @default localStorage
|
||||
*/
|
||||
storage?: StorageLike;
|
||||
/**
|
||||
* 是否在过期后自动删除
|
||||
* @default true
|
||||
*/
|
||||
autoRemoveOnExpire?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用支持过期时间的 Storage
|
||||
* @param key 存储键
|
||||
* @param initialValue 初始值
|
||||
* @param options 配置项
|
||||
* @returns 响应式存储引用
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* // 创建一个 5 分钟后过期的缓存
|
||||
* const token = useStorageCache('user-token', '', { ttl: 300 })
|
||||
*
|
||||
* // 设置值
|
||||
* token.value = 'new-token'
|
||||
*
|
||||
* // 检查是否过期
|
||||
* if (isExpired(token)) {
|
||||
* console.log('Token 已过期')
|
||||
* }
|
||||
*
|
||||
* // 刷新过期时间
|
||||
* refreshExpire(token, 600) // 延长到 10 分钟
|
||||
* ```
|
||||
*/
|
||||
export function useStorageCache<T>(
|
||||
key: string,
|
||||
initialValue: T,
|
||||
options: UseStorageCacheOptions = {},
|
||||
): RemovableRef<T> {
|
||||
const {
|
||||
ttl,
|
||||
storage = localStorage,
|
||||
autoRemoveOnExpire = true,
|
||||
} = options;
|
||||
|
||||
// 尝试从 storage 读取缓存数据
|
||||
const rawData = storage.getItem(key);
|
||||
let cachedData: CacheData<T> | null = null;
|
||||
|
||||
if (rawData) {
|
||||
try {
|
||||
cachedData = JSON.parse(rawData) as CacheData<T>;
|
||||
|
||||
// 检查是否过期
|
||||
if (cachedData.expireTime && Date.now() > cachedData.expireTime) {
|
||||
// 已过期
|
||||
if (autoRemoveOnExpire) {
|
||||
storage.removeItem(key);
|
||||
}
|
||||
cachedData = null;
|
||||
}
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`解析缓存数据失败 [${key}]:`, error);
|
||||
cachedData = null;
|
||||
}
|
||||
}
|
||||
|
||||
// 计算初始值
|
||||
const defaultValue = cachedData ? cachedData.value : initialValue;
|
||||
|
||||
// 使用 useStorage 创建响应式引用
|
||||
const storageRef = useStorage<T>(
|
||||
key,
|
||||
defaultValue,
|
||||
storage,
|
||||
{
|
||||
serializer: {
|
||||
read: (raw: string) => {
|
||||
try {
|
||||
const data = JSON.parse(raw) as CacheData<T>;
|
||||
// 再次检查过期时间(防止并发问题)
|
||||
if (data.expireTime && Date.now() > data.expireTime) {
|
||||
if (autoRemoveOnExpire) {
|
||||
storage.removeItem(key);
|
||||
}
|
||||
return initialValue;
|
||||
}
|
||||
return data.value;
|
||||
}
|
||||
catch {
|
||||
return initialValue;
|
||||
}
|
||||
},
|
||||
write: (value: T) => {
|
||||
const cacheData: CacheData<T> = {
|
||||
value,
|
||||
};
|
||||
|
||||
// 如果设置了 ttl,计算过期时间
|
||||
if (ttl && ttl > 0) {
|
||||
cacheData.expireTime = Date.now() + ttl * 1000;
|
||||
}
|
||||
|
||||
return JSON.stringify(cacheData);
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
return storageRef;
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查缓存是否过期
|
||||
* @param key 存储键或存储引用
|
||||
* @param storage 存储介质
|
||||
* @returns 是否过期
|
||||
*/
|
||||
export function isCacheExpired(
|
||||
key: string | RemovableRef<any>,
|
||||
storage: StorageLike = localStorage,
|
||||
): boolean {
|
||||
const cacheKey = typeof key === "string" ? key : key.value;
|
||||
|
||||
try {
|
||||
const rawData = storage.getItem(cacheKey);
|
||||
if (!rawData) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const data = JSON.parse(rawData) as CacheData<any>;
|
||||
if (!data.expireTime) {
|
||||
return false; // 没有设置过期时间,永不过期
|
||||
}
|
||||
|
||||
return Date.now() > data.expireTime;
|
||||
}
|
||||
catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新缓存过期时间
|
||||
* @param key 存储键
|
||||
* @param ttl 新的过期时间(秒)
|
||||
* @param storage 存储介质
|
||||
*/
|
||||
export function refreshCacheExpire(
|
||||
key: string,
|
||||
ttl: number,
|
||||
storage: StorageLike = localStorage,
|
||||
): void {
|
||||
try {
|
||||
const rawData = storage.getItem(key);
|
||||
if (!rawData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(rawData) as CacheData<any>;
|
||||
data.expireTime = Date.now() + ttl * 1000;
|
||||
|
||||
storage.setItem(key, JSON.stringify(data));
|
||||
}
|
||||
catch (error) {
|
||||
console.error(`刷新缓存过期时间失败 [${key}]:`, error);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取缓存剩余有效时间(秒)
|
||||
* @param key 存储键
|
||||
* @param storage 存储介质
|
||||
* @returns 剩余时间(秒),如果已过期返回 0,如果永不过期返回 Infinity
|
||||
*/
|
||||
export function getCacheRemainingTime(
|
||||
key: string,
|
||||
storage: StorageLike = localStorage,
|
||||
): number {
|
||||
try {
|
||||
const rawData = storage.getItem(key);
|
||||
if (!rawData) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
const data = JSON.parse(rawData) as CacheData<any>;
|
||||
if (!data.expireTime) {
|
||||
return Infinity; // 永不过期
|
||||
}
|
||||
|
||||
const remaining = Math.max(0, data.expireTime - Date.now());
|
||||
return Math.floor(remaining / 1000);
|
||||
}
|
||||
catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 清除指定键的过期缓存
|
||||
* @param keys 要检查的键列表
|
||||
* @param storage 存储介质
|
||||
* @returns 被删除的键数量
|
||||
*/
|
||||
export function clearExpiredCaches(
|
||||
keys: string[],
|
||||
storage: StorageLike = localStorage,
|
||||
): number {
|
||||
let removedCount = 0;
|
||||
|
||||
keys.forEach((key) => {
|
||||
try {
|
||||
const rawData = storage.getItem(key);
|
||||
if (!rawData) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = JSON.parse(rawData) as CacheData<any>;
|
||||
|
||||
// 检查是否过期
|
||||
if (data.expireTime && Date.now() > data.expireTime) {
|
||||
storage.removeItem(key);
|
||||
removedCount++;
|
||||
}
|
||||
}
|
||||
catch {
|
||||
// 忽略解析错误
|
||||
}
|
||||
});
|
||||
|
||||
return removedCount;
|
||||
}
|
||||
Reference in New Issue
Block a user