赵飞扬 發表於 2026-1-14 15:34:00

uniapp+vue2+uview图片上传封装

<h1 id="-打造基于-uviewuniappvue-的高性能图片上传组件自动压缩--更加健壮的类型判断">🔥 打造基于 uView+uniapp+vue 的高性能图片上传组件(自动压缩 + 更加健壮的类型判断)</h1>
<h2 id="前言">前言</h2>
<p>在移动端开发(App/小程序/H5)中, 图片上传 是一个极其高频且容易产生性能瓶颈的场景。直接上传原图往往会带来以下问题:</p>
<ol>
<li>上传缓慢 :现在的手机拍照动辄 5MB-10MB,用户在非 WiFi 环境下体验极差。</li>
<li>体验不好 :大文件导致请求时间过长,容易超时。</li>
<li>服务器压力 :不仅占用大量带宽,还浪费存储空间。<br>
虽然 uView UI 的 u-upload 组件已经非常好用,但它默认不包含<strong>“上传前压缩” 的逻辑。今天我们就来手撸一个 “带自动压缩功能的图片上传组件”</strong>,不仅支持并发上传、进度显示,还具备更智能的图片类型判断逻辑。</li>
</ol>
<h2 id="-核心方案设计">🚀 核心方案设计</h2>
<p>我们的目标是封装一个通用组件 MyUpload ,实现以下流程:</p>
<ol>
<li>拦截选择 :监听 u-upload 的 afterRead 事件。</li>
<li>智能判断 :
<ul>
<li>类型检查 :不仅限于 jpg、png ,兼容所有图片格式。</li>
<li>阈值控制 :仅对超过指定大小(如 1MB,可自行调整)的图片进行压缩,小图直接上传,平衡清晰度与性能。</li>
</ul>
</li>
<li>核心压缩 :利用 Canvas (通过 helang-compress 插件) 进行压缩。</li>
<li>格式转换 :将压缩后的 Base64 转回二进制文件对象(关键步骤,否则 uni.uploadFile 无法识别)。</li>
<li>统一上传 :处理上传进度、成功回填、失败自动移除。</li>
</ol>
<h2 id="️-核心代码实现">🛠️ 核心代码实现</h2>
<h3 id="1-组件结构">1. 组件结构</h3>
<p>我们基于 u-upload 进行二次封装,同时引入压缩插件。</p>
<pre><code class="language-javascript">&lt;template&gt;
&lt;view&gt;
    &lt;u-upload
      :fileList="fileList1"
      @afterRead="afterRead"
      @delete="deletePic"
      name="1"
      multiple
      :maxCount="maxCount"
      :accept="accept"
    &gt;&lt;/u-upload&gt;
   
    &lt;!-- 隐形画布:用于图片压缩 --&gt;
    &lt;helang-compress ref="helangCompress"&gt;&lt;/helang-compress&gt;
