mirror of
https://github.com/algerkong/AlgerMusicPlayer.git
synced 2026-05-19 03:57:28 +08:00
refactor(persist): 抽公共防抖 storage 与 minifySong 工具,playlist/playerCore 接入
把 9caaec3 在 playlist 内联的 minifySong + debouncedLocalStorage 抽到 utils 共用,playerCore 同步接入,为后续推广到 playHistory 做准备。 - 新增 utils/debouncedStorage.ts:高频状态变更(volume 拖动、isPlay 切换)2s 防抖落盘,beforeunload 钩子兜底刷盘;多 store 共用同一 pendingWrites map,flush 时一次写完所有 pending key - 新增 utils/persistedSong.ts:minifySong 剥离 base64 picUrl、仅保留 local:// 永不过期的 playMusicUrl;ar.length 守卫避免空数组吞掉 s.artists 兜底;al.id 守卫避免序列化出无 id 空壳 - playlist.ts 删除内联实现改用 import,行为不变 - playerCore.ts 切到防抖落盘并 minify playMusic;id 守卫避免空 playMusic 走 minify 后失去 Object.keys().length===0 判空能力, 下次启动会误恢复一首无 id 的空歌 Trade-off:极端非正常退出(kill -9 / 断电)下最近 2s 的 volume / isPlay / playMusic 变更会丢——这些状态丢一次无大碍,可接受
This commit is contained in:
@@ -4,6 +4,8 @@ import { computed, ref } from 'vue';
|
||||
import { audioService } from '@/services/audioService';
|
||||
import type { AudioOutputDevice } from '@/types/audio';
|
||||
import type { SongResult } from '@/types/music';
|
||||
import { debouncedLocalStorage } from '@/utils/debouncedStorage';
|
||||
import { minifySong } from '@/utils/persistedSong';
|
||||
|
||||
/**
|
||||
* 核心播放控制 Store
|
||||
@@ -220,7 +222,12 @@ export const usePlayerCoreStore = defineStore(
|
||||
{
|
||||
persist: {
|
||||
key: 'player-core-store',
|
||||
storage: localStorage,
|
||||
// 使用 debouncedLocalStorage:volume 拖动 / 静音切换会高频触发 mutation,
|
||||
// 直接写 localStorage 会导致每次都 stringify + minify 整个 state,浪费 I/O。
|
||||
// 防抖 2s 写一次足够,beforeunload 钩子兜底刷盘。
|
||||
// Trade-off:极端非正常退出(kill -9 / 断电 / 主进程崩溃)下 beforeunload 不触发,
|
||||
// 最近 2s 的 volume / isPlay / playMusic 变更会丢——这些状态丢一次无大碍,可接受
|
||||
storage: debouncedLocalStorage,
|
||||
pick: [
|
||||
'playMusic',
|
||||
'playMusicUrl',
|
||||
@@ -229,7 +236,22 @@ export const usePlayerCoreStore = defineStore(
|
||||
'isMuted',
|
||||
'isPlay',
|
||||
'audioOutputDeviceId'
|
||||
]
|
||||
],
|
||||
// playMusic 持久化前过 minifySong:剥离 base64 封面、lyric、song 等大字段。
|
||||
// 单首歌的 lyric 持久化后没有用(重启后会重新加载),但 picUrl 若是 base64 会
|
||||
// 拖累整个 player-core-store 写入失败(5MB 配额)。playMusicUrl 在 store 层级单独
|
||||
// 持久化,不受 playMusic 内部精简影响——本地音乐 local:// URL 仍保持可恢复。
|
||||
// id 守卫:空 playMusic 走 minifySong 会得到 {picUrl:'', ar:[]}(stripDataUrl
|
||||
// 把 undefined 转成空串、ar 缺省返回空数组),下次启动 playbackController 的
|
||||
// Object.keys().length === 0 判空就会失效,误恢复一首无 id 的空歌
|
||||
serializer: {
|
||||
serialize: (state: any) =>
|
||||
JSON.stringify({
|
||||
...state,
|
||||
playMusic: state.playMusic?.id ? minifySong(state.playMusic as SongResult) : {}
|
||||
}),
|
||||
deserialize: JSON.parse
|
||||
}
|
||||
}
|
||||
}
|
||||
);
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { useThrottleFn } from '@vueuse/core';
|
||||
import { debounce } from 'lodash';
|
||||
import { createDiscreteApi } from 'naive-ui';
|
||||
import { defineStore, storeToRefs } from 'pinia';
|
||||
import { computed, ref, shallowRef, triggerRef } from 'vue';
|
||||
@@ -9,6 +8,8 @@ import { useSongDetail } from '@/hooks/usePlayerHooks';
|
||||
import { preloadService } from '@/services/preloadService';
|
||||
import type { SongResult } from '@/types/music';
|
||||
import { getImgUrl } from '@/utils';
|
||||
import { debouncedLocalStorage } from '@/utils/debouncedStorage';
|
||||
import { minifySongList } from '@/utils/persistedSong';
|
||||
import { performShuffle, preloadCoverImage } from '@/utils/playerUtils';
|
||||
|
||||
import { useIntelligenceModeStore } from './intelligenceMode';
|
||||
@@ -22,74 +23,6 @@ const getMessage = () => {
|
||||
return _message;
|
||||
};
|
||||
|
||||
/**
|
||||
* 精简 SongResult 对象,只保留持久化必要字段
|
||||
* 排除大体积字段:lyric, song, backgroundColor, primaryColor
|
||||
*
|
||||
* picUrl/al.picUrl 若为 base64 Data URL 一律剥离:localStorage 仅 5MB 配额,
|
||||
* 单张 base64 封面动辄几百 KB,几首就能撑爆导致整个 playList 写入失败。
|
||||
* 剥离后恢复时展示默认封面图,picUrl 仍是 http(s):// 或 local:// 短引用时原样保留。
|
||||
*
|
||||
* 仅 local:// 的 playMusicUrl(永不过期)会被持久化,让本地音乐恢复后免重新解析;
|
||||
* expiredAt 不持久化——本地音乐每次走 toSongResult 会重新生成,远程歌曲恢复后重新拉详情即可。
|
||||
*/
|
||||
const stripDataUrl = (url: string | undefined): string =>
|
||||
!url || url.startsWith('data:') ? '' : url;
|
||||
|
||||
const minifySong = (s: SongResult) => ({
|
||||
id: s.id,
|
||||
name: s.name,
|
||||
picUrl: stripDataUrl(s.picUrl),
|
||||
ar: s.ar?.map((a) => ({ id: a.id, name: a.name })),
|
||||
al: s.al && { id: s.al.id, name: s.al.name, picUrl: stripDataUrl(s.al.picUrl) },
|
||||
source: s.source,
|
||||
dt: s.dt,
|
||||
// 仅 local:// 永不过期,保留给本地音乐恢复后免重新解析;其他 URL 会过期,丢掉让恢复时重新拉
|
||||
// JSON.stringify 自动丢 undefined,无需条件 spread
|
||||
playMusicUrl: s.playMusicUrl?.startsWith('local://') ? s.playMusicUrl : undefined
|
||||
});
|
||||
|
||||
const minifySongList = (list: SongResult[] | undefined) => list?.map(minifySong) ?? [];
|
||||
|
||||
/**
|
||||
* 防抖 localStorage 包装,降低写入频率
|
||||
* 通过 pendingWrites 跟踪未写入数据,beforeunload 时刷新
|
||||
*/
|
||||
const pendingWrites = new Map<string, string>();
|
||||
|
||||
const safeSetItem = (key: string, value: string) => {
|
||||
try {
|
||||
localStorage.setItem(key, value);
|
||||
} catch (error) {
|
||||
console.error('[playlist] localStorage 写入失败(可能超出配额):', error);
|
||||
}
|
||||
};
|
||||
|
||||
const flushPendingWrites = () => {
|
||||
pendingWrites.forEach((value, key) => {
|
||||
safeSetItem(key, value);
|
||||
});
|
||||
pendingWrites.clear();
|
||||
};
|
||||
|
||||
const debouncedSetItem = debounce((key: string, value: string) => {
|
||||
safeSetItem(key, value);
|
||||
pendingWrites.delete(key);
|
||||
}, 2000);
|
||||
|
||||
const debouncedLocalStorage = {
|
||||
getItem: (key: string) => localStorage.getItem(key),
|
||||
setItem: (key: string, value: string) => {
|
||||
pendingWrites.set(key, value);
|
||||
debouncedSetItem(key, value);
|
||||
}
|
||||
};
|
||||
|
||||
// 正常关闭时刷新未写入的数据
|
||||
if (typeof window !== 'undefined') {
|
||||
window.addEventListener('beforeunload', flushPendingWrites);
|
||||
}
|
||||
|
||||
/**
|
||||
* 播放列表管理 Store
|
||||
* 负责:播放列表、索引、播放模式、预加载、上/下一首
|
||||
|
||||
Reference in New Issue
Block a user