你小程序里多半有一段长得像这样的上传代码:
javascript
// pages/feedback/index.vue
export default {
data() {
return { uploading: false, percent: 0, uploadTask: null };
},
methods: {
handleUpload() {
uni.chooseImage({
count: 1,
success: res => {
this.uploading = true;
const task = uni.uploadFile({
url: 'https://api.example.com/uploadImg',
filePath: res.tempFilePaths[0],
name: 'file',
formData: { from: 'feedback' },
success: () => { this.uploading = false; },
fail: () => {
this.uploading = false;
uni.showToast({ title: '上传失败', icon: 'none' });
}
});
task.onProgressUpdate(e => { this.percent = e.progress; });
this.uploadTask = task; // 存起来,取消时才用得到
}
});
},
handleCancel() {
this.uploadTask && this.uploadTask.abort();
this.uploading = false;
this.percent = 0;
}
}
};
它能跑,也上线了。但这段代码有三处不对劲:
- 状态是手工账。
uploading、percent、uploadTask三个字段,得在 success / fail / 取消三条路径里挨个复位。哪天加个"失败自动清进度",漏改一条路径,进度条就永远停在 73%。 - 取消靠自觉。
uploadTask引用得自己存,取消得自己调abort(),复位得自己写。页面一销毁忘了取消,请求还在后台跑。 - 没法复用。 头像、意见反馈、工单附件,每个页面把五十行复制一遍改个 url。三个月后想统一加登录态过期处理,得找出所有副本挨个改。
用户那头的体感更直接:点了上传没进度,以为卡死,再点一次,服务器收到两张一样的图。
理想的上传该长什么样
把需求摊开,其实就四条:
- 进度、加载态是响应式状态,不是手工变量,任何路径退出都自动复位;
- 取消是内置动作,不用自己攒 task 引用;
- 重传就是再调一次,不用把 chooseImage 到 uploadFile 整条链重走;
- 写法和普通请求一致,上传不该是一套独立于 GET/POST 之外的方言。
第 4 条最容易被忽略,但它决定三个月后的维护成本:分页一套写法、上传又一套写法,团队每个人都在维护好几套私房封装。
顺着这份清单找,会发现这正是请求策略库在做的事。alova 的定位是一套请求 API 跑在 Web、小程序/App(uni-app/Taro)、服务端,上传只是其中一个策略场景。
换成 alova 写一遍
先装包(uni-app 适配器,注意只支持 vue3 版 uni-app):
bash
npm install alova @alova/adapter-uniapp @alova/shared --save
建实例,请求/存储适配器一次配齐:
javascript
// api/index.js
import { createAlova } from 'alova';
import AdapterUniapp from '@alova/adapter-uniapp';
export const alovaInst = createAlova({
baseURL: 'https://api.example.com',
...AdapterUniapp(),
responded(response) {
const { statusCode, data } = response;
if (statusCode >= 400) throw new Error('请求错误');
return data || null;
}
});
上传本身,在 method 的 config 里标 requestType: 'upload',适配器内部就改调 uni.uploadFile;data 里的 name、filePath 传给 uploadFile,其余字段自动进 formData:
html
<template>
<button @click="handleImageChoose">上传图片</button>
<button v-if="loading" @click="abort">取消</button>
<progress v-if="loading" :percent="percent" show-info />
</template>
<script setup>
import { computed } from 'vue';
import { useRequest } from 'alova/client';
import { alovaInst } from '@/api';
const uploadFile = (name, filePath, formData) =>
alovaInst.Post(
'/uploadImg',
{ name, filePath, ...formData }, // 额外字段自动进 formData
{ requestType: 'upload', fileType: 'image' }
);
const { loading, uploading, send, abort } = useRequest(uploadFile, {
immediate: false
});
// uploading = { loaded, total },响应式,直接算百分比
const percent = computed(() =>
uploading.value.total > 0
? Math.floor((uploading.value.loaded / uploading.value.total) * 100)
: 0
);
const handleImageChoose = () => {
uni.chooseImage({
count: 1,
success: res => send('file', res.tempFilePaths[0], { from: 'feedback' })
});
};
</script>
对照开头那三条清单:
- 进度:
uploading.loaded / uploading.total,响应式,没有任何一行onProgressUpdate; - 加载态:
loading自动起落,失败、取消都会复位,不存在"进度条卡在 73%"; - 取消:解构出来的
abort()直接绑按钮,task 引用的存取是库内部的事; - 重传:失败后再点按钮,
send()再走一次就是重传,参数还能换。
data、loading 这些和普通请求完全同名同形,这就是清单第 4 条:上传不再是方言。团队里会写 useRequest(getList) 的人,看这段代码零学习成本。
顺带一提,如果你项目已经在用 axios,它不冲突:axios 可以作为 alova 的请求适配器继续发请求(@alova/adapter-axios),axios 负责发请求,alova 负责怎么请求。不过小程序环境用不到 axios,这里按下不表。
多文件、批量怎么办
单文件够用就到此为止。要做"九宫格传图 + 每张独立进度 + 失败单张重传",alova@3.3+ 还有个 useUploader:base64 / File / Blob / ArrayBuffer 直接丢进去,每个文件独立的 status 和 progress,upload(1, 3) 指定重传,mode: 'batch' 批量提交。要说明的是:它默认的弹窗选文件只在浏览器里有效,小程序/uni-app 环境需要通过 useUploader.selectFile 自定义选择逻辑。单文件场景用上面的 useRequest 写法就好,不必上它。
alova 是一套请求 API,跑在 Web、小程序/App(uni-app/Taro)和服务端(BFF)。上传只是入口,分页竞态、token 并发刷新、请求缓存这些"每个项目都要再写一遍"的东西,才是请求策略层真正省心的地方。
- 官网:alova.js.org
- GitHub:github.com/alovajs/alo... 本身正确)