淺析FormData
前因
在日常開發中都是使用公司內部封裝好的 request
,一直沒太注意請求引數型別,源於一次常規需求, 服務端提出:之前的請求引數有問題,需要調整,經過排查後發現之前的 Request Headers
的 Content-Type
欄位值為 application/json
,與服務端解碼規則不同,可見這篇文章《 SpringBoot 是如何解析引數的 》,需要更改為 multipart/form-data
,配合改完後,問題解決,也順便總結一下。
簡單介紹 RESTful
我們現在常用的網際網路軟體架構 RESTful ,有一些規則和約束,比如:協議、域名、版本、路徑、 HTTP 動詞
、狀態碼等,本文主要總結 HTTP 動詞
的部分內容,也就是 HTTP
請求方法,我們常用的請求方法有 GET
、 POST
、 PUT
等, GET
請求大家應該比較熟悉,一般是用於獲取資源,客戶端 通過 URL
傳參,但由於請求 URL
的長度限制,引數比較少的時候可以使用,比如一些簡單的列表頁等。而 POST
就稍稍複雜一點了,一般是用於提交資料,客戶端是通過 Request Body
傳參,該請求方式在實際業務場景(特別是在中後臺系統中)應用廣泛,下面我們就以常見的 POST
請求為例簡單介紹 FormData
的使用場景。
引入 FormData
很多時候,在 post
提交資料時我們常採用 application/json
、 application/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
物件,然後列印,如下
使用
可以看到其原型上有很多的方法,個人感覺這個 FormData
跟 Map
有點像,仔細觀察可以知道都有 set
、 get
、 values
、 has
等方法,我們平常開發主要的使用也就是 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
上面寫的很清楚, append
的 key
存在,就會附加到已有值集合的後面,而 set
會使用新值覆蓋已有的值,所以選擇使用哪一種取決於你的需求。
那麼文章開頭就說了 FormData
在檔案上傳這一塊比較有優勢,那麼它是怎麼處理的呢? FormData
物件能夠設定三種類型的值, string
、 Blob
、 File
,所以我們不需要轉換格式,可以直接傳檔案,當我們傳遞 File
到 formatData
層,會直接被 append
到 FormData
物件裡,且可以通過 get
獲取到值,然後傳送請求到服務端,我們能從瀏覽器入參中清晰的看到 d
、 e
引數的型別是 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-Disposition
、 Content-Type
等,其中 Content-Disposition
是必選項, name
屬性代表著表單元素的 key
, filename
則是上傳檔案的名稱,也可以使用 FormData
第三個引數更改 ,另外,我在傳送請求時,並沒有更改請求頭裡面的 Content-Type
,但實際上我們看到的是正確的 multipart/form-data
,這是因為現在的瀏覽器比較智慧,當客戶端未設定請求頭的 Content-Type
時,請求引數為物件時,某一些瀏覽器會自動幫我們在 請求頭中新增 Content-Type: text/plain
,如果傳輸的資料是 FormData
,也會自動幫我們加上 Content-Type: multipart/form-data
等,可能不同瀏覽器表現行為不一樣,但是最好的方式就是客戶端與服務端約定好 Content-Type
型別,固定傳遞。
總結
在我們日常開發中,現有的幾種都能夠滿足我們的使用需求,只是在一些特殊的場景中可能會有一些偏差,具體如何使用還是要看場景,以及和服務端的約定,約定優於配置。
- 淺談低程式碼平臺遠端元件載入方案
- 前端富文字基礎及實現
- 淺談前端埋點&監控
- 如何讓 x == 1 && x == 2 && x == 3 等式成立
- 淺析 path 常用工具函式原始碼
- web components-LitElement實踐
- 模組聯邦淺析
- 效能優化——圖片壓縮、載入和格式選擇
- 如何基於 WebComponents 封裝 UI 元件庫
- CDP 遠端除錯方案
- 如何落地一個智慧機器人
- Form 資料形式配置化設計
- Lerna 執行流程剖析
- Decorator 裝飾器
- 淺析snabbdom中vnode和diff演算法
- 函數語言程式設計(FP)
- 如何利用 SCSS 實現一鍵換膚
- 淺析FormData
- Flutter For Web 編譯的兩種方案
- Web 多執行緒開發利器 Comlink 的剖析與思考