侧边栏壁纸
博主头像
慢行的骑兵博主等级

贪多嚼不烂,欲速则不达

  • 累计撰写 32 篇文章
  • 累计创建 27 个标签
  • 累计收到 1 条评论

目 录CONTENT

文章目录

Uniapp打包H5端实现图片缓存方案

慢行的骑兵
2025-03-06 / 0 评论 / 0 点赞 / 23 阅读 / 1,897 字

一.需求

  • 图片缓存这个需求非常常见,相对于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
0

评论区