uniapp拍照上传图片h5

基于Uniapp H5端的图片拍照、压缩(Canvas)、上传全流程实现方案,优化DOM操作与异步逻辑。

Step 1: 界面代码实现

1
2
3
4
5
6
7
8
9
<template>
	<view class="body">
		<view class="btn">
			<view class="picture-img" ref="uploadContent" id="uploadContent" @click="takePhotos()">
				<div class="loader-19" v-if="loader"></div>点击修改
			</view>
		</view>
	</view>
</template>

Step 2: 使用到的方法

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
takePhotos() {
    const _that = this;
    if (document.getElementById("take-picture") == null) {
        let input = _that.createInputAndSetAttribute();
        _that.$refs.uploadContent.$el.appendChild(input);
        input.onchange = async (event) => {
            _that.loader = true;
            var files = event.target.files
            if (files && files.length > 0) {
                const file = files[0];
                // 压缩图片需要的一些元素和对象
                var reader = new FileReader(),
                    img = new Image();
                // 缩放图片需要的canvas
                var canvas = document.createElement('canvas');
                var context = canvas.getContext('2d');
                // base64地址图片加载完毕后
                img.onload = function() {
                    // 图片原始尺寸
                    var originWidth = this.width;
                    var originHeight = this.height;
                    // 最大尺寸限制
                    var maxWidth = 960,
                        maxHeight = 1280;
                    // 目标尺寸
                    var targetWidth = originWidth,
                        targetHeight = originHeight;
                    // 图片尺寸超过400x400的限制
                    if (originWidth > maxWidth || originHeight > maxHeight) {
                        if (originWidth / originHeight > maxWidth / maxHeight) {
                            // 更宽,按照宽度限定尺寸
                            targetWidth = maxWidth;
                            targetHeight = Math.round(maxWidth * (originHeight / originWidth));
                        } else {
                            targetHeight = maxHeight;
                            targetWidth = Math.round(maxHeight * (originWidth / originHeight));
                        }
                    }
                    // canvas对图片进行缩放
                    canvas.width = targetWidth;
                    canvas.height = targetHeight;
                    // 清除画布
                    context.clearRect(0, 0, targetWidth, targetHeight);
                    // 图片压缩
                    context.drawImage(img, 0, 0, targetWidth, targetHeight);
                    // canvas转为blob并上传
                    canvas.toBlob(async (blob) => {
                        const newFile = new File([blob], '123.jpg', {
                            type: blob.type
                        })
                        const imgPath = await upload(newFile, file.path, `/qy/photo`)
                        const detectionRes = await detection(imgPath)
                        await editFacePhoto(detectionRes).then(res => {
                            if (res.code === 0) {
                                _that.photoSrc = res.imgUrl
                                _that.$u.toast("上传成功!", 2000)
                            } else {
                                _that.$u.toast(res.msg, 3000)
                            }
                        })
                        _that.loader = false
                        _that.removeDocument()
                    }, file.type || 'image/png');
                };
                // 文件base64化,以便获知图片原始尺寸
                reader.onload = function(e) {
                    img.src = e.target.result;
                };
                reader.readAsDataURL(file);
            }
        }
    }
    document.querySelector("#take-picture").click();
}

// 创建input并设置input的属性    
createInputAndSetAttribute() {
    var input = document.createElement('input');
    input.type = 'file';
    input.id = 'take-picture'
    input.accept = 'image/*';
    // input.capture = 'user';
    input.style.opacity = 0;
    input.style.position = 'absolute';
    input.style.top = 0;
    input.style.left = 0;
    input.style.width = '50px';
    input.style.height = '50px';
    return input;
}

// 移除input节点
removeDocument() {
    const uploadContent = document.getElementById("uploadContent");
    const takePicture = document.getElementById("take-picture");
    uploadContent.removeChild(takePicture);
}

可参考: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/input/file

目前的实现方式有几个优化点:

  1. Vue 违规操作:在 Vue/Uniapp 中频繁使用 document.createElement、appendChild 和 removeChild 是不推荐的(这是 jQuery 时代的思维),应该利用 Vue 的模板语法。
  2. 回调地狱:FileReader、Image.onload、canvas.toBlob 层层嵌套,代码难以维护。
  3. 逻辑耦合:压缩逻辑、上传逻辑和 UI 交互混在一起。 以下是优化后的版本,采用 Vue 数据驱动 的方式,并将压缩逻辑封装为 Promise,使代码清晰易读。

优化思路

  1. 移除 DOM 操作:将 <input type="file"> 直接写在 <template> 中并通过 v-show 隐藏,通过 ref 触发点击,避免动态创建销毁 DOM。
  2. 异步扁平化:将图片加载和 Canvas 压缩逻辑封装成 Promise,使用 async/await 替代回调函数。
  3. 功能拆分:将“选择图片”、“压缩图片”、“上传业务”拆分为独立方法。

Step 1: 界面代码实现

