面試官:觀察過 chrome 調試工具的請求體么?Form Data 和 Request Payload 有什么區別?


前言

這篇文章旨在記錄自己解惑過程,比如

  1. 在 chrome 調試工具中,Form DataRequest Payload 有什么區別?
  2. application/x-www-form-urlencodedapplication/json 有什么區別?開發中我們應該怎么選擇?
  3. 為什么后端有時會無法解析自己發送的數據?
  4. POST 的跨域請求中,有辦法不發送 OPTIONS 預檢請求也能發送數據的方法么?

話不多說,直接進入主題。

發現問題,從兩個截圖開始

微信請求

掘金請求

這兩個截圖就是寫這篇文章的初衷,微信文章在打開的時候是顯示的 Form Data,第二張圖是掘金在打開文章發起的請求,當時看到就特奇怪,Form DataRequest 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 序列化后發送。

chrome application/json

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

直接上抓包的截圖

application/json 抓包

上半部分就是一個完整的 http 請求,空行上面為請求頭,空行下面是請求體,可以看到我們的請求體就是一個 json 序列化后的字符串。

下半部分,注意 JSONJSON 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 之外,numberbooleannull,數據類型都被正確的傳輸了。

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]

chrome application/x-www-form-urlencoded

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

application/x-www-form-urlencoded 抓包

上半部分就是 http 請求,可以看到當我們設置 Content-Type: application/x-www-form-urlencoded 請求體也是放在了空行之后。

下半部分,對比剛才的 application/json 就能發現不一樣的地方了,JSONJSON 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 對比,我們可以看到,不僅 numberboolean 的數據類型丟失,並且 foo: null 還被轉換成了 foo: ""

交換序列化方式

剛才我們嘗試了正確的 Content-Type 對應正確的序列化方式

application/json + JSON.stringify

application/x-www-form-urlencoded + qs.stringify

但其實我們觀察到實際的 http 請求,這兩個 Content-Type 都是將數據放在空行后傳輸,所以我們當然也可以交換他們的序列化方式。

application/json + qs.stringify

image.png

image.png

這里直接就說結論,我們設置了 application/json,但使用 qs.stringify 序列化,結果就是

  1. chrome 調試工具的 Request Payload 無法解析,遂無法格式化數據
  2. charles 工具的 JSONJSON Text 無法解析
  3. 最重要的,后端若是讀取了 Content-Typeapplication/json,就會使用 JSON.parse 來解析數據

在后端我們當然可以手動用 qs.parse 來進行解析,但是我們為什么要給自己埋坑?

application/x-www-form-urlencoded + JSON.stringify

image.png

image.png

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

總結

image.png

誒,沒錯,我就想皮一下

前面說了這么多,現在來總結一下

  1. Form DataRequest Payload 就是因為請求的 Content-Type 不同,而不同的解析請求體后的呈現方式
  2. Content-Type 設置成 application/json 還是 application/x-www-urlencoded 在 http 請求中,除了 Header 以外並無區別,都是將請求體放在空行后

那我們在開發中應該如何選擇 Content-Type?建議如果不是項目有特別要求,都使用 application/json,原因有以下幾點

  1. 原生自帶的 JSON.stringifyJSON.parse 不香么?qs 在前端就有很多實現,比如 qsquery-string,還有 node 自帶的 querystring
  2. x-www-form-urlencoded 需要使用配套 qs.stringify后端解析數據后會丟失數據類型,比如 numberbooleannull
  3. 不同的框架對於 qs.parse 的實現方式不同,在項目剛開始對接時可能會有前后端對齊解析方式的操作
  4. 前端的 qs 倉庫默認只能處理 5 層對象,默認只能解析 1000 個參數(當然,這兩個配置都可以修改)舉一個例子
{
  "a": {
    "b": {
      "c": {
        "d": {
          "e": {
            "f": {
              "g": { "name": "king" }
            }
          }
        }
      }
    }
  }
}

因為對象嵌套的層數太深,解析后就成了如下

{
  "a": {
    "b": {
      "c": {
        "d": {
          "e": {
            "[f][g][name]": "king"
          }
        }
      }
    }
  }
}

當然,使用了 application/json 之后會有些不一樣

  1. 配置頭部 Content-Type: application/json 之后就不是簡單請求,會發起一個 Options 預檢請求
  2. 后端需要同步配置 Access-Control-Request-Headers: Content-Type,允許前端配置 Content-Type 頭部

當然,再說下去就是 CORS 的知識點了,這方面也有很多內容可以掰開細說,我也正在整理這方面的內容,可以小小的期待一下。

后語

不知道這篇文章是否給你帶來了一些幫助,如果有的話是我的榮幸,在平時碰到問題的時候不妨可以挖的深一點,就像這次的 Form DataRequest Payload,當我們挖掘到 http 請求層面就能發現兩者其實並無區別,就是瀏覽器對於 http 協議的一種封裝,而正確的使用 Content-Type 就是我們和后端聯調的一個約定,也是一個規范。

我們當然可以隨意設置 Content-Type,但是這就需要和后端進行非必要聯調,並且也不方便后續理解維護,所以我們能簡單就簡單一些,有些框架會自動根據 Content-Type 的值來解析請求體,頭發已經這么少了,我們就不要強行增加游戲難度了。

頁腳

代碼即人生,我甘之如飴。

技術不斷在變
頭腦一直在線
前端路漫漫
我們下期見

by --- 褲襠三重奏

我在這里 gayhub@jsjzh 歡迎大家來找我玩兒。

歡迎小伙伴們直接加我,拉你進群一起搞事情,記得備注一下你是從哪里看到文章的。

ps: 如果圖片失效,可以加我 wechat: kimimi_king


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM