踩坑篇--使用 fetch 上传文件

踩坑篇--使用 fetch 上传文件

首先,向大家问个好,懒癌发作,好久不见 ;-)

--------------------------------------------

这个坑是自己最近使用 fetch 给后端上传文件的时候踩到的,需求很简单,就是需要给后端上传一个文件以及需要的几个字段。看,简单吧。。

于是,撸起袖子准备撸代码。

刷刷刷。。

复现问题

为了复现这个问题,自己搭了个后端,前端写了个很简(chou)单(lou)的页面。

出于简洁的考虑,前端 index.html 页面就放了两个表单字段

<!DOCTYPE HTML>
<html>
  <head>
  </head>
  <body>
    <form>
      <label for="userName">姓名:</label><input id="userName" name="userName" />
      <input type="file" name="file" id="file"/>
    </form>
  </body>
  <script src="index.js"></script>
</html>

使用 fetch 来上传文件,因为是文件类型,所以 Content-Type 需要设置成 multipart/form-data。好,温习一下 Content-Type 的几大类型:

  • application/x-www-form-urlencoded: 最普遍的上传方式,数据格式类似 key1=val1&key2=val2
  • application/json: json格式,数据格式类似于{‘key1’:‘val1’,‘key2’:‘val2’})
  • multipart/form-data: 文件上传的时候需要设置
  • text/xml: 很少用了

前端的代码,如下:

const fileBtn = document.getElementById('file')
const userName = document.getElementById('userName')
fileBtn.addEventListener('change', () => {
  const fd = new FormData()
  fd.append('file', file.files[0])
  fd.append('userName', userName.value)
  fd.append('age','18')
  fetch('http://localhost:3036/upload', {
    method: 'POST',
    body: fd,
    headers: {
      'Content-Type': 'multipart/form-data'
    }
  }).then(res => {
    if(res.ok) {
      console.log('success')
      return res.json();
    } else {
      console.log('error')
    }
  }).then(res => {
    console.log('res is',res);
  })
})

使用 express 很简单搭个项目,启动 3036 端口来接收前端的请求,上代码:

const express = require('express')
const multer  = require('multer')

const app = express()
const upload = multer({ dest: 'upload/' })

app.use(function(req, res, next) {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "Origin, X-Requested-With, Content-Type, Accept");
  next();
})

app.post('/upload',upload.single('file'), (req, res) => {
  console.log('req body', req.body)
  console.log('req file', req.file)
  if (req.file.fieldname) {
    res.status(200).send({message: 'ok', status: 200, res: {}})
  }
})

app.listen(3036, function () {
  console.log('app is listening at port 3036')
})

一切已经就绪,只等前端上传文件了,正当我自信满满地选中文件时,浏览器却抛出了 500 同时报给我这么一个错误:Error: Multipart: Boundary not found

后端也显示同样的错误。

打开 chrome, 看请求好像也什么毛病。。



纳尼???

真是让人摸不着头发

找到症结

一阵 debug…

后面发现有人在 stackoverflow 上遇到了同样的问题,解决方法居然是移除 headers

纳尼???

我之前的 Content-Type 白复习了???

本着瞎蒙的心态,我就移除了 Content-Type, 居然。。居然好了。。后端成功打出了文件信息。。。

看看此时的请求


好了,开始找原因了。。。

仔细对比,发现去掉 Content-Type 的请求头部多了一个 boundary ,于是去查这个 boundary。原来 post 请求上传文件的时候是不需要自己设置 Content-Type,会自动给你添加一个 boundary ,用来分割消息主体中的每个字段,如果这个时候自己设置了 Content-Type, 服务器就不知道怎么分割各个字段,因此就会报错。 orz…

注意

并且这个问题不只是 fetch 里面存在,自己又在原生 ajax 代码里面复现了一遍,在原生的 ajax 上传文件如果设置了 Content-Type 的时候也会存在这个问题,附上一部分代码。。

fileBtn.addEventListener('change', () => {
  const fd = new FormData()
  fd.append('file', file.files[0])
  fd.append('userName', userName.value)
  fd.append('age','18')
  const xhr = new XMLHttpRequest()
  xhr.open('POST', 'http://localhost:3036/upload', true)
  // xhr.setRequestHeader("Content-type","multipart/form-data")
  xhr.send(fd)
  xhr.onreadystatechange = () => {
    if (xhr.readyState === 4 && xhr.status === 200) {
      console.log('res is', xhr.responseText)
    }
  }

(补充)自己又去翻了一下 W3C 的文档,文档里面是这样介绍 xhr 上传 FormData 类型的数据的:

Let the request entity body be the result of running the multipart/form-data encoding algorithm with data as form data set and with utf-8 as the explicit character encoding.
Let mime type be the concatenation of “multipart/form-data;”, a U+0020 SPACE character, “boundary=”, and the multipart/form-data boundary string generated by the multipart/form-data encoding algorithm.

大致意思就是请求的实体数据是经过了 multipart/form-data 算法编码同时以 utf-8 作为显示字符编码。mime-type(Content-Type) 是”multipart/form-data;” 和 运行 multipart/form-data 算法生成的”boundary=xxx” 串联在一起的字符串。

所以使用 FormData 就自动给我们规定了这些内容,不需要我们自己再去指定了。

想到之前的老项目使用 jQuery 上传文件的时候,需要将 contentType也要设置为’false’, 也是这个原因。

这个坑最会踩的地方就是在项目里面写一个方法封装原生 fetch 方法的时候,因为需要根据不同的数据类型设置不同的 Content-Type,可是上传文件的时候又不能设置 Content-Type, 所以需要谨慎地封装 fetch ,一不小心可能就会造成上面的问题。

总结

最后做总结

好像也没啥总结的了。。

那就附上代码地址咯: github.com/LuckyAbby/Po

嘻嘻☺️

编辑于 2018-03-07

文章被以下专栏收录