前言
這篇文章旨在記錄自己解惑過程,比如
- 在 chrome 調試工具中,
Form Data和Request Payload有什么區別? application/x-www-form-urlencoded和application/json有什么區別?開發中我們應該怎么選擇?- 為什么后端有時會無法解析自己發送的數據?
- 在
POST的跨域請求中,有辦法不發送OPTIONS預檢請求也能發送數據的方法么?
話不多說,直接進入主題。
發現問題,從兩個截圖開始


這兩個截圖就是寫這篇文章的初衷,微信文章在打開的時候是顯示的 Form Data,第二張圖是掘金在打開文章發起的請求,當時看到就特奇怪,Form Data 和 Request Payload 這倆貨有啥區別?為啥都是 POST 請求,但卻有兩種發送數據的方式?
我這個人就是屬於碰到這種奇怪的問題不把他搞清楚就睡不了覺的人,我們直接在本地場景重現,好好看看這倆貨。
如果不想看中間的分析過程,可以直接點擊 總結 看傑倫。
場景重現
本地起兩個服務,前端和后端,通過創建 XMLHttpRequest 對象來進行數據傳輸,並通過 setRequestHeader() 來改變 Content-Type,最終我們在調試工具中完美重現了兩種模式。
文章里的示例代碼都可以從這個倉庫里找到,希望自己親自嘗試的小伙伴可以點擊查看詳情 示例地址。
git clone -b demo/study-post-request https://github.com/jsjzh/tiny-codes.git
Request Payload
如果希望看到 Request Payload,需要設置請求頭部 Content-Type: application/json,再將數據經過 JSON.stringify 序列化后發送。

大家可以看到我這里的
Origin: http://localhost.charlesproxy.com:3000,這是因為要用 charles 抓本地包,得用這做一層代理
直接上抓包的截圖

上半部分就是一個完整的 http 請求,空行上面為請求頭,空行下面是請求體,可以看到我們的請求體就是一個 json 序列化后的字符串。
下半部分,注意 JSON 和 JSON Text 兩個 tab,這個是我們設置了 Content-Type: application/json 了之后,charles 自動會給帶上的。
后端接到 http 請求后,就是截取空行后的這個請求體解析,因為我們傳了 Content-Type: application/json,所以后端知道請求體是一個 json 字符串,就可以用 JSON.parse 來解析。
發送的數據為
{
"name": "king",
"age": 18,
"isAdmain": true,
"groups": [1, 2, 3],
"address": "",
"foo": null,
"bar": undefined,
"extra": { "wechat": "kimimi_king", "qq": 454075623 }
}
解析的數據為
{
"name": "king",
"age": 18,
"isAdmain": true,
"groups": [1, 2, 3],
"address": "",
"foo": null,
"extra": { "wechat": "kimimi_king", "qq": 454075623 }
}
可以看到除了 bar: undefined 之外,number、boolean 和 null,數據類型都被正確的傳輸了。
Form Data
再來說說 Form Data,我們需要設置 Content-Type: application/x-www-form-urlencoded,再將數據通過 qs.stringify 序列化后再發送。
qs 即為 qs npm source,是一個將數據 querystring 化的庫
可以簡單理解成他可以把一個對象轉換成類似 get 請求中 ? 后面的查詢字段
key=data&key2=data2如果不經過 qs 處理直接發送,方法會使用
toString()來將數據轉為字符串,如果傳輸的是對象,你會得到[object Object]

這里也直接貼出抓包的截圖

上半部分就是 http 請求,可以看到當我們設置 Content-Type: application/x-www-form-urlencoded 請求體也是放在了空行之后。
下半部分,對比剛才的 application/json 就能發現不一樣的地方了,JSON 和 JSON Text 的 tab 不見了,取而代之的是 Form tab。
后端接到 http 請求之后,也是截取的空行后面的請求體,並使用 qs.parse 進行解析。
發送的數據為
{
"name": "king",
"age": 18,
"isAdmain": true,
"groups": [1, 2, 3],
"address": "",
"foo": null,
"bar": undefined,
"extra": { "wechat": "kimimi_king", "qq": 454075623 }
}
解析的數據為
{
"name": "king",
"age": "18",
"isAdmain": "true",
"groups": ["1", "2", "3"],
"address": "",
"foo": "",
"extra": { "wechat": "kimimi_king", "qq": "454075623" }
}
經過和 Content-Type: application/json 對比,我們可以看到,不僅 number 和 boolean 的數據類型丟失,並且 foo: null 還被轉換成了 foo: ""。
交換序列化方式
剛才我們嘗試了正確的 Content-Type 對應正確的序列化方式
application/json + JSON.stringify
application/x-www-form-urlencoded + qs.stringify
但其實我們觀察到實際的 http 請求,這兩個 Content-Type 都是將數據放在空行后傳輸,所以我們當然也可以交換他們的序列化方式。
application/json + qs.stringify


