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