鸿蒙OS&UniApp 实现图片上传与压缩功能#三方框架 #Uniapp

发布于:2025-05-14 ⋅ 阅读:(9) ⋅ 点赞:(0)

UniApp 实现图片上传与压缩功能

前言

在移动应用开发中,图片上传是一个非常常见的需求。无论是用户头像、朋友圈图片还是商品图片,都需要上传到服务器。但移动设备拍摄的图片往往尺寸较大,直接上传会导致流量消耗过大、上传时间过长,影响用户体验。因此,图片压缩成为了移动应用开发中的必备技能。

通过 UniApp 实现图片上传与压缩功能,既能满足用户体验需求,又能减轻服务器负担。今天就来分享一下我在实际项目中使用的图片上传与压缩方案,希望能对大家有所帮助。

技术方案分析

在 UniApp 中实现图片上传与压缩,主要涉及以下几个方面:

  1. 图片选择:通过 uni.chooseImage() 实现
  2. 图片压缩:通过 canvas 实现
  3. 图片上传:通过 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);
        
        // 绘制图片到canvas
        ctx.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) => {
        // 这里需要注意,返回的数据是字符串,需要手动转为 JSON
        let 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>

鸿蒙系统适配经验

前面已经简单提到了鸿蒙系统的适配,下面来详细说一下我在实际项目中遇到的问题和解决方案:

  1. 画布延迟问题:在鸿蒙系统上,canvas 绘制后转图片需要更长的延迟时间,建议延长 setTimeout 时间。

  2. 文件系统差异:有些文件路径的处理方式可能与 Android 有所不同,建议使用 UniApp 提供的 API 进行文件操作,而不要直接操作路径。

  3. 图片格式支持:在鸿蒙系统上,对 WebP 等格式的支持可能有限,建议统一使用 JPG 或 PNG 格式。

  4. 内存管理:鸿蒙系统对内存的管理略有不同,处理大图片时需要注意内存释放。可以在完成上传后,主动清空临时图片:

// 清空临时文件
clearTempFiles() {
  for (let item of this.imageList) {
    // 如果存在压缩后的临时文件,尝试删除
    if (item.compressedPath && item.compressedPath !== item.path) {
      uni.removeSavedFile({
        filePath: item.compressedPath,
        complete: () => {
          console.log('清理临时文件');
        }
      });
    }
  }
}

总结

通过本文介绍的方法,我们可以在 UniApp 中实现图片上传与压缩功能,主要包括以下几个步骤:

  1. 使用 uni.chooseImage() 选择图片
  2. 使用 canvas 进行图片压缩
  3. 使用 uni.uploadFile() 上传图片
  4. 添加进度显示和预览功能
  5. 针对鸿蒙系统做特殊适配

在实际项目中,可以根据需求调整压缩参数,比如对头像类图片可以压缩得更小,而对需要展示细节的商品图片,可以保留更高的质量。

希望本文能够帮助大家更好地实现 UniApp 中的图片上传与压缩功能。如果有任何问题或建议,欢迎在评论区交流讨论!

参考资料

  1. UniApp 官方文档:https://uniapp.dcloud.io/
  2. Canvas API 参考:https://uniapp.dcloud.io/api/canvas/CanvasContext