1. 前言
當你打開這個網址時:
https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript&plugins=line-numbers
你會發現,所有你需要的主題、語言、插件已經被自動勾選:
當你在頁面修改配置時,URL 也會隨之改變。
你看,這個 URL 不僅僅是一個鏈接,更是一個完整的狀態容器,保存了我的所有配置。無需數據庫、cookie 或 localStorage,一個 URL 就解決了一切。
2. 被忽視的 URL 超能力
URL 是互聯網最偉大的創意之一,通過 URL 請求,我們可以查找到網絡上的唯一資源。
它的標準格式為:<scheme>://<netloc>/<path>?<query>#<fragment>。
但 URL 的價值遠不止於此——它們是天然的狀態管理解決方案。想想 URL 給我們帶來的好處:
- 可分享性:發送鏈接,對方會看到與你完全相同的內容
- 可書籤化:保存 URL 就是保存一個特定時刻的狀態
- 瀏覽器歷史:後退按鈕正常工作
- 深度鏈接:直接跳轉到應用的特定狀態
URL 使 Web 應用具有韌性和可預測性。它們是 Web 最初的狀態管理方案,自 1990 年以來就開始使用,所以千萬不要忘記使用這種方式。
3. URL 如何編碼狀態?
URL 的不同部分編碼不同類型的狀態:
路徑段(/path/to/myfile.html):最適合層次化資源導航
/users/123/posts # 用户123的文章
/docs/api/authentication # 文檔結構
查詢參數(?key1=value1&key2=value2):完美用於過濾器、選項和配置
?theme=dark&lang=en # UI 偏好設置
?page=2&limit=20 # 分頁
?status=active&sort=date # 數據過濾
錨點片段(#SomewhereInTheDocument):適合客户端導航和頁面部分
#L20-L35 # GitHub 行高亮
#features # 滾動到某個章節
4. URL 編碼狀態常見模式
4.1. 多個帶分隔符的值
?languages=javascript+typescript+python
?tags=frontend,react,hooks
這種方式簡潔易讀,但需要在服務器端手動解析。
4.2. 嵌套或結構化數據
?filters=status:active,owner:me,priority:high
?config=eyJyaWNrIjoicm9sbCJ9== (base64-encoded JSON)
開發者有時會將複雜的篩選器或配置對象編碼到單個查詢字符串中。
一種簡單的約定是使用逗號分隔的鍵值對,而其他方法則會序列化 JSON,甚至為了安全起見對其進行 Base64 編碼。
4.3. 數組處理(方括號表示法)
?tags[]=frontend&tags[]=react&tags[]=hooks
?ids[0]=42&ids[1]=73
一種古老的模式是方括號表示法,它用於在查詢參數中表示數組。這種表示法起源於早期的 Web 框架,例如 PHP,在 [] 參數名稱後添加括號表示多個值應該組合在一起。
許多現代框架和解析器(例如 Node 的 qs 庫或 Express 中間件)仍然能夠自動識別這種模式。然而,它並未在 URL 規範中正式標準化,因此其行為可能因服務器或客户端的實現而異。
4.4. 布爾處理
對於 flag 或開關,通常會顯式傳遞布爾值,或者依賴於鍵值是否為真。這樣可以縮短 URL 長度,並簡化功能切換。
?debug=true&analytics=false
?mobile (presence = true)
4.5. 結論
使用哪種模式都是可以的,關鍵在於保持一致性。選擇適合你應用場景的模式,並堅持使用。
5. 實際應用案例
GitHub 行高亮:
https://github.com/zepouet/Xee-xCode-4.5/blob/master/XeePhotoshopLoader.m#L108-L136
鏈接到特定文件,同時高亮顯示 108-136 行。點擊此鏈接,你會直接定位到討論的確切代碼部分。
電商數據過濾器:
https://store.com/laptops?brand=dell+hp&price=500-1500&rating=4&sort=price-asc
這是最常見的實現。每個過濾條件、排序選項都被保存。用户可以用書籤保存他們的篩選條件。
谷歌地圖:
https://www.google.com/maps/@22.443842,-74.220744,19z
座標、縮放級別和地圖類型都包含在 URL 中。分享此鏈接,任何人都可以看到完全相同的地圖視圖。
6. 什麼狀態應該放入 URL?
然而並非所有狀態都應該屬於 URL,那什麼樣的狀態應該放入 URL 呢?
適合 URL 狀態:
- 搜索查詢和篩選器
- 分頁和排序
- 視圖模式(列表/網格、深色/淺色)
- 日期範圍和時間段
- 選中項或活動標籤
- 影響內容的 UI 配置
- 功能開關和 A/B 測試版本
不適合 URL 狀態:
- 敏感信息(密碼、令牌、個人身份信息)
- 臨時 UI 狀態(模態框打開/關閉)
- 表單輸入進行中(未保存的更改)
- 極其龐大或複雜的嵌套數據
- 高頻瞬態(鼠標位置、滾輪位置)
簡單來説,你的判斷標準是:
如果別人點擊這個 URL,他們應該看到相同的狀態嗎?
如果是,它就屬於 URL。
7. 實現方案
7.1. 使用純 JavaScript 實現
現代 URLSearchParams API 使 URL 狀態管理變得簡單:
// 讀取URL參數
const params = new URLSearchParams(window.location.search);
const view = params.get("view") || "grid"; // 默認值
const page = parseInt(params.get("page")) || 1;
// 更新URL參數
function updateFilters(filters) {
const params = new URLSearchParams(window.location.search);
params.set("status", filters.status);
params.set("sort", filters.sort);
// 更新URL而不重新加載頁面
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.pushState({}, "", newUrl);
}
// 處理後/前進按鈕
window.addEventListener("popstate", () => {
const params = new URLSearchParams(window.location.search);
const filters = {
status: params.get("status") || "all",
sort: params.get("sort") || "date",
};
renderContent(filters);
});
7.2. 使用 React 實現
React Router 提供了更簡潔的鈎子:
import { useSearchParams } from "react-router-dom";
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
const color = searchParams.get("color") || "all";
const sort = searchParams.get("sort") || "price";
const handleColorChange = (newColor) => {
setSearchParams((prev) => {
const params = new URLSearchParams(prev);
params.set("color", newColor);
return params;
});
};
return (
<select value={color} onChange={(e) => handleColorChange(e.target.value)}>
<option value="all">所有顏色</option>
<option value="silver">銀色</option>
</select>
);
}
8. URL 使用最佳實踐
8.1. 優雅處理默認值
不要在 URL 中使用默認值:
// ❌
?theme=light&lang=en&page=1&sort=date
// ✅
?theme=dark // light 是默認的,但 dark 不是默認的
在代碼中讀取參數時使用默認值:
function getTheme(params) {
return params.get("theme") || "light"; // 在代碼中設置默認值
}
8.2. URL 更新防抖動
對於高頻更新(例如邊輸入邊搜索),要對 URL 更改進行防抖處理:
import { debounce } from "lodash";
const updateSearchParam = debounce((value) => {
const params = new URLSearchParams(window.location.search);
if (value) {
params.set("q", value);
} else {
params.delete("q");
}
window.history.replaceState({}, "", `?${params.toString()}`);
}, 300);
8.3. URL 傳達意義
https://example.com/p?id=x7f2k&v=3 ❌
https://example.com/products/laptop?color=silver&sort=price ✅
第一個鏈接隱藏了意圖,第二個鏈接則意義清晰。人可以閲讀它並理解其含義。機器可以解析它並提取有意義的結構。這才是優秀的 URL。
9. 使用時要避免的反模式
9.1. 狀態都保存在內存中的單頁應用程序
// 用户一刷新,狀態都丟失了
const [filters, setFilters] = useState({});
如果你的應用在刷新後丟失了之前的狀態,你就破壞了網絡的一項基本功能。用户期望 URL 能夠保留上下文。
9.2. 包含敏感數據
// 別這樣幹
?password=secret123
9.3. 命名不一致或晦澀難懂
// 晦澀難懂
?foo=true&bar=2&x=dark
// 自文檔化且風格保持一致
?mobile=true&page=2&theme=dark
9.4. 注意 URL 長度限制
瀏覽器和服務器對 URL 長度都有實際的限制(通常在 2000 到 8000 個字符之間),但實際情況更為複雜,會有來自瀏覽器行為、服務器配置、CDN 甚至搜索引擎的限制等多種因素。
如果你遇到了這些限制,那就説明你需要重新考慮你的策略了。
10. 總結
好的 URL 不僅僅是指向內容,它更是描述了用户和應用程序之間的對話。
我們已經構建了複雜的狀態管理庫,但有時最好的解決方案其實是最簡單的那一個。當你的應用在點擊刷新時失去了狀態,想一想,你是否錯過了這個 Web 最古老、最優雅的特性?
11. 參考鏈接
- Your URL Is Your State