吐血记录Element-UI文件上传的问题定位

最近工作当中,很多事情靠脑力和简单的文档记录,完全不能灵活的进行,因此进行了一些平台开发工作,由于基本都是些管理平台,没有太多的复杂业务逻辑,因此除了后端服务外,前端服务也试着写一写,毕竟如果还要再找个人支持我,不光需求每次我都要讲,遇到问题,或者有一些变动,比较浪费时间,总体来说复制粘贴的活居多,除了UI样式设计没啥经验,功能实现起码还能干,既然普通管理平台前端能干,那尘封已久的压测平台是不是也可以撸一撸,毕竟后端服务已经早就打磨过,Github上总会有一些同学问我平台怎么部署起来,其实是缺前端根本起不来,所以趁现在的激情,晚上继续撸了一波,那些比较简单的节点,脚本,依赖,数据,配置啥的,基本都是无脑DPS,在内容最多的一个页面,用例关联脚本的时候,折腾了一晚上,简单记录一下

梳理下流程,后端SpringBoot就不说了,前端Vue3+TypeScript,样式组件用的Element-UI,因为是VUE3,所以是Element-Plus,整体前后端分离项目

具体业务流程,之前写过很多篇相关实现,就不说了,遇到的问题十分简单的一个基本功能,文件上传,先列一下前后端调用的流程

1、首先后端提供上传接口服务

@ApiOperation("上传")
@PostMapping(value = "/upload/{testCaseId}")
public Response<Boolean> uploadJmx(@PathVariable Long testCaseId,
@RequestParam(value = "jmxFile") MultipartFile jmxFile) {
return ResponseUtil.buildSuccessResponse(jmxService.uploadJmx(testCaseId, jmxFile, UserUtils.getCurrent()));
}

这个没啥可说的,传testCaseId字段是因为上传的jmeter脚本文件要关联测试用例,很多数据库操作,并不是简单的上传,后端接口自测postman和jmeter都是可执行的

 

2、接着定义前端调用后端的请求,用的是Axios库的request

export const uploadJmx = (testCaseId: number, formData: FormData) => {
return request({
url: '/jmx/upload/' + testCaseId,
method: 'post',
headers:{
'Content-Type':'multipart/form-data',
},
data: formData,
})
}

这里的Content-Type貌似不指定,在用Element-UI里的el-upload的时候,会自动识别,这个也没啥好说的,唯一要注意的就是文件上传时使用FormData对象将文件打包,通过请求的body发送,因此不能用params而要用data

 

3、接下来就是UI样式调用了

对于上传组件,有现成代码,链接:https://element-plus.org/zh-CN/component/upload.html

他这里VUE代码很简洁

<template>
<el-upload
v-model:file-list="fileList"
class="upload-demo"
action="https://run.mocky.io/v3/9d059bf9-4660-45f2-925d-ce80ad6c4d15"
multiple
:on-preview="handlePreview"
:on-remove="handleRemove"
:before-remove="beforeRemove"
:limit="3"
:on-exceed="handleExceed"
>
<el-button type="primary">Click to upload</el-button>
<template #tip>
<div class="el-upload__tip">
jpg/png files with a size less than 500KB.
</div>
</template>
</el-upload>
</template>

简单看下API属性介绍,惊呆了,居然headers,url,xxx全部都放在el-upload里

名称 描述 类型 默认值
action required 请求 URL string #
headers 设置上传的请求头部 object
method 设置上传请求方法 string post
multiple 是否支持多选文件 boolean false
data 上传时附带的额外参数 从 v2.3.13 支持 Awaitable 数据,和 Function object / Function {}
name 上传的文件字段名 string file

这可不是我想要的,基本我的实现都是TypeScript里实现各个click触发逻辑的handle,因此继续找了下,找到了一个参数

http-request 覆盖默认的 Xhr 行为,允许自行实现上传文件的请求 Function 请参考ajaxUpload

因此就有了下面几行简洁的代码

<el-upload
action=""
:show-file-list="false"
:http-request="handleJmxUpload"
:on-success="handleJmxUploadSuccess"
>
<el-button text :icon="Upload" class="blue">上传JMX脚本文件</el-button>
</el-upload>

原始el-upload还显示上传的文件造型,这个不需要,我的需求是选中上传到服务器就行了

下面就是最后一步,直接handleJmxUpload里执行上传操作即可

