日誌裡找不到頁面跳轉時帶的那段錨點,中文搜尋詞在日誌裡變成了一串百分號,金鑰裡的加號莫名變成空格——這三件事都指向同一個源頭:一條網址並不是一整塊送到伺服器的,它被切成六段,每一段的命運不一樣。
六段分別在哪裡
| 段 | 本文實測用的位址 | 會不會送給伺服器 |
|---|---|---|
| 協定 scheme | http | 決定連接埠與是否加密 |
| 主機 host | 127.0.0.1 | 會 |
| 連接埠 port | 8198 | 會;省略時按協定取預設值,http 是 80、https 是 443 |
| 路徑 path | /a/b/c | 會 |
| 查詢 query | x=1&y=2 | 會,帶的是編碼原文 |
| 錨點 fragment | section-3 | 不會 |
實測一:錨點根本沒出瀏覽器
在本機起了一個最小服務,把收到的欄位原樣印出來。請求 http://127.0.0.1:8198/a/b/c?x=1&y=2#section-3 時,伺服器端記錄的是 REQUEST_URI 等於 /a/b/c?x=1&y=2,QUERY_STRING 等於 x=1&y=2,解析後拿到 x=1、y=2,而 section-3 哪裡都沒有出現。所以後台日誌查不到錨點不是欄位丟了,是它從來不上網:頁內定位、前端路由的 hash 都由瀏覽器自己處理。
實測二:加號能當空格,反過來不行
同一組請求裡,q=a+b+c 和 q=a%20b%20c 在伺服器端都解析成 a b c;而 token=a%2Bb 解析出來是字面的 a+b,寫成 token=a+b 得到的卻是「a b」。區別在於查詢字串的解碼規則把加號當成空格的一種寫法,所以你要傳一個真正的加號就必須寫成 %2B。直接拼上去會被解成空格,很多「金鑰莫名不對」的排查最後就落在這一個字元上。
實測三:中文與二次編碼
請求 q=%E4%B8%AD%E6%96%87,QUERY_STRING 裡看到的是編碼原文,而解析後的參數已經是中文。如果同一個字串被編碼了兩遍,位址裡就成 q=%25E4%25B8%25AD,伺服器端解一次得到的是 %E4%B8%AD——仍然帶著百分號,說明只解了一層。這類現象沒有玄學,唯一可靠的解法是數清楚自己解了幾次:傳輸層解一次,業務層若還要再解就得顯式約定。
實測四:同名鍵、沒有鍵的值、非法轉義
重複的鍵名不會自動變成陣列。?q=a&q=b 解析後只剩最後一個值 b;要同時拿到兩個,得寫成 q[]=a&q[]=b,伺服器端才給出含兩項的陣列。只有值沒有鍵的片段更乾脆,?=onlyvalue&k= 解析出來只有一個空字串的 k,那個孤零零的 onlyvalue 直接消失,任何程式碼都取不到它。
鍵名裡也能藏編碼。?a=1&%20b=2&c=%20 這一串,伺服器端解出的鍵是 a 和 b——前導空格被吃掉,而值末尾的 %20 老實變成一個空格保留下來。非法寫法同樣不報錯:q=100%、q=a%zzb 都原樣到了伺服器端,解析結果裡仍然是 100% 和 a%zzb。這只說明本機這個解析器寬容——百分號本該由兩個十六進位字元收尾,寫成裸 % 時不能指望鏈路上每個中介軟體都用同樣的方式放過它。真要傳一個字面的百分號,就寫 %25,實測 q=100%25 解析出來是 100%。
哪些字元在查詢值裡必須轉義
要盯的是那幾個承擔結構職責的字元:& 會被當成分隔符,= 會被當成鍵值分界,+ 會被當成空格,# 會當場把整條查詢截斷,% 是所有轉義的起始符。實測把 q=%3D%2B%26%25 送出去,伺服器端解出的是 =+&% 四個字元,一個不多一個不少,說明一次解碼就還原到位。斜線反而不必管,?path=/x/y 原樣通過。
路徑段是另一套:# 一旦以本來面目出現,它後面的內容全部留在瀏覽器裡;寫成 %23 就只是路徑裡的三個字元,實測 /a%23b/c?q=1 到伺服器端的 REQUEST_URI 仍是 /a%23b/c,查詢照常解析。同類地,/a%2Fb/c 裡的 %2F 會保持編碼態傳到伺服器端,是否被解成一個額外的路徑分隔,就交給各框架的路由規則決定了——這正是「路徑裡帶斜線要不要轉義」不能靠猜的原因。
表單體和查詢字串不是同一套習慣
以 application/x-www-form-urlencoded 提交時,請求體裡的編碼規則和查詢字串一致,空格同樣寫成 +;換成 JSON 請求體之後,這些字元由字串本身承載,不需要百分比轉義。把查詢字串的編碼習慣帶進 JSON,就會存下帶 %20 的髒資料。同一段中文,走查詢要寫成 %E4%B8%AD%E6%96%87,走 JSON 只需要正確的 UTF-8 編碼。
拆分怎麼做省事
排查時我習慣把整條位址貼進 URL 解析工具,讓它把六段列出來,順帶把 QUERY_STRING 原文與解析後的參數並排看清;需要手工往某個值裡塞中文或特殊字元時,用 URL 編解碼 產生轉義結果,別靠肉眼拼百分號,長度一長必錯。
Q:路徑裡的中文和查詢裡的中文編碼規則一樣嗎
都用百分比編碼,但允許的字元集不同:路徑段裡有若干字元可以原樣出現,查詢字串則要把當分隔符用的等號、與號轉義掉。整串交給標準編碼函式,比逐字元判斷安全。
Q:為什麼日誌裡存的是編碼態
因為存取日誌記的是 QUERY_STRING 原文,解碼發生在框架解析參數的那一步。上面實測輸出的同一行裡,兩個欄位分別就是編碼原文和解碼結果,可以直接對照。
Q:hash 路由的頁面切換能被後端統計到嗎
不能,錨點不出瀏覽器。要按頁面維度統計,就得讓不同檢視有不同的路徑或查詢參數,這也是前端路由從 hash 模式改走 history 模式的主要原因。
Q:用 hash 還是 query 存頁面狀態
看要不要離開瀏覽器。改 hash 不會發出請求,不進伺服器端日誌、不被爬蟲當成不同頁面,適合純前端的展開、定位、分頁游標;一旦這個值需要伺服器端讀到——篩選條件、分享連結、埋點維度——就必須放進路徑或查詢,因為實測裡 REQUEST_URI 從頭到尾不含 # 之後的任何東西。