手把手实现一个高性能的Upload组件
又踩坑了,Upload组件传参死活不生效
今天上线前最后联调,发现一个低级到离谱的问题:我用的Upload组件,明明在data里写了token,但发请求的时候header里就是没有Authorization。前端我改了无数遍,后端说没收到,气得我直接开F12抓包看,结果request headers里干干净净,啥都没有。
一开始我以为是async问题,或者this指向错了,各种console.log打上去,发现data里的token明明是有值的,config也写了headers: { Authorization: this.token },但就是传不进去。折腾了一个小时,差点怀疑人生。
后来灵机一动,把upload组件的before-upload钩子打印出来一看,卧槽,这里根本没有走我写的request方法!原来项目里用的是element-ui的el-upload,但我为了控制上传逻辑,加了个:http-request属性,自定义了上传方法。结果这个自定义方法里,我写的是直接用axios.post,但忘了把headers带上……
更坑的是,我之前复制了一段老项目的代码,那个项目用的是form-data提交,不需要token,所以压根没写headers。这次迁移到新项目,接口需要鉴权了,我只改了url,忘了补headers。
核心代码就这几行
改完之后的上传方法长这样:
handleUpload(file) {
const formData = new FormData();
formData.append('file', file.file);
return fetch('https://jztheme.com/api/upload', {
method: 'POST',
body: formData,
headers: {
Authorization: Bearer ${localStorage.getItem('token')}
}
})
.then(res => res.json())
.then(data => {
if (data.code === 0) {
this.$message.success('上传成功');
this.imageUrl = data.data.url;
} else {
this.$message.error(data.msg || '上传失败');
}
})
.catch(err => {
this.$message.error('网络错误,请重试');
console.error(err);
});
}
模板里是这样用的:
<el-upload
class="avatar-uploader"
:http-request="handleUpload"
:show-file-list="false"
accept="image/jpeg,image/png,image/gif"
>
<img v-if="imageUrl" :src="imageUrl" class="avatar">
<i v-else class="el-icon-plus avatar-uploader-icon"></i>
</el-upload>
这里注意我踩过好几次坑::http-request要绑定函数名,不要加括号,不然会直接执行。另外accept限制只能防住用户手动选文件时的类型,不能防住拖拽或粘贴,这块后面还得服务端再校验一次。
你以为这就完了?还有个隐藏bug
改完之后本地测试OK,但QA提了个bug:上传GIF的时候,预览图不动。我去,这还能有事?
查了半天发现是CSS的问题。我们用了tailwind,图片容器加了object-cover,但对GIF这种动画图片,在某些浏览器下会被强制静止渲染。后来试了下发现加上will-change: transform能触发硬件加速,但也没用。
最终解决方案是在img标签上动态加一个时间戳,强制刷新:
<img :src="${imageUrl}?t=${Date.now()}" class="avatar" />
其实更好的做法是用background-image,或者干脆交给canvas处理,但项目赶,就这么凑合了。反正改完之后QA测了几个GIF都能动了,虽然每次上传都会闪一下,但无伤大雅。
关于进度条的骚操作
本来el-upload自带on-progress事件,但我用了:http-request之后,这个事件就不准了——因为fetch没法直接监听上传进度。
后来试了下XMLHttpRequest,才搞定进度条:
handleUpload(file) {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
const formData = new FormData();
formData.append('file', file.file);
xhr.open('POST', 'https://jztheme.com/api/upload');
xhr.upload.onprogress = (e) => {
if (e.total > 0) {
// 这里可以emit出去给父组件更新进度
this.progressPercent = Math.floor((e.loaded / e.total) * 100);
}
};
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
if (xhr.status === 200) {
try {
const data = JSON.parse(xhr.responseText);
if (data.code === 0) {
this.$message.success('上传成功');
resolve(data);
} else {
this.$message.error(data.msg);
reject(new Error(data.msg));
}
} catch (err) {
this.$message.error('解析失败');
reject(err);
}
} else {
this.$message.error('上传失败');
reject(new Error('网络错误'));
}
}
};
xhr.setRequestHeader('Authorization', Bearer ${localStorage.getItem('token')});
xhr.send(formData);
});
}
这里亲测有效,progress事件能正常触发。不过要注意跨域情况下,后端得配Access-Control-Allow-Origin和Access-Control-Expose-Headers,不然拿不到响应头里的自定义字段。
踩坑提醒:这三点一定注意
- 自定义上传方法不会自动带headers,尤其是token这种,一定要手动拼
- FormData不能直接append整个FileList,得遍历单个append,否则后端收不到
- 上传进度依赖xhr.upload,fetch目前还不支持上传进度监听(2024年7月为止)
还有一个小问题没解决:移动端点击上传按钮,有时候唤不起相册。查了是iOS Safari对label嵌套input的click事件拦截太严,加了capture属性也不太稳定。目前 workaround 是让用户长按图片选择“保存后上传”,体验差了点,但比不上线卡着强。
以上是我踩坑后的总结,如果你有更好的方案欢迎评论区交流。我现在只想去喝杯冰可乐压压惊。

暂无评论