头像上传、反馈截图、商品图等多图上传是 UniApp 的常见需求。把 选图、压缩、上传 三步串成一个 Promise 方法,页面传一个回调即可,代码统一且易维护。

实现思路

choose 阶段限制数量并允许拍照;拿到本地临时路径后逐张压缩,质量过高时才真正压缩以省性能;上传阶段用 uni.uploadFile,自带文件流与 formData,通过 header 带 Token,uploadTask 的 onProgressUpdate 可做进度条。

选图与压缩

// utils/upload.js
import { http } from "/utils/request";

export function chooseAndCompress(count = 9, sizeType = ["compressed"]) {
    return new Promise((resolve, reject) => {
        uni.chooseImage({
            count,
            sizeType,
            sourceType: ["album", "camera"],
            success: async (res) => {
                const tasks = res.tempFilePaths.map((path) => compress(path));
                try {
                    resolve(await Promise.all(tasks));
                } catch (e) {
                    reject(e);
                }
            },
            fail: reject
        });
    });
}

function compress(filePath) {
    return new Promise((resolve) => {
        uni.compressImage({
            src: filePath,
            quality: 80,
            success: (res) => resolve(res.tempFilePath || filePath),
            fail: () => resolve(filePath)   // 压缩失败退回原图
        });
    });
}

单图上传方法

export function uploadFile(filePath, extra = {}) {
    return new Promise((resolve, reject) => {
        const token = uni.getStorageSync("token");
        const uploadTask = uni.uploadFile({
            url: "/api/upload",
            filePath,
            name: "file",
            header: token ? { Authorization: "Bearer " + token } : {},
            formData: extra,
            success: (res) => {
                const data = JSON.parse(res.data);
                if (data.code !== 0) {
                    uni.showToast({ title: data.message || "上传失败", icon: "none" });
                    reject(data);
                } else {
                    resolve(data.data);
                }
            },
            fail: (err) => {
                uni.showToast({ title: "上传失败", icon: "none" });
                reject(err);
            }
        });
        uploadTask.onProgressUpdate((res) => {
            if (extra.onProgress) extra.onProgress(res.progress);
        });
    });
}

多图依次上传

选图返回的可能是多张,若接口要求逐张传,用串行循环依次上传,并在每张完成后回调当前进度。

export async function uploadMany(filePaths, extra = {}) {
    const urls = [];
    for (let i = 0; i < filePaths.length; i++) {
        const url = await uploadFile(filePaths[i], extra);
        urls.push(url);
        if (extra.onStep) extra.onStep(i + 1, filePaths.length);
    }
    return urls;
}

页面中的用法

// pages/feedback/index.vue
import { chooseAndCompress, uploadMany } from "/utils/upload";

async function pickImage() {
    const paths = await chooseAndCompress(3);
    if (!paths.length) return;
    const urls = await uploadMany(paths, {
        dir: "feedback",
        onStep: (done, total) => {
            console.log("已上传 " + done + "/" + total);
        }
    });
    images.value = urls;
}

需要注意

  • chooseImage 的临时路径在小程序端仅本次会话有效,上传接口需立即消费,不能存库后下次再用;
  • H5 端 uni.compressImage 部分浏览器不支持,可在 fail 回调直接返回原图;
  • formData 里的字段必须为字符串或数字,对象会丢失;
  • 响应 res.data 在部分端返回的是字符串,需先 JSON.parse 再判断业务码。

小结

把选图、压缩、上传封装成工具后,任意页面 3 行代码即可完成多图上传,后续替换 CDN 或加水印也只需改动一个文件。