记录一次前端文件缓存问题

因成本问题服务器带宽只有百兆,这就造成浏览器访问服务器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();

    // 验证是否为PDF
    if (!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. 缓存到IndexedDB
    await cachePdf(key, blob);
    
    // 4. 显示PDF
    pdfUrlObject.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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值