# 页面、路由和状态应该放在哪里

原文: https://yishto.com/learn/web-interface/state-routing-and-frontend-architecture/

作者: [Yishto](https://yishto.com/about/)

按寿命划分 URL、局部和远程状态，再为阅读清单设计刷新路由、功能边界与可恢复的实时更新。

首次发布: 2026-11-10
内容更新: 2026-11-10

阅读清单页把所有信息放进一个全局 store：搜索词、登录身份、弹窗、服务器结果和连接状态互相引用。刷新页面后筛选丢失，后退按钮不恢复列表，旧消息还会覆盖新数据。问题通常在状态的归属和恢复规则，增加一个管理库未必能解决。

案例是一页可分享的阅读清单，支持分类、排序、收藏和后台任务进度。先按寿命与共享方式分类，再设计路由、功能模块与更新协议。离线示例只验证版本拒绝规则，不建立真实实时连接。

## 按状态寿命确定保存位置

搜索词、分类、排序和可分享的页码适合 URL，别人打开链接可以重建同一视图。弹窗开关、输入焦点和未提交输入通常属于局部界面状态。服务器记录由后端负责，前端缓存只是副本；登录身份还需要明确会话与权限边界。

不要把服务器记录复制到多个互不关联的 store，再让各页面独立更新。为每个数据指定权威来源和同步路径。远程查询结果需要缓存键、失效规则、加载与错误状态，局部弹窗无需承担这些职责。

URL 参数来自外部，必须解析和校验。页码限制范围，排序字段只允许已知值，分类不存在时给出明确回退或错误。不能因为参数出现在自己的页面地址中，就信任它能直接拼进数据库或渲染 HTML。

## 路由同时涉及浏览器与服务器

History API 可以增加或替换历史条目；浏览器后退、前进引发的恢复需要处理相应 popstate。应用自己修改历史后，也要按路由机制更新视图，不能假设每次 pushState 都会自动触发同样的事件。

分享链接与刷新要求服务器或静态托管能够返回对应路径。开发服务器显示页面，不代表生产托管已支持这些 URL。路径导航到资源文件时，也不能把所有请求都回退到同一个 HTML，否则缺失图片会返回页面文本。

筛选改变通常需要决定重置页码还是保留定位；后退需要恢复参数与视图。把这些行为写进路由契约，再测试从入口点击、直接打开和刷新三种路径。URL 与状态若互相无限写入，会造成重复导航，应只在必要变化时同步。

## 功能模块通过窄接口协作

阅读清单模块可以拥有查询、列表和收藏操作，任务模块拥有进度与重试，会话模块提供当前身份。模块暴露必要的读取与操作接口，避免页面跨越边界改动另一个模块的内部对象。

一个 store 可以实现这些边界，但不能凭“统一状态”消除所有责任。也可以用局部状态、查询缓存和少量共享上下文组合。按项目变化选择，不为了架构图把一个简单页面拆成大量互相转发的层。

操作状态需要合法转换，例如 queued 到 running，再到 succeeded 或 failed。取消与重试的语义独立：取消可能只停止界面等待，真实任务是否停止由服务端确认。前端按钮不能直接宣布后台工作已经取消。

## 选择轮询、SSE 或 WebSocket

轮询按间隔请求最新状态，实现简单，但频率越高越增加请求负载。需要明确停止条件、页面不可见时的策略以及重试预算。任务结束后应停止无意义轮询，而不是永远重复查询。

SSE 通过 HTTP 提供服务器到客户端的事件流，适合单向进度通知；WebSocket 提供双向消息通道，适合需要双方频繁通信的场景。选择还取决于代理、认证方式、连接生命周期与平台能力，不能只按“实时”二字决定。

无论哪种通道，应用仍要设计重连、重复消息、状态恢复和背压。WebSocket 能收发消息，不等于业务变化恰好执行一次。连接断开后可以获取最新快照，再按版本继续处理，避免用遗漏事件构造一个永久错误状态。

## 用版本检查拒绝旧更新

假设服务器为完整状态快照分配递增版本。只接受大于当前版本的快照，可以防止重复和较旧快照覆盖现值。下面是合成规则，version=2 在 version=1 前到达，随后旧版本和重复版本都被拒绝。

```python
state = {"version": 0, "value": "initial"}
def apply(version, value):
    if version <= state["version"]:
        return False
    state.update(version=version, value=value)
    return True
assert apply(2, "new")
assert not apply(1, "old")
assert not apply(2, "duplicate")
assert state["value"] == "new"
print(state)
```

输出为 `{'version': 2, 'value': 'new'}`。代码允许从 0 跳到 2，因为输入被当作完整快照。若消息表示“给计数加一”这样的增量，跳过版本 1 就可能丢失变化，需要检测缺口并重新同步，不能直接复用这个策略。

版本只在约定范围内比较。不同资料、租户或连接时代的编号不能随意混用。重连后要知道版本来源是否保持连续，也要处理服务器重置。该函数没有线程、网络和持久化，不能证明真实实时通道可靠。

## 构建工具不包办应用架构

Vite 提供开发服务和构建能力，但不会自动定义权限、生产路由或服务器数据一致性。构建成功需要与托管路径、资源前缀和缓存策略对应，才能在实际部署中正确打开页面。

排查刷新失败时先看托管路由与响应类型；排查列表退回旧值时先看 URL 还原、缓存键和更新版本。把所有问题归为框架故障会错过真正边界。每项修复都需要验证直接访问、后退、重连和错误路径。

收藏操作还需要处理服务器拒绝。若先更新界面再发送请求，失败时应恢复或标记待重试，且不能让旧失败覆盖新的成功操作。对多页面共享数据，明确哪次结果使缓存失效，并检查列表和详情是否看到同一版本。局部按钮状态不能成为永久的收藏事实来源。

## 练习：实现可恢复的清单视图

定义 URL 参数、局部状态、远程缓存和会话信息的归属。为合成任务选择一种更新方式，写出结束、断线、重连、重复与缺口的处理。分别描述完整快照和增量消息的不同规则。

验收要求：分享与刷新保留分类和排序；后退恢复上一视图；未知参数受控处理；旧消息不覆盖新快照；增量缺口触发重新同步；任务终态停止轮询或解除无用订阅。记录真实路由与连接检查，离线版本断言只作为其中一项机制验证。

## 资料来源与延伸阅读

- [Managing React state](https://react.dev/learn/managing-state)
- [History API](https://developer.mozilla.org/en-US/docs/Web/API/History_API)
- [Vite project guide](https://vite.dev/guide/)

资料核验：2026-11-10。教学示例不代表线上实测。

## 相关文章

- [Flutter 应用怎样管理页面和异步状态](https://yishto.com/learn/application-platforms/flutter-reading-library/)
- [用 React Native 与 Expo 设计离线笔记](https://yishto.com/learn/application-platforms/expo-offline-notebook/)
