C08 History 与前端路由

2026-10-31 17:020 阅读约 14 分钟JavaScript

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 验证与讨论

  1. 点击「用户 7」,确认地址变为 /users/7 且页面未刷新,标题变为「用户 7」。
  2. 点击浏览器后退,确认回到首页;前进,确认回到用户页。
  3. 直接访问 /users/7 后刷新,确认服务端回退生效、页面正常渲染,否则会出现 404。
  4. 在控制台执行 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 结论汇总

参考文献

[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

0/2000

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