美文网首页Front End
[FE] 用 FormData 上传多个文件到 Multipar

[FE] 用 FormData 上传多个文件到 Multipar

作者: 何幻 | 来源:发表于2021-01-26 11:54 被阅读0次

    背景

    最近有一个场景,在提交表单的时候,需要实现添加附件的功能,
    表单内容要先提交到服务端,创建一个 issue,然后再将附件添加到这个 issue 中。

    所以,附件在用户添加的时候,是没有立即上传的,
    用户可以随意在浏览器端添加和删除,issue 创建后再一起上传。

    前端采用的组件库是 antd,用到了 upload 组件。
    服务端接口是自定义实现的,也许并不支持 antd upload 上传组件的规范。

    POST /api/issue/attachment
    
    字段 类型 说明
    issueId String 关联的 issue id
    files MultipartFile[] 文件数组
    import lombok.AllArgsConstructor;
    import org.springframework.web.bind.annotation.*;
    import org.springframework.web.multipart.MultipartFile;
    
    import javax.validation.Valid;
    
    @RestController
    @RequestMapping("/api/issue")
    @AllArgsConstructor
    public class XXXController {
    
        @PostMapping("/attachment")
        public XXXResponse<Void> upload(@RequestParam String issueId, @RequestParam MultipartFile[] files) {
    
        }
    }
    

    服务端接受数据时,使用了 MultipartFile,这是 Spring 框架中常用的写法

    1. <input type="file" />

    我们先看看 html input[type=file] 组件默认行为,

    <input type="file" />
    
    <script>
      const $file = document.querySelector("input[type=file]");
      $file.onchange = (e) => {
        const {
          target: {
            files: [file],
          },
        } = e;
        debugger;
      };
    </script>
    

    点击 “选择文件”,浏览器会弹出一个窗口,


    选中一个文件,点 “打开”,就会触发 onchange 事件,

    onchange 事件中,可以通过 e.target.files[0] 拿到刚才上传的那个 File 对象

    2. <Upload />

    再来看一下 upload 组件的默认行为,

    <Form>
      <Form.Item label="附件" name="attachment">
        <Upload>
          <Button>添加</Button>
        </Upload>
      </Form.Item>
    </Form>
    

    点击 “添加”,浏览器也会弹出那个选择文件的窗口,


    选中一个文件,点 “打开”,发现上传失败了。


    打开控制台,看到 upload 组件向 / 这个地址发送了一个 POST 请求,

    数据格式如下,


    我们可以向 upload 组件传入 action 参数,修改 POST 请求地址,

    <Form>
      <Form.Item label="附件" name="attachment">
        <Upload action="/api/issue/attachment">
          <Button>添加</Button>
        </Upload>
      </Form.Item>
    </Form>
    

    但是,选中文件后立即上传不符合我们的场景,我们需要提交表单之后,将多个文件统一上传。
    所以我们得自定义 upload 组件的行为。

    3. customRequest

    upload 组件的有一个 customRequest 属性(#api),
    它可以配置自定义的上传行为。

    参数 说明 类型 默认值 版本
    customRequest 通过覆盖默认的上传行为,可以自定义自己的上传实现 function -

    我们的思路是,先将选中后自动上传的行为取消掉,然后再在提交表单后统一上传。
    取消自动上传的实现片段如下,

    // 调用 onSuccess,告诉 upload 组件,已上传成功,更新页面
    const onAddAttachment = ({ onSuccess }) => onSuccess();
    
    <Form>
      <Form.Item label="附件" name="attachment">
        <Upload customRequest={onAddAttachment}>
          <Button>添加</Button>
        </Upload>
      </Form.Item>
    </Form>
    

    我们只需要在 customRequest 回调中,调用它的 onSuccess 参数即可。

    删除也是可以用的,


    4. FormData

    现在我们添加两个附件,


    接着来看前端怎样将这些附件,统一上传给服务端,具体实现如下,

    POST /api/issue/attachment
    
    字段 类型 说明
    issueId String 关联的 issue id
    files MultipartFile[] 文件数组
    <Form onFinish={onSubmitForm}>
      <Form.Item label="附件" name="attachment">
        <Upload customRequest={onAddAttachment}>
          <Button>添加</Button>
        </Upload>
      </Form.Item>
      <Form.Item>
        <Button type="primary" htmlType="submit">
          提交
        </Button>
      </Form.Item>
    </Form>
    
    // 表单提交事件
    const onSubmitForm = formValues => {
      const {
        // 附件没上传:`attachment === undefined`
        // 上传后:`attachment === {file,fileList}` 我们取 fileList 作为当前上传的文件列表
        attachment,
      } = formValues;
    
      if(attachment == null) {
        // 没有添加附件就不上传
        return;
      }
    
      const issueAttachment = {
        issueId,
    
        // 传入 antd 包装过的 input[type=file] 原始文件对象
        files: attachment.fileList.map(({ originFileObj }) => originFileObj),
      };
    
      // 使用 FormData 上传文件
      const request$ = addAttachmentToIssue(issueAttachment, httpClient);
    };
    
    // 向指定 issue 添加附件
    const addAttachmentToIssue = (issueAttachment, httpClient) => {
      const { issueId, files } = issueAttachment;
    
      // MultipartFile[] 接口,需要接收前端 FormData 中的数据
      const formData = new FormData();
      formData.append('issueId', issueId);  // 其他字段
      files.forEach(file => formData.append('files', file));  // 上传多个文件
    
      // 发送 xhr 或 fetch 请求,这里可忽略这些细节
      const url = `/api/issue/attachment`;
      const request$ = httpClient.post(url, formData);
      return request$;
    };
    

    可以看到请求成功了(项目中的 url 跟本例稍有不同,下图只为了示意),


    POST 了 3 块数据,一个 issueId,还有两个同名的 files(表示我们上传了 2 个文件)。

    还有几个需要注意的点:

    • antd upload 组件包装了原始的 html <input type="file" /> 组件,FormData 需要传入原始的 File 对象,所以要通过 originFileObj 获取一下
    • formData.append('files', ...) 可以多次执行,表示添加了多个名为 files 的字段
    • FormData 是 Web API,直接挂载在了 window 下面(window.FormData),浏览器 支持情况 如下,

    5. CORS

    上文 httpClient.post 实际调用了 XMLHttpRequest 发送请求,可能会遇到跨域的问题。
    所以在调试上传接口的时候,需要检查一下服务端的配置,是否支持跨域请求。

    (1)预检请求

    CORS 相关的内容大致如下:

    浏览器首先使用 OPTIONS 方法发起一个预检请求(preflight request),从而获知服务端是否允许该跨源请求。
    服务器确认允许之后,才发起实际的 HTTP 请求。
    (某些请求不会触发 CORS 预检请求,这样的请求称为 “简单请求”)

    在预检请求阶段,服务端对 OPTION 请求的响应头中会包含 Access-Control-Allow-Origin

    Access-Control-Allow-Origin: http://foo.example
    

    表明服务端接受该域 http://foo.example 的跨域请求。

    (2)携带 cookie

    使用 XMLHttpRequest 发送请求时,也可以携带 cookie 信息,

    const xhr = new XMLHttpRequest();
    
    // 设置请求中携带 cookie 信息
    xhr.withCredentials = true;
    

    同时 预检请求中服务端响应头,也要包含 Access-Control-Allow-Credentials,否则就不会发送 cookie

    Access-Control-Allow-Credentials: true
    

    对于附带 cookie 的请求,服务器不能设置 Access-Control-Allow-Origin 的值为 “*”,否则请求将会失败。
    而将 Access-Control-Allow-Origin 的值设置为具体的地址 http://foo.example,请求才能成功。

    (3)回到示例

    我们上传功能用到了携带 cookie 的跨域请求,
    可以看到服务端响应头中确实包含了,Access-Control-Allow-CredentialsAccess-Control-Allow-Origin 两个字段。


    参考

    Spring: Uploading Files
    Spring: org.springframework.web.multipart #MultipartFile

    ant-design v4.11.1
    Ant Design - Upload #API

    MDN: CORS

    相关文章

      网友评论

        本文标题:[FE] 用 FormData 上传多个文件到 Multipar

        本文链接:https://www.haomeiwen.com/subject/gitgzktx.html