這里直接就說結論,我們設置了 application/json,但使用 qs.stringify 序列化,結果就是
- chrome 調試工具的
Request Payload無法解析,遂無法格式化數據 - charles 工具的
JSON和JSON Text無法解析 - 最重要的,后端若是讀取了
Content-Type為application/json,就會使用JSON.parse來解析數據
在后端我們當然可以手動用
qs.parse來進行解析,但是我們為什么要給自己埋坑?
application/x-www-form-urlencoded + JSON.stringify


同理,使用了 Content-Type 和不正確的序列化方式,不僅 chrome 和 charles 無法解析,后端也會有疑惑,更重要的是會給自己埋坑。
總結

誒,沒錯,我就想皮一下
前面說了這么多,現在來總結一下
Form Data和Request Payload就是因為請求的Content-Type不同,而不同的解析請求體后的呈現方式Content-Type設置成application/json還是application/x-www-urlencoded在 http 請求中,除了Header以外並無區別,都是將請求體放在空行后
那我們在開發中應該如何選擇 Content-Type?建議如果不是項目有特別要求,都使用 application/json,原因有以下幾點
- 原生自帶的
JSON.stringify和JSON.parse不香么?qs 在前端就有很多實現,比如qs和query-string,還有 node 自帶的querystring x-www-form-urlencoded需要使用配套qs.stringify,后端解析數據后會丟失數據類型,比如number、boolean、null- 不同的框架對於
qs.parse的實現方式不同,在項目剛開始對接時可能會有前后端對齊解析方式的操作 - 前端的
qs倉庫默認只能處理 5 層對象,默認只能解析 1000 個參數(當然,這兩個配置都可以修改)舉一個例子
{
"a": {
"b": {
"c": {
"d": {
"e": {
"f": {
"g": { "name": "king" }
}
}
}
}
}
}
}
因為對象嵌套的層數太深,解析后就成了如下
{
"a": {
"b": {
"c": {
"d": {
"e": {
"[f][g][name]": "king"
}
}
}
}
}
}
當然,使用了 application/json 之后會有些不一樣
- 配置頭部
Content-Type: application/json之后就不是簡單請求,會發起一個Options預檢請求 - 后端需要同步配置
Access-Control-Request-Headers: Content-Type,允許前端配置Content-Type頭部
當然,再說下去就是 CORS 的知識點了,這方面也有很多內容可以掰開細說,我也正在整理這方面的內容,可以小小的期待一下。
后語
不知道這篇文章是否給你帶來了一些幫助,如果有的話是我的榮幸,在平時碰到問題的時候不妨可以挖的深一點,就像這次的 Form Data 和 Request Payload,當我們挖掘到 http 請求層面就能發現兩者其實並無區別,就是瀏覽器對於 http 協議的一種封裝,而正確的使用 Content-Type 就是我們和后端聯調的一個約定,也是一個規范。
我們當然可以隨意設置 Content-Type,但是這就需要和后端進行非必要聯調,並且也不方便后續理解維護,所以我們能簡單就簡單一些,有些框架會自動根據 Content-Type 的值來解析請求體,頭發已經這么少了,我們就不要強行增加游戲難度了。
頁腳
代碼即人生,我甘之如飴。
技術不斷在變
頭腦一直在線
前端路漫漫
我們下期見by --- 褲襠三重奏
我在這里 gayhub@jsjzh 歡迎大家來找我玩兒。
歡迎小伙伴們直接加我,拉你進群一起搞事情,記得備注一下你是從哪里看到文章的。
ps: 如果圖片失效,可以加我 wechat: kimimi_king
