如何上传文件及content-type的设置

前言

在使用 fetch / axios 时经常会涉及到文件上传,以及其余请求,其中包括一些 content-type ,被这些不一样类型到 content-type 搞得头大,到底何时该用怎么样的类型呢,本文将会梳理这些问题。javascript

实例分析,如何上传一张图片

表单提交方式

表单<form>用来收集用户提交的数据,发送到服务器。下面代码中包含:文件选择框(获取本地文件),提交按钮(提交表单控件)。html

<form action="http://localhost:8899/react/aa" method="post" enctype="multipart/form-data">
    <input type="file" name="image"  multiple="multiple" />
    <input type="submit" value="提交"/>
</form>
复制代码

用户点击“提交”按钮,每个控件都会生成一个键值对,键名是控件的name属性,键值是控件的value属性。咱们采用node做为服务端,使用 koa-body 解析 post 方式传递的文件前端

// 服务端获取请求中的文件
router.post('/aa',  async ctx => {
  console.log(ctx.request.files);
})
复制代码

使用 axios 请求

表单数据以键值对的形式向服务器发送,这个过程是浏览器自动完成的。可是有时候,咱们但愿经过脚本完成过程,构造和编辑表单键值对。浏览器原生提供了 FormData 对象 来完成这项工做。java

new FormData(form)

let formdata = new FormData(form);
复制代码

FormData()构造函数的参数是一个表单元素,这个参数是可选的。若是省略参数,就表示一个空的表单,不然就会处理表单元素里面的键值对。通常咱们使用的方法就是构建一个空的表单对象,FormData 提供不少实例方法,咱们能够经过 append 方法来添加表单中的键值对。node

formdata.append(key1,value1)
formdata.append(key2,value2)
复制代码

下面的代码经过 axios 提交 formdata 表单数据来实现文件上传react

<input type="file" @change="onChange">
    methods:{
        onChange(event){
            const params = new FormData()
            params.append('file',event.target.files[0])
             axios.post('http://localhost:8899/react/aa',params,{
              headers:{
                'content-type':'multipart/form-data'
              }
            })
        }
    }
复制代码

查看了 axios 源码 发现其实上传文件不须要设置 content-type 源码 lib/adapters/xhr.js 文件中定义了浏览器使用 XHR :ios

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
        // config 是传入的配置对象 如: {url,method,data,headers}
        // 获取传入的参数和请求头
        var requestData = config.data;
        var requestHeaders = config.headers;
        // 判断是否为 formData 实例,若是是删除 请求头中的 content-type
        if (utils.isFormData(requestData)) {
          delete requestHeaders['Content-Type']; // Let the browser set it
        }
  })
}
复制代码

isFormData 实现: FormData 就是表单对象的构造函数, 使用 instanceof 来检测构造函数的 prototype 属性是否出如今实例对象的原型链上。git

/**
 * Determine if a value is a FormData
 *
 * @param {Object} val The value to test
 * @returns {boolean} True if value is an FormData, otherwise false
 */
function isFormData(val) {
  return (typeof FormData !== 'undefined') && (val instanceof FormData);
}
复制代码

给content-type 设置默认值

lib/defaults.js 文件github

defaults.headers = {
  common: {
    'Accept': 'application/json, text/plain, */*'
  }
};

utils.forEach(['delete', 'get', 'head'], function forEachMethodNoData(method) {
  defaults.headers[method] = {};
});

utils.forEach(['post', 'put', 'patch'], function forEachMethodWithData(method) {
  defaults.headers[method] = utils.merge(DEFAULT_CONTENT_TYPE);
});
复制代码

判断传入数据的类型来设置不通的content-type

当使用 axios 发起请求时,会经过默认设置的 transformRequest 在发送给服务器前改变请求的数据,'PUT', 'POST', 'PATCH' and 'DELETE' 只对这几种请求方式有效。chrome

// lib/defaults.js 文件
 transformRequest: [function transformRequest(data, headers) {
    normalizeHeaderName(headers, 'Accept');
    normalizeHeaderName(headers, 'Content-Type');
    if (utils.isFormData(data) ||
      utils.isArrayBuffer(data) ||
      utils.isBuffer(data) ||
      utils.isStream(data) ||
      utils.isFile(data) ||
      utils.isBlob(data)
    ) {
      return data;
    }
    if (utils.isArrayBufferView(data)) {
      return data.buffer;
    }
    if (utils.isURLSearchParams(data)) {
      setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
      return data.toString();
    }
    if (utils.isObject(data)) {
      setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
      return JSON.stringify(data);
    }
    return data;
  }],
复制代码

因此通常状况下使用 axios 请求不须要设置 content-type 若是有些特殊状况须要处理的能够放在 transformRequest:[function(data,headers){return data}] 中作处理

使用 fetch 请求

为了展现清楚,直接简化fetch请求的封装,具体能够查看下面的 codepen 示例,这里主要是为了展现使用 fetch 上传文件时,同时设置了 headers 请求时会出现什么问题,将代码设置以下:

fetch(api,{
    url,
    headers:{
        'content-type':'multipart/form-data'
    },
    mode:'no-cors'
})
复制代码

当咱们发起图片上传请求时,获得两处报错,在报错右处有错误产生的文件地方,点击这个文件能够看到

