Skip to content

Commit d0049c8

Browse files
committed
feat: 添加缓存功能
1 parent c6fbdeb commit d0049c8

File tree

10 files changed

+1228
-2643
lines changed

10 files changed

+1228
-2643
lines changed

.husky/pre-commit

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
#!/bin/sh
22
. "$(dirname "$0")/_/husky.sh"
33

4-
pnpm lint && pnpm format
4+
pnpm format && pnpm lint

demo/react-lyric-demo/src/App.tsx

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,9 @@ const App = () => {
3030
height="60vh"
3131
showAudioControl={true}
3232
onLyricChange={handleLyricChange}
33-
onAudioPlay={(isPlaying) => console.log('音频播放状态:', isPlaying)}
33+
onAudioPlay={(isPlaying: boolean) =>
34+
console.log('音频播放状态:', isPlaying)
35+
}
3436
/>
3537
</div>
3638
)

package.json

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
"version": "1.0.0",
66
"scripts": {
77
"dev": "pnpm -filter demo dev",
8-
"build": "pnpm -filter \"packages/**\" build",
8+
"build": "pnpm -filter core build && pnpm -filter react build",
9+
"build:core": "pnpm -filter core build",
10+
"build:react": "pnpm -filter react build",
911
"lint": "eslint . --ext .js,.jsx,.ts,.tsx,.vue",
1012
"format": "prettier --write .",
1113
"prepare": "husky",
@@ -33,6 +35,7 @@
3335
"markdownlint-cli": "^0.47.0",
3436
"prettier": "^3.1.0",
3537
"rimraf": "^6.1.2",
38+
"tslib": "^2.8.1",
3639
"typescript": "^5.9.3"
3740
},
3841
"config": {

packages/core/src/utils/lyricCache.ts

Lines changed: 232 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,14 @@ export interface LyricCacheOptions {
1212
* 缓存过期时间(毫秒)
1313
*/
1414
expiration?: number
15+
/**
16+
* IndexedDB数据库名称
17+
*/
18+
dbName?: string
19+
/**
20+
* IndexedDB存储对象名称
21+
*/
22+
storeName?: string
1523
}
1624

1725
/**
@@ -29,10 +37,57 @@ interface CacheData {
2937
export class LyricCache {
3038
private prefix: string
3139
private expiration: number
40+
private dbName: string
41+
private storeName: string
42+
private db: IDBDatabase | null = null
43+
private dbReady: Promise<void> | null = null
3244

3345
constructor(options: LyricCacheOptions = {}) {
3446
this.prefix = options.prefix || 'lyric_cache_'
3547
this.expiration = options.expiration || 7 * 24 * 60 * 60 * 1000 // 默认7天
48+
this.dbName = options.dbName || 'lyric_cache_db'
49+
this.storeName = options.storeName || 'lyrics'
50+
this.initDB()
51+
}
52+
53+
/**
54+
* 初始化IndexedDB
55+
*/
56+
private initDB(): void {
57+
if (typeof window === 'undefined' || !('indexedDB' in window)) {
58+
console.warn('IndexedDB is not supported in this environment')
59+
return
60+
}
61+
62+
this.dbReady = new Promise<void>((resolve) => {
63+
const request = window.indexedDB.open(this.dbName, 1)
64+
65+
request.onerror = () => {
66+
console.error('Error opening IndexedDB:', request.error)
67+
resolve() // 即使失败也继续,回退到localStorage
68+
}
69+
70+
request.onsuccess = () => {
71+
this.db = request.result
72+
resolve()
73+
}
74+
75+
request.onupgradeneeded = (event) => {
76+
const db = (event.target as IDBOpenDBRequest).result
77+
if (!db.objectStoreNames.contains(this.storeName)) {
78+
db.createObjectStore(this.storeName, { keyPath: 'key' })
79+
}
80+
}
81+
})
82+
}
83+
84+
/**
85+
* 确保DB已初始化
86+
*/
87+
private ensureDBInitialized(): void {
88+
if (!this.dbReady) {
89+
this.initDB()
90+
}
3691
}
3792

3893
/**
@@ -55,20 +110,155 @@ export class LyricCache {
55110
return now - data.timestamp > expirationTime
56111
}
57112

113+
/**
114+
* 检查localStorage是否有足够空间
115+
* @param data 要存储的数据
116+
* @returns 是否有足够空间
117+
*/
118+
private hasLocalStorageSpace(data: string): boolean {
119+
if (typeof window === 'undefined' || !('localStorage' in window)) {
120+
return false
121+
}
122+
123+
try {
124+
const testKey = `${this.prefix}test`
125+
window.localStorage.setItem(testKey, data)
126+
window.localStorage.removeItem(testKey)
127+
return true
128+
} catch (error) {
129+
return false
130+
}
131+
}
132+
133+
/**
134+
* 使用IndexedDB存储数据
135+
* @param key 缓存键
136+
* @param data 缓存数据
137+
*/
138+
private async setToIndexedDB(key: string, data: CacheData): Promise<boolean> {
139+
this.ensureDBInitialized()
140+
if (!this.dbReady) return false
141+
142+
await this.dbReady
143+
if (!this.db) return false
144+
145+
const db = this.db
146+
return new Promise<boolean>((resolve) => {
147+
const transaction = db.transaction(this.storeName, 'readwrite')
148+
const store = transaction.objectStore(this.storeName)
149+
const request = store.put({ key, ...data })
150+
151+
request.onsuccess = () => resolve(true)
152+
request.onerror = () => {
153+
console.error('Error storing data in IndexedDB:', request.error)
154+
resolve(false)
155+
}
156+
})
157+
}
158+
159+
/**
160+
* 从IndexedDB获取数据
161+
* @param key 缓存键
162+
* @returns 缓存数据或null
163+
*/
164+
private async getFromIndexedDB(key: string): Promise<BaseLyricItem[] | null> {
165+
this.ensureDBInitialized()
166+
if (!this.dbReady) return null
167+
168+
await this.dbReady
169+
if (!this.db) return null
170+
171+
const db = this.db
172+
return new Promise<BaseLyricItem[] | null>((resolve) => {
173+
const transaction = db.transaction(this.storeName, 'readonly')
174+
const store = transaction.objectStore(this.storeName)
175+
const request = store.get(key)
176+
177+
request.onsuccess = () => {
178+
const data = request.result
179+
if (!data) {
180+
resolve(null)
181+
return
182+
}
183+
184+
if (this.isExpired(data)) {
185+
this.removeFromIndexedDB(key)
186+
resolve(null)
187+
return
188+
}
189+
190+
resolve(data.data)
191+
}
192+
193+
request.onerror = () => {
194+
console.error('Error getting data from IndexedDB:', request.error)
195+
resolve(null)
196+
}
197+
})
198+
}
199+
200+
/**
201+
* 从IndexedDB移除数据
202+
* @param key 缓存键
203+
*/
204+
private async removeFromIndexedDB(key: string): Promise<void> {
205+
this.ensureDBInitialized()
206+
if (!this.dbReady) return
207+
208+
await this.dbReady
209+
if (!this.db) return
210+
211+
const db = this.db
212+
const transaction = db.transaction(this.storeName, 'readwrite')
213+
const store = transaction.objectStore(this.storeName)
214+
store.delete(key)
215+
}
216+
217+
/**
218+
* 清除IndexedDB中的所有数据
219+
*/
220+
private async clearIndexedDB(): Promise<void> {
221+
this.ensureDBInitialized()
222+
if (!this.dbReady) return
223+
224+
await this.dbReady
225+
if (!this.db) return
226+
227+
const db = this.db
228+
const transaction = db.transaction(this.storeName, 'readwrite')
229+
const store = transaction.objectStore(this.storeName)
230+
store.clear()
231+
}
232+
58233
/**
59234
* 缓存歌词
60235
* @param key 缓存键
61236
* @param data 歌词数据
62237
*/
63-
set(key: string, data: BaseLyricItem[]): void {
238+
async set(key: string, data: BaseLyricItem[]): Promise<void> {
64239
try {
65240
const cacheKey = this.generateKey(key)
66241
const cacheData: CacheData = {
67242
data,
68243
timestamp: Date.now(),
69244
expiration: this.expiration,
70245
}
71-
localStorage.setItem(cacheKey, JSON.stringify(cacheData))
246+
247+
const jsonData = JSON.stringify(cacheData)
248+
249+
// 尝试使用localStorage
250+
if (this.hasLocalStorageSpace(jsonData)) {
251+
window.localStorage.setItem(cacheKey, jsonData)
252+
return
253+
}
254+
255+
// localStorage空间不足,尝试使用IndexedDB
256+
const storedInIndexedDB = await this.setToIndexedDB(cacheKey, cacheData)
257+
if (storedInIndexedDB) {
258+
return
259+
}
260+
261+
console.warn('Failed to cache lyric in both localStorage and IndexedDB')
72262
} catch (error) {
73263
console.error('Error caching lyric:', error)
74264
}
@@ -79,19 +269,29 @@ export class LyricCache {
79269
* @param key 缓存键
80270
* @returns 缓存的歌词数据,或null如果不存在或已过期
81271
*/
82-
get(key: string): BaseLyricItem[] | null {
272+
async get(key: string): Promise<BaseLyricItem[] | null> {
83273
try {
84274
const cacheKey = this.generateKey(key)
85-
const cachedData = localStorage.getItem(cacheKey)
86-
if (!cachedData) {
87-
return null
275+
276+
// 尝试从localStorage获取
277+
if (typeof window !== 'undefined' && 'localStorage' in window) {
278+
const cachedData = window.localStorage.getItem(cacheKey)
279+
if (cachedData) {
280+
const parsedData: CacheData = JSON.parse(cachedData)
281+
if (!this.isExpired(parsedData)) {
282+
return parsedData.data
283+
}
284+
this.remove(key)
285+
}
88286
}
89-
const parsedData: CacheData = JSON.parse(cachedData)
90-
if (this.isExpired(parsedData)) {
91-
this.remove(key)
92-
return null
287+
288+
// 从localStorage获取失败,尝试从IndexedDB获取
289+
const indexedDBData = await this.getFromIndexedDB(cacheKey)
290+
if (indexedDBData) {
291+
return indexedDBData
93292
}
94-
return parsedData.data
293+
294+
return null
95295
} catch (error) {
96296
console.error('Error getting cached lyric:', error)
97297
return null
@@ -102,10 +302,13 @@ export class LyricCache {
102302
* 移除缓存的歌词
103303
* @param key 缓存键
104304
*/
105-
remove(key: string): void {
305+
async remove(key: string): Promise<void> {
106306
try {
107307
const cacheKey = this.generateKey(key)
108-
localStorage.removeItem(cacheKey)
308+
if (typeof window !== 'undefined' && 'localStorage' in window) {
309+
window.localStorage.removeItem(cacheKey)
310+
}
311+
await this.removeFromIndexedDB(cacheKey)
109312
} catch (error) {
110313
console.error('Error removing cached lyric:', error)
111314
}
@@ -114,14 +317,20 @@ export class LyricCache {
114317
/**
115318
* 清除所有缓存的歌词
116319
*/
117-
clear(): void {
320+
async clear(): Promise<void> {
118321
try {
119-
const keys = Object.keys(localStorage)
120-
keys.forEach((key) => {
121-
if (key.startsWith(this.prefix)) {
122-
localStorage.removeItem(key)
123-
}
124-
})
322+
// 清除localStorage中的缓存
323+
if (typeof window !== 'undefined' && 'localStorage' in window) {
324+
const keys = Object.keys(window.localStorage)
325+
keys.forEach((key) => {
326+
if (key.startsWith(this.prefix)) {
327+
window.localStorage.removeItem(key)
328+
}
329+
})
330+
}
331+
332+
// 清除IndexedDB中的缓存
333+
await this.clearIndexedDB()
125334
} catch (error) {
126335
console.error('Error clearing cached lyrics:', error)
127336
}
@@ -132,8 +341,9 @@ export class LyricCache {
132341
* @param key 缓存键
133342
* @returns 是否存在有效缓存
134343
*/
135-
has(key: string): boolean {
136-
return this.get(key) !== null
344+
async has(key: string): Promise<boolean> {
345+
const data = await this.get(key)
346+
return data !== null
137347
}
138348
}
139349

0 commit comments

Comments
 (0)