如何上傳文件及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編碼與解碼 請求頭截圖

相關文章
相關標籤/搜索