&lt;/view&gt;
&lt;/template&gt;
</code></pre>
<h3 id="2-更加健壮的压缩判断逻辑">2. 更加健壮的压缩判断逻辑✨</h3>
<pre><code class="language-javascript">// 上传核心逻辑
async uploadFilePromise(file, lists) {
    let OriginalUrl = file.url
    let afterCompressFile = null
    let ifcompress = false
   
    // 1. file.type 判空保护:防止部分安卓机型或特殊场景下 type 丢失导致报错
    // 2. 模糊匹配 'image':覆盖 image/png, image/jpeg, image/gif 等所有图片类型
    // 3. 大小阈值:只有超过 1MB (1024KB) 才压缩,小图直接上传,阈值可自行调整
    if (file.type &amp;&amp; file.type.indexOf('image') != -1 &amp;&amp; file.size / 1024 &gt; 1024) {
      // 标记为需要压缩
      ifcompress = true
      
      // 调用压缩插件,返回值是压缩后的 Base64 字符串
      let afterCompressBase64 = await this.$refs.helangCompress.compress({
            src: OriginalUrl,
            maxSize: 1024,   // 限制最大分辨率
            fileType: 'jpg', // 统一输出为 jpg 减少体积
            quality: 0.8,    // 压缩质量
            minSize: 640   // 最小尺寸保护
      })
      
      // uni.uploadFile 不支持直接传 Base64,必须转为 File 对象
      afterCompressFile = await base64ToFile(afterCompressBase64, file.name)
    }
   
    return new Promise((resolve, reject) =&gt; {
      uni.uploadFile({
            url: config.upLoadUrl,
            name: 'file',
            // 如果压缩了,filePath 传 null(或根据平台差异调整),file 传转换后的对象
            // 如果没压缩,直接用原路径
            filePath: !ifcompress ? file.url : file.name,
            file: !ifcompress ? null : afterCompressFile,
            header: {
                'Authorization': 'Bearer ' + uni.getStorageSync('Token') ?? '',
            },
            success: (res) =&gt; {
                // 处理服务端返回
                let data = JSON.parse(res.data);
                if(data.code == 200){
                  resolve(data.url)
                } else {
                  uni.$u.toast(data.message)
                  reject(data)
                }
            },
            fail: (err) =&gt; {
                console.log("Upload failed", err)
                reject(err)
            }
      });
    })
}
</code></pre>
<h3 id="3-队列上传与状态管理">3. 队列上传与状态管理</h3>
<p>实时更新 UI 的 loading 状态,并在失败时自动清理,这点蛮重要的,很多时候上传失败但是组件上展示是有图片的(这是本地的blob图片,并不是真正上传服务器后的图片)。</p>
<pre><code class="language-javascript">async afterRead(event) {
    // 1. 预处理:将新选择的文件加入列表,状态设为 'uploading'
    let lists = [].concat(event.file)
    let fileListLen = this[`fileList${event.name}`].length
    lists.map((item) =&gt; {
      this[`fileList${event.name}`].push({
            ...item,
            status: 'uploading',
            message: '上传中'
      })
    });

    // 2. 串行上传(也可以改为 Promise.all 并行,视服务器压力而定)
    for (let i = 0; i &lt; lists.length; i++) {
      try {
            // 等待单个文件上传(含压缩耗时)
            const result = await this.uploadFilePromise(lists, lists)
            
            // 3. 成功回调:更新列表状态为 success,并回填 URL
            let item = this[`fileList${event.name}`]
            this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
                status: 'success',
                message: '',
                url: result
            }))
            fileListLen++
      } catch(e) {
            // 4. 失败回滚:移除该项,避免 UI 显示错误的占位
            this[`fileList${event.name}`].splice(fileListLen, 1)
            uni.$u.toast('上传失败,请重试')
      }
    }
   
    // 5. 通知父组件更新数据
    this.emitInput(this[`fileList${event.name}`])
}
</code></pre>
<h2 id="️-避坑指南--最佳实践">⚠️ 避坑指南 &amp; 最佳实践</h2>
<ol>
<li>H5 与 App 的差异 :
<ul>
<li>在 H5 端,图片选择后通常是 Blob URL;在 App 端是绝对路径。 uni.uploadFile 在处理 Base64 转成的 File 对象时,不同平台的参数传递略有不同(主要体现在 filePath 和 file 字段的互斥使用上),代码中通过 !ifcompress ? ... : ... 做了很好的兼容。</li>
</ul>
</li>
<li>Base64 转 File :
<ul>
<li>压缩插件返回的是 Base64 字符串,必须通过 base64ToFile (利用 uni.getFileSystemManager 或 Blob ) 转换后才能上传,否则服务端无法解析。</li>
</ul>
</li>
<li>内存泄漏 :
<ul>
<li>如果在循环中大量进行 Canvas 操作,记得及时销毁或重用 Canvas 上下文。本方案使用了 helang-compress 插件,内部处理了 Canvas 的生命周期。</li>
</ul>
</li>
<li>用户体验 :
<ul>
<li>务必在压缩时给用户反馈(如“处理中...”),因为大图压缩可能需要几百毫秒到 1 秒的时间。</li>
</ul>
</li>
</ol>
<h2 id="完整代码">完整代码</h2>
<pre><code class="language-javascript">/* File Info
* 二次封装上传图片组件
*/
&lt;template&gt;
        &lt;view class=""&gt;
                &lt;u-upload :fileList="fileList1" @afterRead="afterRead" @delete="deletePic" name="1" multiple
                        :maxCount="maxCount" :accept="accept"&gt;&lt;/u-upload&gt;
                &lt;helang-compress ref="helangCompress"&gt;&lt;/helang-compress&gt;
                &lt;compress ref="compress" /&gt;
        &lt;/view&gt;

