1. 前言
当你打开这个网址时:
plain
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):最适合层次化资源导航
plain
/users/123/posts # 用户123的文章
/docs/api/authentication # 文档结构
查询参数(?key1=value1&key2=value2):完美用于过滤器、选项和配置
plain
?theme=dark&lang=en # UI 偏好设置
?page=2&limit=20 # 分页
?status=active&sort=date # 数据过滤
锚点片段(#SomewhereInTheDocument):适合客户端导航和页面部分
plain
#L20-L35 # GitHub 行高亮
#features # 滚动到某个章节
4. URL 编码状态常见模式
4.1. 多个带分隔符的值
plain
?languages=javascript+typescript+python
?tags=frontend,react,hooks
这种方式简洁易读,但需要在服务器端手动解析。
4.2. 嵌套或结构化数据
plain
?filters=status:active,owner:me,priority:high
?config=eyJyaWNrIjoicm9sbCJ9== (base64-encoded JSON)
开发者有时会将复杂的筛选器或配置对象编码到单个查询字符串中。
一种简单的约定是使用逗号分隔的键值对,而其他方法则会序列化 JSON,甚至为了安全起见对其进行 Base64 编码。
4.3. 数组处理(方括号表示法)
plain
?tags[]=frontend&tags[]=react&tags[]=hooks
?ids[0]=42&ids[1]=73
一种古老的模式是方括号表示法,它用于在查询参数中表示数组。这种表示法起源于早期的 Web 框架,例如 PHP,在 [] 参数名称后添加括号表示多个值应该组合在一起。
许多现代框架和解析器(例如 Node 的 qs 库或 Express 中间件)仍然能够自动识别这种模式。然而,它并未在 URL 规范中正式标准化,因此其行为可能因服务器或客户端的实现而异。
4.4. 布尔处理
对于 flag 或开关,通常会显式传递布尔值,或者依赖于键值是否为真。这样可以缩短 URL 长度,并简化功能切换。
plain
?debug=true&analytics=false
?mobile (presence = true)
4.5. 结论
使用哪种模式都是可以的,关键在于保持一致性。选择适合你应用场景的模式,并坚持使用。
5. 实际应用案例
GitHub 行高亮:
plain
https://github.com/zepouet/Xee-xCode-4.5/blob/master/XeePhotoshopLoader.m#L108-L136
链接到特定文件,同时高亮显示 108-136 行。点击此链接,你会直接定位到讨论的确切代码部分。
电商数据过滤器:
plain
https://store.com/laptops?brand=dell+hp&price=500-1500&rating=4&sort=price-asc
这是最常见的实现。每个过滤条件、排序选项都被保存。用户可以用书签保存他们的筛选条件。
谷歌地图:
plain
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 状态管理变得简单:
javascript
// 读取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 提供了更简洁的钩子:
jsx
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 中使用默认值:
jsx
// ❌
?theme=light&lang=en&page=1&sort=date
// ✅
?theme=dark // light 是默认的,但 dark 不是默认的
在代码中读取参数时使用默认值:
jsx
function getTheme(params) {
return params.get("theme") || "light"; // 在代码中设置默认值
}
8.2. URL 更新防抖动
对于高频更新(例如边输入边搜索),要对 URL 更改进行防抖处理:
jsx
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 传达意义
jsx
https://example.com/p?id=x7f2k&v=3 ❌
https://example.com/products/laptop?color=silver&sort=price ✅
第一个链接隐藏了意图,第二个链接则意义清晰。人可以阅读它并理解其含义。机器可以解析它并提取有意义的结构。这才是优秀的 URL。
9. 使用时要避免的反模式
9.1. 状态都保存在内存中的单页应用程序
plain
// 用户一刷新,状态都丢失了
const [filters, setFilters] = useState({});
如果你的应用在刷新后丢失了之前的状态,你就破坏了网络的一项基本功能。用户期望 URL 能够保留上下文。
9.2. 包含敏感数据
plain
// 别这样干
?password=secret123
9.3. 命名不一致或晦涩难懂
plain
// 晦涩难懂
?foo=true&bar=2&x=dark
// 自文档化且风格保持一致
?mobile=true&page=2&theme=dark
9.4. 注意 URL 长度限制
浏览器和服务器对 URL 长度都有实际的限制(通常在 2000 到 8000 个字符之间),但实际情况更为复杂,会有来自浏览器行为、服务器配置、CDN 甚至搜索引擎的限制等多种因素。
如果你遇到了这些限制,那就说明你需要重新考虑你的策略了。
10. 总结
好的 URL 不仅仅是指向内容,它更是描述了用户和应用程序之间的对话。
我们已经构建了复杂的状态管理库,但有时最好的解决方案其实是最简单的那一个。当你的应用在点击刷新时失去了状态,想一想,你是否错过了这个 Web 最古老、最优雅的特性?