UniApp 实现图片上传与压缩功能
前言
在移动应用开发中,图片上传是一个非常常见的需求。无论是用户头像、朋友圈图片还是商品图片,都需要上传到服务器。但移动设备拍摄的图片往往尺寸较大,直接上传会导致流量消耗过大、上传时间过长,影响用户体验。因此,图片压缩成为了移动应用开发中的必备技能。
通过 UniApp 实现图片上传与压缩功能,既能满足用户体验需求,又能减轻服务器负担。今天就来分享一下我在实际项目中使用的图片上传与压缩方案,希望能对大家有所帮助。
技术方案分析
在 UniApp 中实现图片上传与压缩,主要涉及以下几个方面:
- 图片选择:通过
uni.chooseImage()
实现 - 图片压缩:通过 canvas 实现
- 图片上传:通过
uni.uploadFile()
实现
这个方案的优点是:
- 压缩在客户端进行,减轻了服务器压力
- 减少了网络流量,提高了上传速度
- 可以根据不同场景设置不同的压缩参数
具体实现
1. 图片选择
首先实现图片选择功能:
// 选择图片
chooseImage() {return new Promise((resolve, reject) => {uni.chooseImage({count: 9, // 最多可选择的图片张数sizeType: ['original', 'compressed'], // 可选择原图或压缩后的图片sourceType: ['album', 'camera'], // 从相册选择或使用相机拍摄success: (res) => {resolve(res.tempFilePaths);},fail: (err) => {reject(err);}});});
}
2. 图片压缩实现
压缩图片是整个功能的核心,我们使用 canvas 来实现:
/*** 图片压缩* @param {String} src 图片路径* @param {Number} quality 压缩质量(0-1)* @param {Number} maxWidth 最大宽度* @param {Number} maxHeight 最大高度*/
compressImage(src, quality = 0.8, maxWidth = 800, maxHeight = 800) {return new Promise((resolve, reject) => {// 获取图片信息uni.getImageInfo({src: src,success: (imgInfo) => {// 计算压缩后的尺寸let width = imgInfo.width;let height = imgInfo.height;// 等比例缩放if (width > maxWidth || height > maxHeight) {const ratio = Math.max(width / maxWidth, height / maxHeight);width = Math.floor(width / ratio);height = Math.floor(height / ratio);}// 创建canvas上下文const ctx = uni.createCanvasContext('compressCanvas', this);// 绘制图片到canvasctx.drawImage(src, 0, 0, width, height);// 将canvas转为图片ctx.draw(false, () => {setTimeout(() => {uni.canvasToTempFilePath({canvasId: 'compressCanvas',fileType: 'jpg',quality: quality,success: (res) => {// 获取压缩后的图片路径resolve(res.tempFilePath);},fail: (err) => {reject(err);}}, this);}, 300); // 延迟确保canvas绘制完成});},fail: (err) => {reject(err);}});});
}
在页面中需要添加对应的 canvas 元素:
<canvas canvas-id="compressCanvas" style="width: 0px; height: 0px; position: absolute; left: -1000px; top: -1000px;"></canvas>
3. 图片上传实现
图片上传时,我们往往需要添加额外的参数,比如表单字段、用户 token 等:
/*** 上传图片到服务器* @param {String} filePath 图片路径* @param {String} url 上传地址* @param {Object} formData 附加数据*/
uploadFile(filePath, url, formData = {}) {return new Promise((resolve, reject) => {uni.uploadFile({url: url,filePath: filePath,name: 'file', // 服务器接收的字段名formData: formData,header: {// 可以添加自定义 header,如 token'Authorization': 'Bearer ' + uni.getStorageSync('token')},success: (res) => {// 这里需要注意,返回的数据是字符串,需要手动转为 JSONlet data = JSON.parse(res.data);resolve(data);},fail: (err) => {reject(err);}});});
}
4. 完整的上传流程
将以上三个步骤组合,实现完整的图片上传流程:
// 实现完整的上传流程
async handleUpload() {try {// 显示加载提示uni.showLoading({title: '上传中...',mask: true});// 选择图片const imagePaths = await this.chooseImage();// 用于存储上传结果const uploadResults = [];// 循环处理每张图片for (let i = 0; i < imagePaths.length; i++) {// 压缩图片const compressedPath = await this.compressImage(imagePaths[i],0.7, // 压缩质量800, // 最大宽度800 // 最大高度);// 获取原图和压缩后的图片大小进行对比const originalInfo = await this.getFileInfo(imagePaths[i]);const compressedInfo = await this.getFileInfo(compressedPath);console.log(`原图大小: ${(originalInfo.size / 1024).toFixed(2)}KB, 压缩后: ${(compressedInfo.size / 1024).toFixed(2)}KB`);// 上传压缩后的图片const uploadResult = await this.uploadFile(compressedPath,'https://your-api.com/upload',{type: 'avatar', // 附加参数userId: this.userId});uploadResults.push(uploadResult);}// 隐藏加载提示uni.hideLoading();// 提示上传成功uni.showToast({title: '上传成功',icon: 'success'});// 返回上传结果return uploadResults;} catch (error) {uni.hideLoading();uni.showToast({title: '上传失败',icon: 'none'});console.error('上传错误:', error);}
}// 获取文件信息
getFileInfo(filePath) {return new Promise((resolve, reject) => {uni.getFileInfo({filePath: filePath,success: (res) => {resolve(res);},fail: (err) => {reject(err);}});});
}
进阶优化
以上代码已经可以基本满足图片上传与压缩需求,但在实际项目中,我们还可以进一步优化:
1. 添加图片预览功能
在上传前,通常需要让用户预览选择的图片:
// 预览图片
previewImage(current, urls) {uni.previewImage({current: current, // 当前显示图片的路径urls: urls, // 需要预览的图片路径列表indicator: 'number',loop: true});
}
2. 使用 uniCloud 上传
如果你使用 uniCloud 作为后端服务,可以利用其提供的云存储功能简化上传流程:
// 使用 uniCloud 上传
async uploadToUniCloud(filePath) {try {const result = await uniCloud.uploadFile({filePath: filePath,cloudPath: 'images/' + Date.now() + '.jpg'});return result.fileID; // 返回文件ID} catch (error) {throw error;}
}
3. 添加上传进度显示
对于大图片,添加上传进度能提升用户体验:
uploadFileWithProgress(filePath, url, formData = {}) {return new Promise((resolve, reject) => {const uploadTask = uni.uploadFile({url: url,filePath: filePath,name: 'file',formData: formData,success: (res) => {let data = JSON.parse(res.data);resolve(data);},fail: (err) => {reject(err);}});uploadTask.onProgressUpdate((res) => {console.log('上传进度', res.progress);// 更新进度条this.uploadProgress = res.progress;});});
}
4. 针对鸿蒙系统的适配
随着国产操作系统鸿蒙的普及,我们也需要考虑在鸿蒙系统上的兼容性。虽然目前 UniApp 官方还没有专门针对鸿蒙系统的适配文档,但我们可以通过一些方法来优化:
// 检测当前系统
checkSystem() {const systemInfo = uni.getSystemInfoSync();console.log('当前系统:', systemInfo.platform);// 鸿蒙系统目前会被识别为 android,可以通过 brand 和 model 辅助判断const isHarmonyOS = systemInfo.brand === 'HUAWEI' && /HarmonyOS/i.test(systemInfo.system);if (isHarmonyOS) {console.log('当前是鸿蒙系统');// 针对鸿蒙系统进行特殊处理// 例如:调整压缩参数、使用不同的 API 等}return systemInfo;
}
根据我的测试,在鸿蒙系统上,有时 canvas 绘制需要更长的延迟时间,可以适当调整:
// 针对鸿蒙系统的 canvas 延迟调整
const delay = isHarmonyOS ? 500 : 300;
setTimeout(() => {uni.canvasToTempFilePath({// 配置项...});
}, delay);
实际案例
下面是一个完整的实际案例,用于实现商品发布页面的图片上传功能:
<template><view class="container"><view class="image-list"><!-- 已选图片预览 --><view class="image-item" v-for="(item, index) in imageList" :key="index"><image :src="item.path" mode="aspectFill" @tap="previewImage(index)"></image><view class="delete-btn" @tap.stop="deleteImage(index)">×</view></view><!-- 添加图片按钮 --><view class="add-image" @tap="handleAddImage" v-if="imageList.length < 9"><text class="add-icon">+</text><text class="add-text">添加图片</text></view></view><!-- 上传按钮 --><button class="upload-btn" @tap="submitUpload" :disabled="imageList.length === 0">上传图片 ({{imageList.length}}/9)</button><!-- 压缩画布(隐藏) --><canvas canvas-id="compressCanvas" style="width: 0px; height: 0px; position: absolute; left: -1000px; top: -1000px;"></canvas><!-- 上传进度条 --><view class="progress-bar" v-if="isUploading"><view class="progress-inner" :style="{width: uploadProgress + '%'}"></view><text class="progress-text">{{uploadProgress}}%</text></view></view>
</template><script>
export default {data() {return {imageList: [], // 已选图片列表isUploading: false, // 是否正在上传uploadProgress: 0, // 上传进度isHarmonyOS: false // 是否鸿蒙系统};},onLoad() {// 检测系统const systemInfo = this.checkSystem();this.isHarmonyOS = systemInfo.brand === 'HUAWEI' && /HarmonyOS/i.test(systemInfo.system);},methods: {// 添加图片async handleAddImage() {try {const imagePaths = await this.chooseImage();// 添加到图片列表for (let path of imagePaths) {this.imageList.push({path: path,compressed: false,compressedPath: '',uploaded: false,fileID: ''});}} catch (error) {console.error('选择图片失败:', error);}},// 预览图片previewImage(index) {const urls = this.imageList.map(item => item.path);uni.previewImage({current: this.imageList[index].path,urls: urls});},// 删除图片deleteImage(index) {this.imageList.splice(index, 1);},// 提交上传async submitUpload() {if (this.imageList.length === 0) {uni.showToast({title: '请至少选择一张图片',icon: 'none'});return;}this.isUploading = true;this.uploadProgress = 0;uni.showLoading({title: '准备上传...',mask: true});try {// 上传结果const uploadResults = [];// 总进度let totalProgress = 0;// 遍历所有图片进行压缩和上传for (let i = 0; i < this.imageList.length; i++) {let item = this.imageList[i];// 如果还没压缩过,先压缩if (!item.compressed) {uni.showLoading({title: `压缩第 ${i+1}/${this.imageList.length} 张图片`,mask: true});try {const compressedPath = await this.compressImage(item.path,0.7,800,800);// 更新图片信息this.imageList[i].compressed = true;this.imageList[i].compressedPath = compressedPath;// 获取压缩前后的大小对比const originalInfo = await this.getFileInfo(item.path);const compressedInfo = await this.getFileInfo(compressedPath);console.log(`图片 ${i+1}: 原图 ${(originalInfo.size / 1024).toFixed(2)}KB, 压缩后 ${(compressedInfo.size / 1024).toFixed(2)}KB, 压缩率 ${((1 - compressedInfo.size / originalInfo.size) * 100).toFixed(2)}%`);} catch (error) {console.error(`压缩第 ${i+1} 张图片失败:`, error);// 如果压缩失败,使用原图this.imageList[i].compressedPath = item.path;this.imageList[i].compressed = true;}}// 准备上传uni.showLoading({title: `上传第 ${i+1}/${this.imageList.length} 张图片`,mask: true});try {// 使用压缩后的图片路径,如果没有则使用原图const fileToUpload = item.compressedPath || item.path;// 上传图片const result = await this.uploadFileWithProgress(fileToUpload,'https://your-api.com/upload',{type: 'product',index: i});// 更新图片信息this.imageList[i].uploaded = true;this.imageList[i].fileID = result.fileID || result.url;uploadResults.push(result);// 更新总进度totalProgress = Math.floor((i + 1) / this.imageList.length * 100);this.uploadProgress = totalProgress;} catch (error) {console.error(`上传第 ${i+1} 张图片失败:`, error);uni.showToast({title: `第 ${i+1} 张图片上传失败`,icon: 'none'});}}uni.hideLoading();this.isUploading = false;uni.showToast({title: '所有图片上传完成',icon: 'success'});// 返回上传结果,可以传给父组件或进行后续处理this.$emit('uploadComplete', uploadResults);} catch (error) {uni.hideLoading();this.isUploading = false;console.error('上传过程出错:', error);uni.showToast({title: '上传失败,请重试',icon: 'none'});}},// 其他方法实现(chooseImage, compressImage, uploadFile等,同前面的实现)}
};
</script><style lang="scss">
.container {padding: 20rpx;
}.image-list {display: flex;flex-wrap: wrap;
}.image-item {width: 220rpx;height: 220rpx;margin: 10rpx;position: relative;border-radius: 8rpx;overflow: hidden;image {width: 100%;height: 100%;}.delete-btn {position: absolute;top: 0;right: 0;width: 44rpx;height: 44rpx;background-color: rgba(0, 0, 0, 0.5);color: #ffffff;text-align: center;line-height: 44rpx;font-size: 32rpx;z-index: 10;}
}.add-image {width: 220rpx;height: 220rpx;margin: 10rpx;background-color: #f5f5f5;display: flex;flex-direction: column;justify-content: center;align-items: center;border-radius: 8rpx;border: 1px dashed #dddddd;.add-icon {font-size: 60rpx;color: #999999;}.add-text {font-size: 24rpx;color: #999999;margin-top: 10rpx;}
}.upload-btn {margin-top: 40rpx;background-color: #007aff;color: #ffffff;border-radius: 8rpx;&:disabled {background-color: #cccccc;}
}.progress-bar {margin-top: 30rpx;height: 40rpx;background-color: #f5f5f5;border-radius: 20rpx;overflow: hidden;position: relative;.progress-inner {height: 100%;background-color: #007aff;transition: width 0.3s;}.progress-text {position: absolute;top: 0;left: 0;right: 0;bottom: 0;display: flex;justify-content: center;align-items: center;font-size: 24rpx;color: #ffffff;}
}
</style>
鸿蒙系统适配经验
前面已经简单提到了鸿蒙系统的适配,下面来详细说一下我在实际项目中遇到的问题和解决方案:
-
画布延迟问题:在鸿蒙系统上,canvas 绘制后转图片需要更长的延迟时间,建议延长 setTimeout 时间。
-
文件系统差异:有些文件路径的处理方式可能与 Android 有所不同,建议使用 UniApp 提供的 API 进行文件操作,而不要直接操作路径。
-
图片格式支持:在鸿蒙系统上,对 WebP 等格式的支持可能有限,建议统一使用 JPG 或 PNG 格式。
-
内存管理:鸿蒙系统对内存的管理略有不同,处理大图片时需要注意内存释放。可以在完成上传后,主动清空临时图片:
// 清空临时文件
clearTempFiles() {for (let item of this.imageList) {// 如果存在压缩后的临时文件,尝试删除if (item.compressedPath && item.compressedPath !== item.path) {uni.removeSavedFile({filePath: item.compressedPath,complete: () => {console.log('清理临时文件');}});}}
}
总结
通过本文介绍的方法,我们可以在 UniApp 中实现图片上传与压缩功能,主要包括以下几个步骤:
- 使用
uni.chooseImage()
选择图片 - 使用 canvas 进行图片压缩
- 使用
uni.uploadFile()
上传图片 - 添加进度显示和预览功能
- 针对鸿蒙系统做特殊适配
在实际项目中,可以根据需求调整压缩参数,比如对头像类图片可以压缩得更小,而对需要展示细节的商品图片,可以保留更高的质量。
希望本文能够帮助大家更好地实现 UniApp 中的图片上传与压缩功能。如果有任何问题或建议,欢迎在评论区交流讨论!
参考资料
- UniApp 官方文档:https://uniapp.dcloud.io/
- Canvas API 参考:https://uniapp.dcloud.io/api/canvas/CanvasContext