&lt;/template&gt;

&lt;script&gt;
        import {
                base64ToFile
        } from '@/utils/compress.js'
        import helangCompress from '@/components/helang-compress/helang-compress';
        export default {
                // props: ['maxCount', 'value'],
                components: {
                        helangCompress,
                },
                props: {
                        maxCount: {
                                type: Number,
                                default: 1
                        },
                        value: {
                                type: String,
                                default: ''
                        },
                        accept: {
                                type: String,
                                default: 'image'
                        },
                        //如果需要循环使用组件,index从父组件串过来,然后再传回父组件,以便父组件区分上传的图片是循环中的第几项
                        index: {
                                type: Number,
                                default: null
                        }
                },
                data() {
                        return {
                                fileList1: [],
                        }
                },
                onLoad() {

                },

                methods: {
                        //对向父组件通信方法封装
                        emitInput(list) {
                                const resUrl = []
                                // const list=this[`fileList${event.name}`]
                                list.forEach(item =&gt; {
                                        resUrl.push(item.url)
                                })
                                this.$emit('input', resUrl.join(','))
                                //父组件需要循环渲染此组件的时候(index!==null)才触发
                                this.index !== null &amp;&amp; this.$emit('sendIndex', {
                                        index: this.index,
                                        photo: resUrl.join(',')
                                })
                        },
                        // 删除图片
                        deletePic(event) {
                                this[`fileList${event.name}`].splice(event.index, 1);
                                // this.emitInput()
                                this.emitInput(this[`fileList${event.name}`])
                        },
                        // 新增图片
                        async afterRead(event, filelists) {
                                console.log("event", event, filelists)
                                // 当设置 multiple 为 true 时, file 为数组格式,否则为对象格式
                                let lists = [].concat(event.file)
                                let fileListLen = this[`fileList${event.name}`].length
                                lists.map((item) =&gt; {
                                        this[`fileList${event.name}`].push({
                                                ...item,
                                                status: 'uploading',
                                                message: '上传中'
                                        })
                                });
                                // console.log("上传中")
                                for (let i = 0; i &lt; lists.length; i++) {
                                        // console.log('list', lists)
                                        try{
                                                const result = await this.uploadFilePromise(lists,lists)
                                                let item = this[`fileList${event.name}`]
                                                this[`fileList${event.name}`].splice(fileListLen, 1, Object.assign(item, {
                                                        status: 'success',
                                                        message: '',
                                                        url: result
                                                }))
                                                fileListLen++
                                        }catch(e){
                                                // 上传失败时删除对应的文件项
                                        this[`fileList${event.name}`].splice(fileListLen, 1)
                                        }
                                       
                                }
                                console.log('this[`fileList${event.name}`]=',this[`fileList${event.name}`])
                                this.emitInput(this[`fileList${event.name}`])

                        },
                        async uploadFilePromise(file, lists) {
                                let OriginalUrl = file.url
                                let afterCompressFile = null
                                let ifcompress = false
                                console.log('file.type',file.type,file.size)
                                if (file.type &amp;&amp; file.type.indexOf('image') != -1 &amp;&amp; file.size / 1024 &gt; 1024) {
                                        // 单张压缩
                                        ifcompress = true
                                        let afterCompressBase64 = await this.$refs.helangCompress.compress({
                                                src: OriginalUrl,
                                                maxSize: 1024,
                                                fileType: 'jpg',
                                                quality: 1,
                                                minSize: 640 //最小压缩尺寸,图片尺寸小于该时值不压缩,非H5平台有效。若需要忽略该设置,可设置为一个极小的值,比如负数。
                                        })
                                        afterCompressFile = await base64ToFile(afterCompressBase64, file.name)
                                }
            
                                return new Promise((resolve, reject) =&gt; {
                                        console.log('file.url==', afterCompressFile)
                                        uni.uploadFile({
                                                url: xxxx,// 上传服务器地址
                                                timeout: 60000,
                                                name: 'file',
                                                filePath: !ifcompress ? file.url : file.name,
                                                file: !ifcompress ? null : afterCompressFile,
                                                header: {
                                                        'Authorization': 'Bearer ' + uni.getStorageSync('Token') ?? '',
                                                },
                                                success: (res) =&gt; {
                                                        res = JSON.parse(res.data);
                                                        // console.log('photo===',res,lists)
                                                        if(res.code==200){
                                                                resolve(res.url)
                                                        }else{
                                                                uni.$u.toast(res.message||res.msg)
                                                                reject({ code: res.code })
                                                        }
                                                },
                                                fail(fail) {
                                                        console.log("fail", fail)
                                                }
                                        });
                                })
                        },
                }
        }