使用 ref 引用隐藏的 input 元素,保持页面结构整洁。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
<template>
  <view class="body">
    <view class="btn">
      <!-- 触发按钮 -->
      <view class="picture-img" @click="triggerSelect">
        <div class="loader-19" v-if="loader"></div>
        {{ loader ? '处理中...' : '点击修改' }}
      </view>
      
      <!-- 隐藏的原生Input,用于H5调用相机/文件 -->
      <!-- accept="image/*" 限制图片,capture="user" 可选:强制调用前置摄像头 -->
      <input 
        type="file" 
        ref="fileInput" 
        accept="image/*" 
        class="hidden-input" 
        @change="handleFileChange" 
      />
    </view>
  </view>
</template>

<style scoped>
.hidden-input {
  display: none; /* 隐藏input */
}
/* 你的其他样式... */
</style>

Step 2: 逻辑实现 (Script)

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
export default {
  data() {
    return {
      loader: false,
      photoSrc: ''
    };
  },
  methods: {
    // 1. 触发文件选择
    triggerSelect() {
      // 在Uniapp H5中,通过$refs获取原生DOM元素并点击
      // 注意:App端不支持此操作,App端请使用 uni.chooseImage
      this.$refs.fileInput.click();
    },

    // 2. 监听文件改变
    async handleFileChange(event) {
      const files = event.target.files;
      if (!files || files.length === 0) return;

      this.loader = true;
      const file = files[0];

      try {
        // A. 压缩图片
        const { blob, filename } = await this.compressImage(file);
        
        // B. 构造新的File对象 (解决部分浏览器兼容性)
        const newFile = new File([blob], filename || 'compressed.jpg', { type: blob.type });

        // C. 执行上传业务
        await this.uploadProcess(newFile, file.path); // file.path 在H5 input中可能不存在,视业务需求调整
        
      } catch (error) {
        console.error("处理失败", error);
        this.$u.toast("图片处理失败", 2000);
      } finally {
        this.loader = false;
        // 清空value,确保下次选择同一张图也能触发change事件
        event.target.value = ''; 
      }
    },

    // 3. 核心:图片压缩工具函数 (返回Promise)
    compressImage(file) {
      return new Promise((resolve, reject) => {
        const reader = new FileReader();
        reader.readAsDataURL(file);
        
        reader.onload = (e) => {
          const img = new Image();
          img.src = e.target.result;
          
          img.onload = () => {
            // 原始尺寸
            const originWidth = img.width;
            const originHeight = img.height;
            
            // 目标尺寸配置
            const maxWidth = 960;
            const maxHeight = 1280;
            let targetWidth = originWidth;
            let targetHeight = originHeight;

            // 计算缩放比例
            if (originWidth > maxWidth || originHeight > maxHeight) {
              if (originWidth / originHeight > maxWidth / maxHeight) {
                targetWidth = maxWidth;
                targetHeight = Math.round(maxWidth * (originHeight / originWidth));
              } else {
                targetHeight = maxHeight;
                targetWidth = Math.round(maxHeight * (originWidth / originHeight));
              }
            }

            // Canvas 绘制
            const canvas = document.createElement('canvas');
            const context = canvas.getContext('2d');
            canvas.width = targetWidth;
            canvas.height = targetHeight;
            context.clearRect(0, 0, targetWidth, targetHeight);
            context.drawImage(img, 0, 0, targetWidth, targetHeight);

            // 导出 Blob
            canvas.toBlob((blob) => {
              if (blob) {
                resolve({ blob, filename: file.name });
              } else {
                reject(new Error('Canvas to Blob failed'));
              }
            }, file.type || 'image/jpeg', 0.8); // 0.8 为压缩质量
          };
          
          img.onerror = (err) => reject(err);
        };
        
        reader.onerror = (err) => reject(err);
      });
    },

    // 4. 上传业务逻辑
    async uploadProcess(file, originalPath) {
      // 假设 upload, detection, editFacePhoto 已导入或定义
      const imgPath = await upload(file, originalPath, `/qy/photo`);
      const detectionRes = await detection(imgPath);
      
      const res = await editFacePhoto(detectionRes);
      if (res.code === 0) {
        this.photoSrc = res.imgUrl;
        this.$u.toast("上传成功!", 2000);
      } else {
        this.$u.toast(res.msg, 3000);
      }
    }
  }
}

关键改进点说明

  1. Input 复用:
  • 原代码每次点击都 createElement,不仅性能低,而且在部分安卓 WebView 中可能因为 DOM 未完全插入而无法唤起相机。
  • 新代码将 <input> 静态化,利用 event.target.value = '' 重置状态,确保稳定性。
  1. Promise 封装:
  • 原代码在 img.onload 内部嵌套了大量业务逻辑。
  • 新代码将压缩逻辑提取为 compressImage,返回 Promise,使得主流程 handleFileChange 可以使用 await 顺序执行,逻辑一目了然。
  1. 兼容性处理:
  • 增加了 canvas.toBlob 的质量参数(0.8),可进一步减小体积。
  • 增加了 inputdisplay: none 样式,避免页面出现奇怪的占位。
comments powered by Disqus