前端路由策略:Memory、Hash 与 History

前端路由策略:Memory、Hash 与 History

前端路由可以理解为:根据当前路径,匹配并渲染对应的组件或页面。对于单页应用,站内导航通常不重新加载整个 HTML 文档,而是由 JavaScript 更新界面。

Memory、Hash、History 的主要区别在于路径存在哪里、导航记录由谁管理,以及刷新时服务器会收到什么请求。它们可以共用同一套路由匹配和渲染逻辑。

一、路由器的共同结构

一个路由器的基本流程是:

text 复制代码
导航操作 → 更新地址状态 → 匹配路由规则 → 提取参数 → 渲染组件树

例如访问 /users/42,匹配规则 /users/:id,提取 id = "42",再渲染用户详情组件。如果存在嵌套路由,匹配结果可能是 AppLayout → UserLayout → UserDetail,而不只是单个组件。

这里要分清两层职责:地址管理决定如何读写路径,路由匹配决定路径对应什么界面。三种策略主要解决前者。

二、Memory:自己维护导航历史

Memory Router 把路径和历史记录保存在 JavaScript 内存中,不修改浏览器地址栏。核心数据结构是一个数组和一个当前位置索引:

js 复制代码
let entries = ["/"];
let index = 0;

function push(path) {
  entries.splice(index + 1); // 删除当前位置之后的前进记录
  entries.push(path);
  index++;
  notify(entries[index]);
}

function replace(path) {
  entries[index] = path;
  notify(entries[index]);
}

function go(delta) {
  const next = index + delta;
  if (next < 0 || next >= entries.length) return;
  index = next;
  notify(entries[index]);
}

以上是原理示意,notify 表示通知上层重新匹配和渲染。真实实现还会保存查询参数、自定义状态和记录标识。

假设历史是 [/, /users, /users/42],后退到 /users 再跳转到 /settings,历史就变为 [/, /users, /settings]。与浏览器一样,新的导航会截断原来的前进分支。

Memory 适合自动化测试、无浏览器环境,以及需要与宿主 URL 隔离的嵌入式界面。它的前进后退由自己的 API 控制,默认不与浏览器按钮联动。React Router 的 MemoryRouter 就采用这种内存记录方式。

刷新时,浏览器请求的仍是地址栏中的真实页面地址,内存路径不会发送给服务器。因此,Memory 不会因内部路径产生 History 模式那种 404,但如果没有额外持久化和恢复逻辑,刷新后会重新初始化路由,也无法直接通过 URL 分享当前内部页面。

三、Hash:把路径放在 Fragment 中

Hash Router 使用 URL 的 # 后部分保存路由地址:

text 复制代码
https://example.com/app/#/users/42

一种基础实现是读取 location.hash,通过修改 hash 导航,再监听 hashchange 更新界面:

js 复制代码
function readPath() {
  return location.hash.slice(1) || "/";
}

function push(path) {
  location.hash = path;
}

window.addEventListener("hashchange", () => {
  notify(readPath());
});

notify(readPath()); // 首次加载也需要匹配

Fragment 的关键特性是不会随 HTTP 请求发送给服务器。所以上述 URL 刷新时,服务器收到的是 GET /app/,返回应用入口后,前端再读取 /users/42 并渲染页面。这正是 Hash 通常不需要为内部路由配置服务器回退的原因。MDN:Hash routing

Hash 变化通常会形成浏览器历史记录,因此能够使用浏览器前进后退。不过,入口 /app/ 本身必须可以访问;Hash 并不能解决入口不存在的问题。

另一个细节是:在 /#/users?tab=info 中,tab=info 位于 Fragment 内,需要路由器自行解析,不能从 location.search 读取。Hash 还占用了原生页面锚点的位置,锚点跳转需要额外约定。

上述代码展示的是经典实现。现代路由库也可能通过 History API 写入带 hash 的 URL,此时不能只等待 hashchange,还需要主动通知和处理历史遍历。模式由地址形式决定,不等于固定使用某一个事件。

四、History:使用真实 URL 路径

History Router 使用普通路径,例如 /users/42,通过 History API 修改 URL 和浏览器历史记录:

js 复制代码
function readPath() {
  return location.pathname + location.search + location.hash;
}