&lt;/script&gt;

&lt;style&gt;
        .imgCanvas {
                position: absolute;
                top: -100%;
                width: 100%;
                height: 100%;
        }
&lt;/style&gt;
</code></pre>
<pre><code class="language-javascript">/* File Info
* 封装压缩图片的canvas
*/
&lt;template&gt;
        &lt;view class="compress" v-if="canvasId"&gt;
                &lt;canvas :canvas-id="canvasId" :style="{ width: canvasSize.width,height: canvasSize.height}"&gt;&lt;/canvas&gt;
        &lt;/view&gt;
&lt;/template&gt;

&lt;script&gt;
        export default {
                data() {
                        return {
                                pic:'',
                                canvasSize: {
                                        width: 0,
                                        height: 0
                                },
                                canvasId:""
                        }
                },
                mounted() {
                        if(!uni || !uni._helang_compress_canvas){
                                uni._helang_compress_canvas = 1;
                        }else{
                                uni._helang_compress_canvas++;
                        }
                        this.canvasId = `compress-canvas${uni._helang_compress_canvas}`;
                },
                methods: {
                        // 压缩
                        compressFun(params) {
                                return new Promise(async (resolve, reject) =&gt; {
                                        // 等待图片信息
                                        let info = await this.getImageInfo(params.src).then(info=&gt;info).catch(()=&gt;null);
                                       
                                        if(!info){
                                                reject('获取图片信息异常');
                                                return;
                                        }
                                       
                                        // 设置最大 &amp; 最小 尺寸
                                        const maxSize = params.maxSize || 1080;
                                        const minSize = params.minSize || 640;
                                       
                                        // 当前图片尺寸
                                        let {width,height} = info;
                                       
                                        // 非 H5 平台进行最小尺寸校验
                                        // #ifndef H5
                                        if(width &lt;= minSize &amp;&amp; height &lt;= minSize){
                                                resolve(params.src);
                                                return;
                                        }
                                        // #endif
                                       
                                        // 最大尺寸计算
                                        if (width &gt; maxSize || height &gt; maxSize) {
                                                if (width &gt; height) {
                                                        height = Math.floor(height / (width / maxSize));
                                                        width = maxSize;
                                                } else {
                                                        width = Math.floor(width / (height / maxSize));
                                                        height = maxSize;
                                                }
                                        }
                                       
                                        // 设置画布尺寸
                                        this.$set(this,"canvasSize",{
                                                width: `${width}px`,
                                                height: `${height}px`
                                        });
                                       
                                       
                                        // Vue.nextTick 回调在 App 有异常,则使用 setTimeout 等待DOM更新
                                        setTimeout(() =&gt; {
                                                const ctx = uni.createCanvasContext(this.canvasId, this);
                                                ctx.clearRect(0,0,width, height)
                                                ctx.drawImage(info.path, 0, 0, width, height);
                                                ctx.draw(false, () =&gt; {
                                                        uni.canvasToTempFilePath({
                                                                x: 0,
                                                                y: 0,
                                                                width: width,
                                                                height: height,
                                                                destWidth: width,
                                                                destHeight: height,
                                                                canvasId: this.canvasId,
                                                                fileType: params.fileType || 'png',
                                                                quality: params.quality || 0.9,
                                                                success: (res) =&gt; {                                                                       
                                                                        // 在H5平台下,tempFilePath 为 base64
                                                                        resolve(res.tempFilePath);
                                                                },
                                                                fail:(err)=&gt;{
                                                                        reject(null);
                                                                }
                                                        },this);
                                                });
                                        }, 300);
                                });
                        },
                        // 获取图片信息
                        getImageInfo(src){
                                return new Promise((resolve, reject)=&gt;{
                                        uni.getImageInfo({
                                                src,
                                                success: (info)=&gt; {
                                                        resolve(info);
                                                },
                                                fail: () =&gt; {
                                                        reject(null);
                                                }
                                        });
                                });
                        },
                        // 批量压缩
                        compress(params){
                                // index:进度,done:成功,fail:失败
                                let = ;
                                // 压缩完成的路径集合
                                let paths = [];
                                // 待压缩的图片
                                let waitList = [];
                                if(typeof params.src == 'string'){
                                        waitList = ;
                                }else{
                                        waitList = params.src;
                                }
                                // 批量压缩方法
                                let batch = ()=&gt;{
                                        return new Promise((resolve, reject)=&gt;{
                                                // 开始
                                                let start = async ()=&gt;{
                                                        // 等待图片压缩方法返回
                                                        let path = await next().catch(()=&gt;null);
                                                        if(path){
                                                                done++;
                                                                paths.push(path);
                                                        }else{
                                                                fail++;
                                                        }
                                                       
                                                        params.progress &amp;&amp; params.progress({
                                                                done,
                                                                fail,
                                                                count:waitList.length
                                                        });
                                                       
                                                        index++;
                                                        // 压缩完成
                                                        if(index &gt;= waitList.length){
                                                                resolve(true);
                                                        }else{
                                                                start();
                                                        }
                                                }
                                                start();
                                        });
                                }
                                // 依次调用压缩方法
                                let next = ()=&gt;{
                                        return this.compressFun({
                                                src:waitList,
                                                maxSize:params.maxSize,
                                                fileType:params.fileType,
                                                quality:params.quality,
                                                minSize:params.minSize
                                        })
                                }
                               
                                // 全部压缩完成后调用
                                return new Promise(async (resolve, reject)=&gt;{
                                        // 批量压缩方法回调
                                        let res = await batch();
                                        if(res){
                                                if(typeof params.src == 'string'){
                                                        resolve(paths);
                                                }else{
                                                        resolve(paths);
                                                }
                                        }else{
                                                reject(null);
                                        }
                                });
                        }
                }
        }