500 (Internal Server Error) 是请求api服务端报的错, Unexpected end of input 是浏览器运行 response.json()报的错。 为何浏览器会报 Unexpected end of input 后面我会单独讲,接下来咱们看下服务端的报错缘由。

上传的文件内容是须要经过 boundary 来标明分割线的,正确的Content-Type: mutlipart/form-data; boundary = -----xxxx这种形式。 查看咱们当前上传的请求:

来对比一下正确的图片上传时 content-type 的格式:

在使用 fetch 请求时,设置 Content-type 就会丢失 boundary 参数,所以 在上传文件时,不须要设置 headers 字段,浏览器会自动生成完整的 content-type(包含 boundary)。

移除 headers 字段后,fetch api能够正常的上传文件了!!

codepen 示例

codepen示例

Unexpected end of input 报错

其实这就是 js 语法错误,下面代码中使用 fetch api 获取资源后返回一个响应的 response 对象,若是咱们指定 content-type 为 application/json 那么服务端会返回给咱们一个 json 格式的字符串,因此当咱们调用 resopnse.json() 时候能够解析出正确当对象。

fetch(api).then(response=>response.json()).then(res=>res)
复制代码

你能够把 resopnse.json() 理解为 JSON.parse(),因此能够经过 JSON.parse() 来模拟前面的 Unexpected end of input 报错浏览器在读取咱们的代码时,碰到了不可预知的错误,致使浏览器 无语进行下面的读取以下面的代码都会输出这个错误。

JSON.parse("{")
JSON.parse('[{"test": 4}')
复制代码

常见都还有 Unexpected token < in JSON at position 0 继续模拟下该错误发生的场景,前端继续使用 response.json去解析服务端返回的数据。而在服务端不按照 content-type 预约的值传回,就会获得这个报错。

// 服务端
router.post('/aa',  async ctx => {
  ctx.body = '<div>内容</div>'
})
复制代码

若是想简单的模拟直接使用 JSON.parse("<h1>1</h1>")就会获得一样的结果。下面的代码都是一个道理。

JSON.parse("{sd}")
// Unexpected token s in JSON at position 1
JSON.parse("d}")
// Unexpected token d in JSON at position 0
复制代码

JSON.parse()支持的类型以下:

JSON.parse('{}');              // {}
JSON.parse('true');            // true
JSON.parse('"foo"');           // "foo"
JSON.parse('[1, 5, "false"]'); // [1, 5, "false"]
JSON.parse('null');            // null
JSON.parse('1');               //  1
复制代码

因此想要正确的解析服务端的返回值,先后端要统一设定好 content-type 对应传输的数据类型,响应对象response也支持其余多个方法

整理 content-type

axios

get 请求方式

请求方式为 get 只会使用 application/x-www-form-urlencoded 编码方式

axios.get('/user',{
              params:{
                id:1,
                name:'dd',
                person:'张三'
              }
            })
复制代码

理论上会请求 http://localhost:8080/user?id=1&name=dd&person=张三 可是对于特殊字符会进行 Url编码。
Url编码一般也被称为百分号编码(Url Encoding,also known as percent-encoding),是由于它的编码方式很是简单,使用%百分号加上两位的字符——0123456789ABCDEF——表明一个字节的 十六进制形式。Url编码默认使用的字符集是US-ASCII。例如a在US-ASCII码中对应的字节是0x61,那么Url编码以后获得的就 是%61
因此通过编码之后实际请求路径变成 http://localhost:8080/user?id=1&name=dd&person=%E5%BC%A0%E4%B8%89

post 请求方式

  1. application/x-www-form-urlencoded格式
    默认使用该方式,使用该方式时须要对传入对参数处理成 name=hehe&age=10 格式,引入若是你传入对是一个对象,在 axios 默认配置中会将 content-type 设置为 application/json;charset=utf-8

2. text/plain axios.post('http://localhost:8899/react/aa','我就是内容',{ headers:{ 'content-type':'text/plain' } })

3. multipart/form-data 上传文件是不须要设置该类型,浏览器会自动添加!!!
4. application/json axios.post('http://localhost:8899/react/aa',{name:'dd',age:18})

fetch

get 方式 application/x-www-form-urlencoded 编码

get 传参数的方式须要添加到路径上,因此 Url编码的工做须要咱们手动实现

// 在URL中写上传递的参数
fetch('http://localhost:8080?a=1&b=2', { 
    method: 'GET'
  })
  
 // 处理传入的 params 参数
    for(let key in params){
      param += `${key}=${encodeURIComponent(params[key])}&`
    }
复制代码

post 方式

  1. application/x-www-form-urlencoded
fetch('http://localhost:8080',{
    method:'POST',
    headers:{
        'content-type':'application/x-www-form-urlencoded'
    },
    mode:'no-cors',
    body:'name=dd&age=12'
})
复制代码
  1. application/json

须要使用 JSON.stringify() 将对象转换成 JSON 字符串,body做为接受数据的字段

fetch(url, {
  method: 'POST', // or 'PUT'
  body: JSON.stringify(data), 
  headers: new Headers({
    'Content-Type': 'application/json'
  })
})
复制代码
  1. multipart/form-data 上传文件是不须要设置该类型,浏览器会自动添加!!!

参考

axios
FormData 对象的使用
Error when POST file multipart/form-data
Chrome: Uncaught SyntaxError: Unexpected end of input
Web开发须知URL编码与解码 请求头截图

相关文章
相关标签/搜索