在构建 Web 应用时,我们常依赖复杂的状态管理工具,却忽视了最简单也最强大的方式——URL 本身。这篇文章从一个小发现出发,重新审视 URL 作为状态容器的价值:可分享、可恢复、可理解。
原文链接:alfy.blog
几周前,我在发布《URL 设计的隐藏成本》时,需要添加 SQL 语法高亮。我打开了 PrismJS 的网站想确认到底需要插件还是别的什么。但在下载页面被大量选项淹没,我又回到自己的代码里。在 PrismJS 的文件顶部,我发现一条注释中居然有一个 URL:
/* https://prismjs.com/download.html#themes=prism&languages=markup+css+clike+javascript+bash+css-extras+markdown+scss+sql&plugins=line-highlight+line-numbers+autolinker */
我完全忘记了这回事。我点开这个 URL,然后出现的,正是 PrismJS 下载页面,其中所有复选框、下拉框、选项都完全按照我当时的配置自动勾选好了。主题选定,语言选定,插件启用,一切都被从这个 URL 完美重建。
那一刻,就像某个你曾经知道的东西突然以新的意义重新点亮。这个 URL 不只是指向一个页面,它在存储状态、编码意图,让我的整个配置可分享、可恢复。不需要数据库。不需要 cookies。不需要 localStorage。只要一个 URL。
于是我开始思考:作为前端工程师,我们到底有多经常忽视 URL 作为状态管理工具?为了管理状态我们会使用各种抽象方式,例如全局 store、context、缓存等,却常常忽略了 web 中最优雅、最古老的一个特性:URL。
在上一篇文章中,我讨论了糟糕 URL 设计的隐藏成本。今天,我想从另一个角度出发,谈谈良好 URL 设计的巨大价值。具体来说,URL 如何在现代 Web 应用中成为一等公民的状态容器。
URL 的被忽视的力量
Scott Hanselman 曾说过“URLs are UI(URL 就是 UI)”,这句话再正确不过。URL 不仅是浏览器用来获取资源的技术地址,它们也是界面,是用户体验的一部分。
但 URL 还远不止 UI ——它们是状态容器。每次你构建一个 URL,你其实都在决定:
- 哪些信息应该被保存
- 哪些应该可分享
- 哪些应该可被收藏
- 哪些应该参与浏览器历史
换句话说,URL 提供了一些我们常常视为理所当然的特性:
- 可分享:把链接发给别人,他们看到的就是你看到的
- 可收藏:保存一个 URL,就是保存一个时刻
- 浏览器历史:后退按钮自动工作
- 深链接:跳入应用的任意内部状态
URL 让 Web 应用变得具有弹性、可预期。它是 Web 上最早的状态管理方案,从 1991 年以来一直稳定运作。问题不是 URL 是否能存储状态,而是我们是否充分利用它。
URL 如何编码状态
下面是一个典型的带状态的 URL:
/download.html#themes=prism&languages=markup+css+clike+javascript+bash+css-extras+markdown+scss+sql&plugins=line-highlight+line-numbers+autolinker
不同部分的 URL 编码不同种类的状态:
1. 路径段(Path Segments)
/users/123/posts
/docs/api/authentication
/dashboard/analytics
适合层级结构、资源导航。
2. 查询参数(Query Parameters)
?theme=dark&lang=en
?page=2&limit=20
?status=active&sort=date
?from=2025-01-01&to=2025-12-31
适合过滤器、配置项、小型结构化数据等。
3. 锚点(#Fragment)
#L20-L35
#features
#/dashboard (旧式 SPA)
文本片段(Text Fragments)
这是一种较新的能力:允许直接链接到页面中的某段文字。
查询参数的常见模式
1. 多值参数(使用分隔符)
?languages=javascript+typescript+python
?tags=frontend,react,hooks
2. 嵌套或结构化数据
?filters=status:active,owner:me,priority:high
?config=eyJyaWNrIjoicm9sbCJ9== // Base64 JSON
3. 布尔开关(Presence = true)
?debug=true&analytics=false
?mobile // 出现即为 true
4. 数组(Bracket Notation)
?tags[]=frontend&tags[]=react&tags[]=hooks
?ids[0]=42&ids[1]=73
关键始终是 —— 一致性。
使用 URL 作为状态容器的真实案例
PrismJS 配置
download.html#themes=prism&languages=markup+css+clike+javascript&plugins=line-numbers
GitHub 行号高亮
/#L108-L136
Google Maps
包含坐标、缩放级别、视野等:
/maps/@22.443842,-74.220744,19z
Figma
MyDesign?node-id=123:456&viewport=100,200,0.5
包含节点、画布位置、缩放比例等。
电商过滤器
?brand=dell+hp&price=500-1500&rating=4&sort=price-asc
哪些状态适合放在 URL?
✔ 适合
- 搜索条件、过滤器
- 分页与排序
- 视图模式(列表/网格、深浅色)
- 日期范围
- 活跃 tab、选择项
- UI 配置
- A/B 测试参数
✘ 不适合
- 敏感信息(密码、token、个人隐私)
- 暂时性 UI 状态(弹窗是否打开)
- 临时输入内容
- 过大或复杂的数据结构
- 高频变化的瞬时状态
判断标准很简单:
如果别人点击你的 URL 应该看到同样的状态,这个状态就应该放进 URL。
JavaScript 实现示例
读取参数
const params = new URLSearchParams(window.location.search);
const view = params.get('view') || 'grid';
const page = params.get('page') || 1;
更新参数
function updateFilters(filters) {
const params = new URLSearchParams(window.location.search);
params.set('status', filters.status);
params.set('sort', filters.sort);
const newUrl = `${window.location.pathname}?${params.toString()}`;
window.history.pushState({}, '', newUrl);
renderContent(filters);
}
监听 Back/Forward
window.addEventListener('popstate', () => {
const params = new URLSearchParams(window.location.search);
const filters = {
status: params.get('status') || 'all',
sort: params.get('sort') || 'date'
};
renderContent(filters);
});
React 实现示例
import { useSearchParams } from 'react-router-dom';
// or for Next.js 13+: import { useSearchParams } from 'next/navigation';
function ProductList() {
const [searchParams, setSearchParams] = useSearchParams();
// Read from URL (with defaults)
const color = searchParams.get('color') || 'all';
const sort = searchParams.get('sort') || 'price';
// Update URL
const handleColorChange = (newColor) => {
setSearchParams(prev => {
const params = new URLSearchParams(prev);
params.set('color', newColor);
return params;
});
};
return (
<div>
<select value={color} onChange={e => handleColorChange(e.target.value)}>
<option value="all">All Colors</option>
<option value="silver">Silver</option>
<option value="black">Black</option>
</select>
{/* Your filtered products render here */}
</div>
);
}
URL 状态管理的最佳实践
1. 处理默认值
不要把默认值写进 URL。
2. 合理使用 pushState / replaceState
- 有“导航意义”的操作 →
pushState - 频繁小更新(搜索联想)→
replaceState
3. 控制 URL 更新频率(Debounce)
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);
// Use replaceState instead of pushState to avoid flooding history
4. URL 是契约(Contract)
URL 是应用与用户、开发者、机器之间的协议。
抗模式(Anti-patterns)
1. SPA 状态只存在于内存
刷新就丢失,一定是反模式。
2. URL 中放敏感信息
永远不要。
3. 不一致的参数命名
?foo=true&bar=2&x=dark
vs
?mobile=true&page=2&theme=dark
4. 过度复杂的巨大参数(如巨型 Base64)
这是 URL 在向你呼救。
5. 破坏返回按钮
滥用 replaceState。
最后的想法
PrismJS 的那个 URL 再次提醒我:设计良好的 URL 不仅仅指向一个页面,它描述了用户与应用之间的对话。它捕捉意图、保存上下文,并以任何其他状态管理方案都无法比拟的方式实现可分享性。
我们构建了大量复杂的状态管理库 —— Redux、MobX、Zustand、Recoil……它们都各有用武之地,但有时最好的解决方案,正是一直以来就存在的那个。
如果你的应用在刷新后就忘记自己的状态,那么你就错过了 Web 上最古老、最优雅的特性之一。
