你的 URL 就是你的状态

 

在构建 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 上最古老、最优雅的特性之一。