淺析FormData

語言: CN / TW / HK

前因

在日常開發中都是使用公司內部封裝好的 request ,一直沒太注意請求引數型別,源於一次常規需求, 服務端提出:之前的請求引數有問題,需要調整,經過排查後發現之前的 Request HeadersContent-Type 欄位值為 application/json ,與服務端解碼規則不同,可見這篇文章《 SpringBoot 是如何解析引數的 》,需要更改為 multipart/form-data ,配合改完後,問題解決,也順便總結一下。

簡單介紹 RESTful

我們現在常用的網際網路軟體架構 RESTful ,有一些規則和約束,比如:協議、域名、版本、路徑、 HTTP 動詞 、狀態碼等,本文主要總結 HTTP 動詞 的部分內容,也就是 HTTP 請求方法,我們常用的請求方法有 GETPOSTPUT 等, GET 請求大家應該比較熟悉,一般是用於獲取資源,客戶端 通過 URL 傳參,但由於請求 URL 的長度限制,引數比較少的時候可以使用,比如一些簡單的列表頁等。而 POST 就稍稍複雜一點了,一般是用於提交資料,客戶端是通過 Request Body 傳參,該請求方式在實際業務場景(特別是在中後臺系統中)應用廣泛,下面我們就以常見的 POST 請求為例簡單介紹 FormData 的使用場景。

引入 FormData

很多時候,在 post 提交資料時我們常採用 application/jsonapplication/x-www-form-urlencoded 等型別,也確實能夠覆蓋到大部分的場景,但是有一些場景下,比如檔案上傳的時候,就不算是好的解決方案了, application/json 作為請求頭 Content-Type 欄位值時,表示告知服務端引數是序列化後的 JSON 字串,所以一般在傳參時都會用 JSON.stringify 序列化一下,且瀏覽器對 JSON.stringify API 支援程度比較高,但是 JSON.stringify 在轉換某一些資料結構時會出問題,比如 會丟失 function 型別的引數、迴圈引用時會報錯、 Blob / File 物件會被轉化成 {} 等等,,可以參考 為何不推薦使用 JSON.stringify 做深拷貝 ,不過 JSON.stringify 還有第三個引數,有興趣的同學可以去了解下,這是其一,其二,有同學要說了,如果要是圖片那可以轉換成 base64 格式進行上傳解決,這種方式雖然可行,但是轉換成 base64 格式需要很多字元,佔用很多資源,而且很長,不便於閱讀,另外就是服務端接收到這個引數還得解析,很麻煩,此時, FormData 就可用上了。

定義

FormData 這種方式相信很多同學都比較熟悉,它提供了一種表示表單資料的鍵值對 key/value 的構造方式,由名稱和定義就知道 FormData 是專門為表單量身定做的型別,但其實其功能要比 application/json 強得多,比如檔案上傳的問題,用 FormData 傳參能很好的解決, window 上也直接掛載了 FormData 物件,很方便我們直接使用。

我們在控制檯例項化一個 FormData 物件,然後列印,如下

使用

可以看到其原型上有很多的方法,個人感覺這個 FormDataMap 有點像,仔細觀察可以知道都有 setgetvalueshas 等方法,我們平常開發主要的使用也就是 append 方法了,一般都會封裝一層 request ,呼叫層只需要傳入引數的物件集合就可以。

const specialFileType = ['Blob', 'File'];

function formatData (_data) {
  const data = new window.FormData()
  for (const key in _data) {
    let value = _data[key]
    if (_data[key] instanceof Object && !specialFileType.includes(_data[key].constructor.name)) {
      value = JSON.stringify(_data[key])
    }
    data.append(key, value)
  }
  return data
}

append or set

這就有同學要問了,為啥不用 set 方法, MDN 上面寫的很清楚, appendkey 存在,就會附加到已有值集合的後面,而 set 會使用新值覆蓋已有的值,所以選擇使用哪一種取決於你的需求。

那麼文章開頭就說了 FormData 在檔案上傳這一塊比較有優勢,那麼它是怎麼處理的呢? FormData 物件能夠設定三種類型的值, stringBlobFile ,所以我們不需要轉換格式,可以直接傳檔案,當我們傳遞 FileformatData 層,會直接被 appendFormData 物件裡,且可以通過 get 獲取到值,然後傳送請求到服務端,我們能從瀏覽器入參中清晰的看到 de 引數的型別是 binary ,因為就是二進位制的檔案型別,這樣服務端接到值之後很方便獲取。

cosnt View = () => {
  const [fileA, setFileA] = useState(null);
  const [fileB, setFileB] = useState(null);
  const handleClick = () => {
    console.log('fileA:', fileA)
    console.log('fileB:', fileB)
    const p = {
      a: { a1: 11, a2: 22 },
      b: [1,2,3],
      c: 123,
      d: fileA[0],
      e: fileB[0],
    }
    const data = formatData(p);
    axios({
      method: 'POST',
      url: '/aa',
      data,
      // headers: {
      //   'content-type': 'multipart/formdata'
      // },
    })
  }

  return <div>
    <div onClick={handleClick}>傳送請求</div>
    <input
      type='file'
      onChange={(a) => {
        const v = a.target.files;
      setFileA(v);
    }}
    />
    <input
      type='file'
      onChange={(a) => {
        const v = a.target.files;
      setFileB(v);
    }}
    />
  </div>
}

可以看到 每一個引數之間都有一個 ------WebKitFormBoundary *** 區分開,這實際上是 FormData 的規範標誌,後面的字串是瀏覽器幫我們自動建立的,以 ------WebKitFormBoundary *** 作為分隔符,也作為開始和結尾,其內容主要有 Content-DispositionContent-Type 等,其中 Content-Disposition 是必選項, name 屬性代表著表單元素的 keyfilename 則是上傳檔案的名稱,也可以使用 FormData 第三個引數更改 ,另外,我在傳送請求時,並沒有更改請求頭裡面的 Content-Type ,但實際上我們看到的是正確的 multipart/form-data ,這是因為現在的瀏覽器比較智慧,當客戶端未設定請求頭的 Content-Type 時,請求引數為物件時,某一些瀏覽器會自動幫我們在 請求頭中新增 Content-Type: text/plain ,如果傳輸的資料是 FormData ,也會自動幫我們加上 Content-Type: multipart/form-data 等,可能不同瀏覽器表現行為不一樣,但是最好的方式就是客戶端與服務端約定好 Content-Type 型別,固定傳遞。

總結

在我們日常開發中,現有的幾種都能夠滿足我們的使用需求,只是在一些特殊的場景中可能會有一些偏差,具體如何使用還是要看場景,以及和服務端的約定,約定優於配置。