C08 History 与前端路由
C08 History 与前端路由
摘要:前端路由把地址与视图状态绑定:
history.pushState在不刷新页面的前提下改写地址并压入历史记录,popstate在前进后退时回调,URLSearchParams负责查询串读写。一个可用路由需要四部分:路由表、路径解析、渲染函数与未匹配兜底。本文给出一个无依赖的实现,并说明部署时刷新 404 的成因与服务端回退配置。
关键词:History API,pushState,popstate,URLSearchParams,路由表
所属:frontend/js / C-高级 | 上一篇:C07-观察者与WebAPI | 下一篇:D00-总结 | 返回:C00-总结
1 引言
多视图页面常见两种状态丢失:刷新后回到首页、前进后退不响应用户预期。两者都源于视图状态只存在于内存,没有与 URL 建立映射。
本文给出把视图状态写进 URL 的完整做法。范围见表 1-1。
| 项 | 本文覆盖 | 本文不覆盖 |
|---|---|---|
| 地址 | pushState、replaceState、state |
服务端路由与渲染(SSR) |
| 导航 | popstate、链接拦截、滚动恢复 |
路由懒加载与代码分割(C05) |
| 查询串 | URLSearchParams 读写与序列化 |
表单编码细节 |
| 部署 | history 回退配置、404 兜底 | 静态托管平台差异 |
表 1-1 本文覆盖范围
2 预备知识
| 项 | 要求 | 核对方式 |
|---|---|---|
| 环境 | Chrome 130+,本地服务器(需支持回退) | 刷新子路径不 404 |
| 前置 | B05、C04 | 模块与事件已掌握 |
| 文件 | router/index.html、router/router.mjs |
编码 UTF-8 |
表 2-1 环境与前置要求
3 原理与实现
3.1 一次导航的三个环节
地址变更、历史记录与渲染是三个独立动作,必须显式串联,如图 3-1 所示。
flowchart TD
A["用户点击链接或调用 navigate"] --> B{"是否允许离开当前视图"}
B -->|否| C["保持原地址"]
B -->|是| D["pushState 改写地址并压栈"]
D --> E["执行渲染函数,替换视图容器"]
E --> F["恢复滚动位置并发通知"]
G["用户点击前进/后退"] --> H["popstate 事件"]
H --> E
图 3-1 导航由地址变更与渲染两步组成,回退只经过 popstate
图上可见:
pushState本身不触发任何渲染,也不触发popstate;忘记手动渲染会出现「地址变了页面没变」。
3.2 完整实现
<!-- router/index.html;运行环境:Chrome 130+;服务器需把子路径回退到本文件 -->
<nav>
<a href="/">首页</a>
<a href="/users/7">用户 7</a>
<a href="/search?q=css">搜索 css</a>
</nav>
<main id="view"></main>
<script type="module" src="./router.mjs"></script>
// router/router.mjs;运行环境:Chrome 130+;支持 history 回退的服务器
const view = document.querySelector('#view');
// 1 路由表:路径模式 → 渲染函数
const routes = [
{ pattern: /^\/$/, render: renderHome },
{ pattern: /^\/users\/(\d+)$/, render: renderUser },
{ pattern: /^\/search$/, render: renderSearch },
];
function renderHome() {
view.replaceChildren(el('h1', '首页'));
}
function renderUser(params) {
const id = params[0];
view.replaceChildren(el('h1', `用户 ${id}`), el('p', `编号 ${id} 的详情`));
}
function renderSearch(_params, query) {
const keyword = query.get('q') ?? '';
view.replaceChildren(el('h1', '搜索'), el('p', `关键词:${keyword}`));
}
function el(tag, text) {
const node = document.createElement(tag);
node.textContent = text;
return node;
}
// 2 解析当前地址并执行渲染
function resolve() {
const url = new URL(location.href);
for (const route of routes) {
const matched = route.pattern.exec(url.pathname);
if (!matched) continue;
route.render(matched.slice(1), url.searchParams);
return;
}
view.replaceChildren(el('h1', '未找到页面'), el('p', url.pathname));
}
// 3 编程式导航:改地址后必须手动渲染
function navigate(path, { replace = false } = {}) {
const url = new URL(path, location.origin);
if (url.href === location.href) return;
const method = replace ? 'replaceState' : 'pushState';
history[method]({ path: url.pathname }, '', url);
resolve();
}
// 4 拦截站内链接,避免整页刷新
document.addEventListener('click', (event) => {
const link = event.target.closest('a[href^="/"]');
if (!link || link.target === '_blank' || event.metaKey || event.ctrlKey) return;
event.preventDefault();
navigate(link.getAttribute('href'));
});
// 5 前进与后退:popstate 是唯一入口
window.addEventListener('popstate', resolve);
// 6 查询串读写:用 URLSearchParams,不手工拼字符串
function setQuery(key, value) {
const url = new URL(location.href);
url.searchParams.set(key, value);
navigate(url.pathname + url.search, { replace: true });
}
resolve();
导航相关的 API 对照见表 3-1。
| API | 是否新增历史记录 | 是否触发 popstate |
用途 |
|---|---|---|---|
history.pushState |
是 | 否 | 常规导航 |
history.replaceState |
否 | 否 | 改查询串、改写当前记录 |
history.back()/forward() |
否 | 是 | 脚本触发前进后退 |
location.assign |
是 | 否(整页刷新) | 需要服务端参与时 |
location.hash = |
是 | 触发 hashchange |
无需服务端回退的简易方案 |
表 3-1 导航 API 的行为差异
两种模式的取舍见表 3-2。
| 模式 | 地址形态 | 服务端要求 | 适用场景 |
|---|---|---|---|
| history | /users/7 |
未知路径回退到 index.html |
正式站点,利于分享 |
| hash | /#/users/7 |
无 | 静态托管、无需配置 |
| 查询串参数 | /?page=users&id=7 |
无 | 只需少量状态,不做路由 |
表 3-2 三种地址模式的取舍
4 验证与讨论
- 点击「用户 7」,确认地址变为
/users/7且页面未刷新,标题变为「用户 7」。 - 点击浏览器后退,确认回到首页;前进,确认回到用户页。
- 直接访问
/users/7后刷新,确认服务端回退生效、页面正常渲染,否则会出现 404。 - 在控制台执行
setQuery('q', 'js'),确认地址中的查询串被替换且不新增历史记录。
# 需支持的服务器回退(Nginx 示例)
location / {
try_files $uri $uri/ /index.html;
}
| 检查项 | 预期结果 | 说明 |
|---|---|---|
| 地址同步 | 导航后地址与视图一致 | 图 3-1 |
| 前进后退 | popstate 后重新渲染 |
3.2 节第 5 段 |
| 刷新可用 | 子路径直接访问不 404 | 4 节 Nginx 配置 |
| 查询串 | 用 URLSearchParams 读写 |
3.2 节第 6 段 |
| 未匹配兜底 | 未知路径渲染未找到页面 | 3.2 节 resolve |
表 4-1 五项验收
讨论与局限。 history 模式要求服务端把未知路径回退到入口文件,静态托管平台需单独配置;无法配置时改用 hash 模式。另,pushState 的 state 只存可结构化克隆的数据,且刷新后仍可读,适合放置轻量视图状态,不适合放大量业务数据(见 D03 的缓存策略)。
5 常见问题
| 现象 | 原因 | 处理 |
|---|---|---|
| 地址变了页面没变 | pushState 后未调用渲染 |
改地址后显式调用 resolve() |
| 刷新子路径出现 404 | 服务端未配置回退 | 配 try_files 或改用 hash 模式 |
| 前进后退不更新视图 | 未监听 popstate |
注册监听器并调用渲染 |
| 外链被拦截导致无法跳转 | 链接拦截未排除其它域名 | 只拦截以 / 开头的同源链接 |
| 查询串中文变乱码 | 手工拼接字符串 | 用 URLSearchParams,自动编码 |
| 同一地址重复压栈 | 未比较当前地址 | navigate 中先比对再 pushState |
表 5-1 前端路由的六类问题
6 结论
| 序号 | 结论 | 依据 |
|---|---|---|
| 1 | pushState 不触发渲染,需手动串联 |
图 3-1 |
| 2 | 前进后退统一由 popstate 驱动 |
表 3-1 |
| 3 | 查询串一律用 URLSearchParams 读写 |
表 5-1 |
| 4 | history 模式需要服务端回退,否则用 hash | 表 3-2 |
表 6-1 结论汇总
- 上一篇:C07-观察者与WebAPI
- 下一篇:D00-总结
- 返回:C00-总结 | 科目 README
参考文献
[1] MDN Web Docs · History API,https://developer.mozilla.org/zh-CN/docs/Web/API/History_API(访问于 2026-10-31)
[2] MDN Web Docs · URLSearchParams,https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams(访问于 2026-10-31)

评论 0
还没有评论,快来抢沙发吧~