//上传JMX
const handleJmxUpload = async (file) => {
const testCaseId = testCaseFullData.value.id;
const formData = new FormData();
formData.append("jmxFile", file);
const res = await uploadJmx(testCaseId, formData);
const code = res.data.code
if (code != 0) {
ElMessage.error(res.data.message);
} else {
ElMessage.success("上传成功");
await getList();
}
}

这一步,唯独就FormData相关内容是网上搜的,然后根据后端传入的参数名设定的jmxFile,从而调用uploadJmx方法,执行request操作,最终调用后端springboot里controller里的接口

 

通过这几步,基本很容易捋清整个调用流程

NewImage但测试了一把上传功能,报了一个莫名其妙的错误,既然是和jmxFile有关的,关于这个,走了一晚上弯路

(1)先直接google了一把,各种奇葩的都来了,什么@RequestParam要改成@RequestPart,springboot版本问题,配置冲突,少了multipart相关开关配置,乱七八糟五花八门,我甚至傻不拉几还真的试着改了半天,都不行,搞了好久,静静思考了下,我后端接口调用是没问题的呀,那关后端服务屁事,脑子抽了折腾了半天

(2)前端打印一些日志看看,这不看则已,看了更迷了

NewImage

A:这看上去也没啥,该有的都有了,只能关注jmxFile里的一堆了

B:headers为啥是空,这是最终调用http的header么?说实话我把request调用那里,手动加减了content-type的属性尝试了下,然并卵,甚至还把request.ts扫了一眼

import axios, {AxiosInstance, AxiosError, AxiosResponse, AxiosRequestConfig, AxiosRequestHeaders} from 'axios';

const service:AxiosInstance = axios.create({
baseURL: "/mysterious",
timeout: 30000
});

service.interceptors.request.use(
(config: AxiosRequestConfig) => {
if (localStorage.getItem('ms_username') &&
localStorage.getItem('token')) {
(config.headers as AxiosRequestHeaders).token = `${localStorage.getItem('token')}`;
}
return config;
},
(error: AxiosError) => {
console.log(error);
return Promise.reject();
}
);

我也就最近加了个塞一个token进去,认证用,为啥axios调用超时时间30S?哦,想起来了,分布式节点启用,要在slave节点启动jmeter-server,一大堆检查,默认5秒完全不够,我后来改成了30秒;也看不出啥问题;这里折腾最久了,就差像网上的一样,不用调用request,直接全部写vue文件里调用,但如果这样又有一个问题,统一认证没法搞,难道这里又重新获取一次token,单独塞一次,也是醉了

C:这下真的技穷了,请教一下ChatGPT吧,结果给我的答案是,貌似没明显问题

NewImage

接着不知道我是脑子哪根筋不对,搜了下百度的文言一心,第一次玩,登录验证,下一步一大堆,给我的回复,给我指出了问题,我还喜出望外,结果我用我萌胧的双眼对比了不下几十秒,和我的有区别吗?

NewImageNewImage

无语之下,怼了一下,这家伙真是自恋,怒关

D:没办法了,只好继续看看console打印的日志,扫了一眼ChatGPT,奈何不是4.0,在它说的几点里,唯一我不确定的就是第一点,file这个参数,没办法继续打印,看下file

NewImage

这个file里面一大堆乱七八糟的,继续搜下Element-UI的el-upload官方文档,发现了这样一个定义

interface UploadRequestOptions {
  action: string
  method: string
  data: Record<string, string | Blob | [string | Blob, string]>
  filename: string
  file: File
  headers: Headers | Record<string, string | number | null | undefined>
  onError: (evt: UploadAjaxError) => void
  onProgress: (evt: UploadProgressEvent) => void
  onSuccess: (response: any) => void
  withCredentials: boolean
}

可以看到这个和上面打印的file一模一样,那是不是里面的file:File才是我们上传的,其它一堆乱七八糟的根本不是我们关心的,把append的地方改成file.file果然就好了

//上传JMX
const handleJmxUpload = async (uploadRequestOptions) => {
const testCaseId = testCaseFullData.value.id;
const formData = new FormData();
formData.append("jmxFile", uploadRequestOptions.file);
const res = await uploadJmx(testCaseId, formData);
const code = res.data.code
if (code != 0) {
ElMessage.error(res.data.message);
} else {
ElMessage.success("上传成功");
await getFullTestCase(testCaseId);
}
}

handle里的参数根本不叫file,人家叫【UploadRequestOptions】,直接参数改掉,也是醉了,果然这代码不能乱copy,出问题的地方肯定是不确定,不熟悉的地方

行吧,完工,这个页面做了,应该就没有啥难度的了,OVER

发表回复