動態

詳情 返回 返回

從 useState 到 URLState:為什麼大佬們都在刪狀態管理代碼?

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. 參考鏈接

  1. Your URL Is Your State
user avatar
0 用戶, 點贊了這篇動態!

發表 評論

Some HTML is okay.