记录一次前端文件缓存问题
因成本问题服务器带宽只有百兆,这就造成浏览器访问服务器PDF时间过长问题。
为解决该问题,就想到了浏览器304状态功能。
在后端服务增加了文件返回响应头
Cache-Control: public, max-age=31536000, immutable
经过测试在第二次访问文件时请求状态为304,说明文件缓存成功。
但经过后续测试发现一个问题,超过一定大小的文件不会走浏览器缓存,经过多次测试发现谷歌浏览器304缓存文件的大小为25MB,这明显不符合我们的需求,只能去想其他的方案。
查看文件请求的信息发现ETag属性可以控制文件是否缓存,但超过25MB的请求信息没有带,我们就想办法获取了第一次网络请求中的ETag信息,用于缓存请求中,经过不懈的努力,终于让超过25MB的文件请求带上了ETag请求信息,但发现浏览器还是会走网络请求而不会走缓存,经查找资料也没有发现绕过这一浏览器规则的地方,没办法,这个方案只能放弃。
后续还使用了Service Worker功能,想做离线访问发现也不行。
最后发现了一个浏览器自带的功能:IndexedDB,也不知道是什么时候上线,发现可以处理该问题。
最后附上代码
indexDb.ts
interface PdfCacheEntry {key: string;blob: Blob;timestamp: number; // 缓存时间戳ttl: number; // 过期时间(毫秒),默认7天
}
// IndexedDB 配置
const DB_CONFIG = {name: 'PdfCacheDatabase',version: 1,storeName: 'pdfCache',
};
// 打开数据库连接
export const openDatabase = (): Promise<IDBDatabase> => {return new Promise((resolve, reject) => {const request = indexedDB.open(DB_CONFIG.name, DB_CONFIG.version);// 数据库版本升级时触发request.onupgradeneeded = (event) => {const db = (event.target as IDBOpenDBRequest).result;// 如果存储对象不存在则创建if (!db.objectStoreNames.contains(DB_CONFIG.storeName)) {db.createObjectStore(DB_CONFIG.storeName, { keyPath: 'key' });// 使用自动生成的整数ID作为主键,避免键路径错误const store = db.createObjectStore(DB_CONFIG.storeName, {keyPath: 'id',autoIncrement: true,});// 创建时间戳索引,用于查询最早的记录store.createIndex('byTimestamp', 'timestamp', { unique: false });// 创建业务键索引,便于查询store.createIndex('byKey', 'key', { unique: false });}};request.onsuccess = (event) => {resolve((event.target as IDBOpenDBRequest).result);};request.onerror = (event) => {console.error('IndexedDB 打开失败:', (event.target as IDBOpenDBRequest).error);reject((event.target as IDBOpenDBRequest).error);};});
};
// 从IndexedDB获取缓存的PDF
export const getCachedPdf = async (key: string): Promise<Blob | null> => {try {const db = await openDatabase();return new Promise((resolve) => {const transaction = db.transaction(DB_CONFIG.storeName, 'readonly');const store = transaction.objectStore(DB_CONFIG.storeName);const request = store.get(key);request.onsuccess = () => {const entry = request.result as PdfCacheEntry | undefined;if (entry) {// 检查是否过期const now = Date.now();if (now - entry.timestamp < entry.ttl) {resolve(entry.blob);return;} else {console.log(`PDF缓存已过期: ${key}`);// 删除过期缓存deleteCachedPdf(key);}}resolve(null);};request.onerror = () => {console.error('获取PDF缓存失败:', request.error);resolve(null);};});} catch (error) {console.error('获取PDF缓存出错:', error);return null;}
};
// 添加获取缓存总数的函数
const getCacheCount = async (db: IDBDatabase): Promise<number> => {return new Promise((resolve) => {const transaction = db.transaction(DB_CONFIG.storeName, 'readonly');const store = transaction.objectStore(DB_CONFIG.storeName);const request = store.count();request.onsuccess = () => {resolve(request.result);};request.onerror = () => {console.error('获取缓存数量失败:', request.error);resolve(0);};});
};// 删除最早的一条缓存记录
// 添加删除最早缓存记录的函数
const deleteOldestCache = async (db: IDBDatabase): Promise<boolean> => {return new Promise((resolve) => {const transaction = db.transaction(DB_CONFIG.storeName, 'readwrite');const store = transaction.objectStore(DB_CONFIG.storeName);const index = store.index('byTimestamp'); // 使用时间戳索引// 查找最早的记录(按时间戳升序)const request = index.openCursor(null, 'next');request.onsuccess = (event) => {const cursor = (event.target as IDBRequest<IDBCursorWithValue>).result;if (cursor) {// 删除找到的最早记录const deleteRequest = cursor.delete();deleteRequest.onsuccess = () => {console.log('已删除最早的缓存记录');resolve(true);};deleteRequest.onerror = () => {console.error('删除最早缓存失败:', deleteRequest.error);resolve(false);};} else {// 没有找到记录resolve(false);}};request.onerror = () => {console.error('获取最早缓存失败:', request.error);resolve(false);};});
};// 修改缓存PDF的函数,添加存储空间检查和清理逻辑
export const cachePdf = async (key: string, blob: Blob, ttl: number = 7 * 24 * 60 * 60 * 1000): Promise<boolean> => {const entry: PdfCacheEntry = {key,blob,timestamp: Date.now(),ttl};// 递归尝试存储函数const tryStore = async (db: IDBDatabase): Promise<boolean> => {return new Promise((resolve, reject) => {const transaction = db.transaction(DB_CONFIG.storeName, 'readwrite');const store = transaction.objectStore(DB_CONFIG.storeName);const request = store.put(entry);request.onsuccess = () => {console.log(`PDF已缓存到IndexedDB: ${key}`);resolve(true);};request.onerror = async () => {console.error('PDF缓存失败:', request.error);// 检查是否是存储空间不足错误if (request.error && request.error.name === 'QuotaExceededError') {console.log('存储空间不足,尝试删除最早的缓存记录...');// 删除最早的记录const deleted = await deleteOldestCache(db);if (deleted) {// 递归尝试再次存储resolve(await tryStore(db));} else {reject(new Error('存储空间不足且无法删除旧缓存'));}} else {reject(new Error(`缓存失败: ${request.error?.message}`));}};});};try {const db = await openDatabase();console.log('数据库已经连接上');return await tryStore(db);} catch (error) {console.error('缓存PDF出错:', error);return false;}
};// 删除过期或无效的缓存
export const deleteCachedPdf = async (url: string): Promise<boolean> => {try {const db = await openDatabase();return new Promise((resolve) => {const transaction = db.transaction(DB_CONFIG.storeName, 'readwrite');const store = transaction.objectStore(DB_CONFIG.storeName);const request = store.delete(url);request.onsuccess = () => {console.log(`已删除过期缓存: ${url}`);resolve(true);};request.onerror = () => {console.error('删除缓存失败:', request.error);resolve(false);};});} catch (error) {console.error('删除缓存出错:', error);return false;}
};// 从网络获取PDF
export const fetchPdf = async (url: string): Promise<Blob> => {try {const response = await fetch(url, {method: 'GET',headers: {Accept: 'application/pdf',},});if (!response.ok) {throw new Error(`请求失败: ${response.status} ${response.statusText}`);}const blob = await response.blob();// 验证是否为PDFif (!blob.type.includes('pdf')) {throw new Error('获取的文件不是PDF格式');}return blob;} catch (error) {console.error('从网络获取PDF失败:', error);throw error;}
};
缓存使用
import { openDatabase,getCachedPdf,cachePdf,deleteCachedPdf,fetchPdf } from '@/utils/cache/indexDb'
const pdfUrlObject = ref<string | null>(null);
const loading = ref<boolean>(false);
const error = ref<string | null>(null);// 处理PDF显示错误
const handlePdfError = () => {error.value = '无法显示PDF文件,可能是文件损坏或格式不支持';revokePdfUrl();
};// 释放Object URL
const revokePdfUrl = () => {if (pdfUrlObject.value) {URL.revokeObjectURL(pdfUrlObject.value);pdfUrlObject.value = null;}
};// 加载并显示PDF
const loadPdf = async (url,key) => {// 重置状态loading.value = true;error.value = null;revokePdfUrl();try {const urls = url; // 1. 尝试从缓存获取const cachedBlob = await getCachedPdf(key);if (cachedBlob) {console.log('从缓存加载PDF');pdfUrlObject.value = URL.createObjectURL(cachedBlob);return;}// 2. 缓存未命中,从网络获取console.log('从网络加载PDF');const blob = await fetchPdf(urls);// 3. 缓存到IndexedDBawait cachePdf(key, blob);// 4. 显示PDFpdfUrlObject.value = URL.createObjectURL(blob);} catch (err) {error.value = err instanceof Error ? err.message : '加载PDF时发生错误';} finally {loading.value = false;}
};const key=iId+'-'+bId
loadPdf(res,key)