一.需求
- 图片缓存这个需求非常常见,相对于APP而言,H5端的图片缓存实现方案相对较为复杂一点,这里自己将使用localforage的方式来实现Uniapp打包H5端的图片缓存方案;
- 对于localforage的机制和优缺点这里就不做详细的说明,就强调一点:localforage适合存储较大级的数据;
- 效果图:

二.实现细节、方案(代码环节)
- 执行命令:npm install localforage,并封装一个localforageConfig.js文件,代码如下:
import localforage from 'localforage';
// H5 端配置 localforage
if (process.env.VUE_APP_PLATFORM === 'h5') {
localforage.config({
driver: [localforage.INDEXEDDB, localforage.WEBSQL, localforage.LOCALSTORAGE], // 优先使用 IndexedDB
name: 'ImageCacheDB', // 数据库名称
storeName: 'image_cache', // 存储表名称
});
}
var tempSize = uni.getStorageSync("自定义key")
let totalCacheSize = tempSize == '' ? 0 : tempSize; // 记录缓存总大小
const CACHE_SIZE_LIMIT = 30 * 1024 * 1024; // 30MB
async function calculateBase64CacheSize() {
try {
const keys = await localforage.keys();
let totalBytes = 0;
for (const key of keys) {
const value = await localforage.getItem(key);
// 计算键的字节数
const keyBytes = getBytes(key);
// 计算值的字节数(假设value为base64字符串)
const valueBytes = calculateBase64Bytes(value);
totalBytes += keyBytes + valueBytes;
}
return totalBytes;
} catch (error) {
return '0B';
}
}
function getBytes(str) {
let bytes = str.length;
for (let i = 0; i < str.length; i++) {
if (str.charCodeAt(i) > 255) bytes++;
}
return bytes;
}
function calculateBase64Bytes(base64Str) {
const pureBase64 = base64Str.replace(/^data:\w+\/\w+;base64,/, '').replace(/=/g, '');
return Math.ceil((pureBase64.length * 3) / 4);
}
function formatSize(bytes) {
const units = ['B', 'KB', 'MB', 'GB'];
if (bytes <= 0) return '0B';
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return (bytes / Math.pow(1024, i)).toFixed(2) + units[i];
}
/**
* 缓存图片(仅做H5端,app端更换其它方案 APP端使用-唯一的不足,预加载效果不理想:https://ext.dcloud.net.cn/plugin?id=10935)
* @param {string} imageUrl - 图片的在线URL
* @param {number} [scale=0.75] - 宽高缩放比(仅H5端有效)
* @param {number} [quality=0.8] - 图片压缩质量(仅H5端有效)
* @returns {Promise<string>} - 返回Base64格式的图片数据(H5)或本地路径(APP)
*/
async function cacheImage(imageUrl, scale = 0.75, quality = 0.8) {
if (process.env.VUE_APP_PLATFORM === 'h5') {
if (process.env.NODE_ENV === 'development') {
return imageUrl
}
// H5 端:使用 localforage 缓存 Base64
return cacheImageH5(imageUrl, scale, quality);
} else {
// throw new Error('不支持的平台');
return imageUrl
}
}
/**
* 获取所有缓存的键
* @returns {Promise<string[]>} - 返回所有缓存的键
*/
function getAllCacheKeys() {
return new Promise((resolve, reject) => {
const keys = [];
plus.storage.getLength((length) => {
for (let i = 0; i < length; i++) {
const key = plus.storage.key(i);
keys.push(key);
}
resolve(keys);
}, (error) => {
reject(error);
});
});
}
/**
* 获取文件大小
* @param {string} filePath - 文件路径
* @returns {Promise<number>} - 返回文件大小(字节)
*/
function getFileSize(filePath) {
return new Promise((resolve, reject) => {
plus.io.getFileInfo({
filePath,
success: (info) => {
resolve(info.size); // 返回文件大小
},
fail: (error) => {
reject(error);
},
});
});
}
/**
* 统计缓存图片文件的总大小
* @returns {Promise<number>} - 返回总缓存大小(字节)
*/
async function getAppTotalCacheSize() {
try {
const keys = await getAllCacheKeys(); // 获取所有缓存的键
let totalSize = 0;
// 遍历所有键,获取对应的文件大小
for (const key of keys) {
const filePath = plus.storage.getItem(key); // 获取文件路径
if (filePath) {
const size = await getFileSize(filePath); // 获取文件大小
totalSize += size; // 累加文件大小
}
}
return totalSize;
} catch (error) {
throw error;
}
}
/**
* 将图片URL转换为Base64并缓存到本地
* @param {string} imageUrl - 图片的在线URL
* @returns {Promise<string>} - 返回Base64格式的图片数据
*/
async function cacheImageH5(imageUrl, scale = 0.75, quality = 0.8) {
try {
// 检查是否已缓存
let cachedImage = await localforage.getItem(imageUrl);
if (cachedImage) {
// console.log("获取图片信息 从缓存中获取图片 cachedImage " + cachedImage)
if (cachedImage.indexOf('data:image') != 0) {
//判断是否有前缀/没有则拼接(图片类型png,jpg按自己返回参数情况修改)
cachedImage = 'data:image/jpeg;base64,' + cachedImage
}
//base64 图片显示问题,base64码可能过长,有可能存在换行符\r \n
cachedImage = cachedImage.replace(/[\r\n]/g, "")
return cachedImage;
}
// 未缓存,则从网络获取
const response = await fetch(imageUrl);
let blob = new Blob([response.data], {
type: 'image/jpeg'
});
//压缩
let base64 = await compressImage(blob, scale, quality);
// console.log("获取图片信息 url转blob成功")
// 计算Base64数据的大小
const size = base64.length * 0.75; // Base64编码后的大小约为原始数据的1.33倍
if (totalCacheSize + size > CACHE_SIZE_LIMIT) {
await clearOldCache(); // 清理旧缓存
}
// 缓存到本地
await localforage.setItem(imageUrl, base64);
totalCacheSize += size; // 更新缓存总大小
if (base64.indexOf('data:image') != 0) {
//判断是否有前缀/没有则拼接(图片类型png,jpg按自己返回参数情况修改)
base64 = 'data:image/jpeg;base64,' + base64
}
// console.log("获取图片信息 缓存到本地 3")
return base64.replace(/[\r\n]/g, "");
} catch (error) {
// console.log("获取图片信息 图片缓存失败:", error)
throw error;
}
}
/**
* 清理旧缓存,直到缓存总大小低于限制
*/
async function clearOldCache() {
const keys = await localforage.keys();
while (totalCacheSize > CACHE_SIZE_LIMIT && keys.length > 0) {
const oldestKey = keys.shift(); // 移除最早的缓存
const item = await localforage.getItem(oldestKey);
const size = item.length * 0.75; // 计算缓存大小
await localforage.removeItem(oldestKey);
totalCacheSize -= size; // 更新缓存总大小
console.log('移除缓存:', oldestKey);
}
}
/**
* H5 端图片压缩
*/
function compressImage(blob, scale, quality) {
return new Promise((resolve, reject) => {
const img = new Image();
const reader = new FileReader();
reader.onload = () => {
img.src = reader.result;
};
reader.onerror = reject;
reader.readAsDataURL(blob);
img.onload = () => {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const width = img.width * scale;
const height = img.height * scale;
canvas.width = width;
canvas.height = height;
ctx.drawImage(img, 0, 0, width, height);
canvas.toBlob(
(blob) => {
const reader = new FileReader();
reader.onload = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
},
'image/jpeg',
quality
);
};
img.onerror = reject;
});
}
/**
* 清除指定图片的缓存
* @param {string} imageUrl - 图片的在线URL
*/
async function clearImageCache(imageUrl) {
try {
await localforage.removeItem(imageUrl);
console.log('图片缓存已清除:', imageUrl);
} catch (error) {
console.error('清除缓存失败:', error);
}
}
/**
* 清除所有图片缓存
*/
async function clearAllImageCache() {
try {
await localforage.clear();
console.log('所有图片缓存已清除');
} catch (error) {
console.error('清除所有缓存失败:', error);
}
}
/**
* 将Blob对象转换为Base64字符串
* @param {Blob} blob - Blob对象
* @returns {Promise<string>} - Base64格式的字符串
*/
function blobToBase64(blob) {
return new Promise((resolve, reject) => {
const reader = new FileReader();
reader.onloadend = () => resolve(reader.result);
reader.onerror = reject;
reader.readAsDataURL(blob);
});
}
async function fetch(imageUrl) {
console.log("获取图片信息 fetch 1")
return new Promise((resolve, reject) => {
uni.request({
url: imageUrl,
method: "GET",
responseType: 'arraybuffer', // 设置响应类型为arraybuffer
success: (res) => {
console.log("获取图片信息 fetch 2 ")
resolve(res);
},
fail: (err) => {
console.log("获取图片信息 fetch 3")
reject(err);
}
});
});
}
// 导出方法
export {
cacheImage,
clearImageCache,
clearAllImageCache,
calculateBase64CacheSize,
getAppTotalCacheSize
};
- 该js文件中包含了:图片压缩,在线图片url转blob,计算base64的大小,图片缓存达到了指定阈值清除旧的缓存;
- 使用示例(将在线url进行转换,然后给image标签通过上方的js文件中的cacheImage方法设置cacheImageInfo字段):
api接口名称(params, {
custom: {
catch: true
},
}).then(async res => {
var arr = res.records ? res.records : [];
// #ifdef H5
for (let i = 0; i < arr.length; i++) {
var temp = arr[i]
temp.cacheImageInfo = await cacheImage(temp.imageUrl)//将在线url进行转换,然后给image标签设置cacheImageInfo字段
}
// #endif
this.$refs.paging.complete(arr);
}).catch(e => {
this.$refs.paging.complete(false);
});
三.总结
- 只需要将步骤二中的js文件复制到项目中,然后按照使用说明就可以快速地实现H5端的离线缓存。对于清除旧的缓存的方法可以改成LRU方案,每一个缓存下来的图片增加一个时间标记,同时有新缓存时检查一下缓存大小是否超过阈值,然后更改清除旧的缓存的逻辑即可;而APP端的离线缓存建议使用插件市场中的组件,可以选择image-cache;
评论区