&lt;/script&gt;

&lt;style lang="scss" scoped&gt;
        .compress{
                position: fixed;
                width: 12px;
                height: 12px;
                overflow: hidden;
                top: -99999px;
                left: 0;
        }
&lt;/style&gt;
</code></pre>
<pre><code class="language-javascript">/* File Info
* 转换base64方法
*/
export function base64ToFile(base64Data, filename='xxx1.jpg') {
// 将base64的数据部分提取出来
const parts = base64Data.split(';base64,');
const contentType = parts.split(':');
const raw = window.atob(parts);

// 将原始数据转换为Uint8Array
const rawLength = raw.length;
const uInt8Array = new Uint8Array(rawLength);
for (let i = 0; i &lt; rawLength; ++i) {
    uInt8Array = raw.charCodeAt(i);
}

// 使用Blob创建一个新的文件
const blob = new Blob(, {type: contentType});

// 创建File对象
const file = new File(, filename, {type: contentType});
// console.log('创建File对象==',file,blob)

return file;
}
</code></pre>
<h2 id="-总结">🎯 总结</h2>
<p>通过这次封装,我们不仅解决了一个具体的业务需求,更重要的是提升了代码的 复用性 和 健壮性 。</p>
<ul>
<li>复用性 :任何页面需要上传图片,引入这个组件即可,无需关心压缩细节。</li>
<li>健壮性 :完善的类型判断 file.type.indexOf('image') 保证了各种奇葩图片格式也能被正确处理或透传,删除上传失败图片避免发生误会。<br>
希望这篇文章能帮你优化你的 Uni-app 项目!如果你觉得有用,点个赞再走吧~ 👍</li>
</ul><br><br>
来源:https://www.cnblogs.com/lijinhuaboke/p/19482259
頁: [1]
查看完整版本: uniapp+vue2+uview图片上传封装