function push(path, state = null) {
  history.pushState(state, "", path);
  notify(readPath());
}

function replace(path, state = null) {
  history.replaceState(state, "", path);
  notify(readPath());
}

window.addEventListener("popstate", () => {
  notify(readPath());
});

notify(readPath());

pushState 添加记录,replaceState 替换当前记录;二者都不会立即请求目标页面,也不会触发 popstate,所以代码需要主动通知。用户在这些同文档历史记录之间前进、后退时,再通过 popstate 同步界面。此外,pushState 要求目标 URL 与当前页面同源,即使只修改 hash,也不会触发 hashchange。MDN:pushState、popstate

路由库的链接组件通常会拦截普通站内点击,阻止浏览器默认加载文档,再调用导航 API。新标签页、下载和外链等操作则应保留原生行为。

为什么刷新会出现 404?

前端跳转到 /users/42 时,pushState 只更新地址,组件由前端渲染。刷新或直接访问这个地址时,浏览器会真正发出 GET /users/42。

如果服务器只有 index.html 和静态资源,没有 /users/42 对应的文件或服务端路由,就会返回 404。此时前端应用尚未加载,无法接管这个请求。

对于纯 SPA,需要让服务器将应用路径回退到 index.html。例如在应用部署于域名根目录、静态资源位于 /assets/ 的前提下:

nginx 复制代码
location /api/ {
    proxy_pass http://backend;
}

location /assets/ {
    try_files $uri =404;
}

location / {
    try_files $uri $uri/ /index.html;
}

这里的 backend 需替换为实际后端或已配置的 upstream。其他静态资源目录也应按项目单独处理,避免缺失的脚本被返回成 HTML。

回退通常是内部处理,浏览器地址仍保留 /users/42,前端启动后才能恢复对应页面。如果使用 SSR,则应由服务端匹配请求并渲染,而不是简单返回同一份静态入口。

五、策略选择与工程边界

维度 Memory Hash History
路径来源 内存记录 URL Fragment URL 正常路径
浏览器前进后退 默认不联动 支持 支持
刷新后恢复当前路由 需额外实现 从 hash 恢复 从 URL 恢复,需服务端支持
服务端能否看到内部路径 不能 不能 能
典型场景 测试、独立路由容器 无法配置路由回退的静态托管 常规 Web 应用、SSR

能够配置服务器或托管平台的 Web 应用,通常选择 History;无法配置应用路径回退时,Hash 更方便;不需要把导航暴露给浏览器 URL 的环境,可以使用 Memory。

策略本身不决定 SEO 效果:History 提供正常路径,但内容能否被索引还取决于渲染方式等因素。服务器返回 index.html 也不意味着该业务路径真实存在,应用仍需处理未匹配路由;需要正确 HTTP 404 时,还要服务端配合。

无论采用哪种策略,生产实现都应处理部署子路径、参数编码、导航请求竞态和滚动恢复。前端路由守卫只能控制界面访问流程,真正的数据权限必须由后端校验。

相关推荐
李剑一1 小时前
AntDesign老版本没有月份和年份区间选择组件?我封装了一个完美版,附源码!
前端·vue.js
牧艺1 小时前
浏览器端 3D 看车重磅升级:从三款展车到可试驾的八车展厅
前端·three.js·交互设计
老男孩游戏时光1 小时前
Electron 桌面应用开发入门,前端也能写客户端
前端
默_笙1 小时前
🚊 别再让 AI 的记忆写两遍了:PostgreSQL 把关系查询和语义检索装进一张表
前端·javascript
进击的明明1 小时前
TypeScript速通笔记(下)
前端·面试·typescript
lzhdim1 小时前
C#不为人知的10个魔法特性:资深开发者也会震惊的底层奥秘
java·前端·javascript·算法·c#
zach2 小时前
前端落地实战:Next.js 项目 Nginx + PM2 生产部署全流程(附踩坑总结)
前端·pm2·next.js
overmind3 小时前
oeasy教h5前端001 修改的乐趣_直接修改在线网页
前端
只睡四小时3 小时前
JS 手写 D-pad 空间导航:电视端焦点引擎实战
android·开发语言·前端·javascript·ecmascript·hls·空间导航