前端路由的秘密:手写一个迷你路由,看懂Hash和History的较量

作者:良山有风来日期:2025/10/9

你是不是也遇到过这样的场景?开发单页面应用时,页面跳转后刷新一下就404,或者URL里带着难看的#号,被产品经理吐槽不够优雅?

别担心,今天我就带你彻底搞懂前端路由的两种模式,手把手教你实现一个迷你路由,并告诉你什么场景该用哪种方案。

读完本文,你能获得一套完整的前端路由知识体系,从原理到实战,再到生产环境配置,一次性全搞定!

为什么需要前端路由?

想象一下,你正在开发一个后台管理系统。传统做法是每个页面都对应一个HTML文件,切换页面就要重新加载,体验特别差。

而前端路由让你可以在一个页面内实现不同视图的切换,URL变化了但页面不刷新,用户体验流畅得像原生APP一样。

手写迷你路由:50行代码看懂原理

我们先来实现一个最简单的路由,这样你就能彻底明白路由是怎么工作的了。

1// 定义我们的迷你路由类
2class MiniRouter {
3  constructor() {
4    // 保存路由配置
5    this.routes = {};
6    // 当前URL的hash
7    this.currentUrl = '';
8    
9    // 监听hashchange事件
10    window.addEventListener('hashchange', this.refresh.bind(this));
11  }
12  
13  // 添加路由配置
14  route(path, callback) {
15    this.routes[path] = callback || function() {};
16  }
17  
18  // 路由刷新
19  refresh() {
20    // 获取当前hash,去掉#号
21    this.currentUrl = location.hash.slice(1) || '/';
22    
23    // 执行对应的回调函数
24    if (this.routes[this.currentUrl]) {
25      this.routes[this.currentUrl]();
26    }
27  }
28  
29  // 初始化
30  init() {
31    window.addEventListener('load', this.refresh.bind(this), false);
32  }
33}
34
35// 使用示例
36const router = new MiniRouter();
37
38router.init();
39
40// 配置路由
41router.route('/', function() {
42  document.body.innerHTML = '这是首页';
43});
44
45router.route('/about', function() {
46  document.body.innerHTML = '这是关于页面';
47});
48
49router.route('/contact', function() {
50  document.body.innerHTML = '这是联系我们页面';
51});
52

这段代码虽然简单,但包含了路由的核心逻辑:监听URL变化,然后执行对应的函数来更新页面内容。

现在你可以在浏览器里试试,访问http://your-domain.com/#/about就能看到效果了!

Hash模式:简单粗暴的解决方案

Hash模式是利用URL中#号后面的部分来实现的。比如http://example.com/#/user#/user就是hash部分。

Hash模式的实现原理

1// 监听hash变化
2window.addEventListener('hashchange', function() {
3  const hash = location.hash.slice(1); // 去掉#号
4  console.log('当前hash:', hash);
5  
6  // 根据不同的hash显示不同内容
7  switch(hash) {
8    case '/home':
9      showHomePage();
10      break;
11    case '/about':
12      showAboutPage();
13      break;
14    default:
15      showNotFound();
16  }
17});
18
19// 手动改变hash
20function navigateTo(path) {
21  location.hash = path;
22}
23
24// 使用示例
25navigateTo('/about'); // URL变成 http://example.com/#/about
26

Hash模式的优点

兼容性极好,能支持到IE8。不需要服务器端任何配置,因为#号后面的内容不会发给服务器。

部署简单,直接扔到静态服务器就能用。

Hash模式的缺点

URL中带着#号,看起来不够优雅。SEO支持不好,搜索引擎对#后面的内容理解有限。

History模式:优雅的专业选择

History模式利用了HTML5的History API,让URL看起来和正常页面一样,比如http://example.com/user

History API的核心方法

1// 跳转到新URL,但不刷新页面
2history.pushState({}, '', '/user');
3
4// 替换当前URL
5history.replaceState({}, '', '/settings');
6
7// 监听前进后退
8window.addEventListener('popstate', function() {
9  // 这里处理路由变化
10  handleRouteChange(location.pathname);
11});
12

手写History路由

1class HistoryRouter {
2  constructor() {
3    this.routes = {};
4    
5    // 监听popstate事件(浏览器前进后退)
6    window.addEventListener('popstate', (e) => {
7      const path = location.pathname;
8      this.routes[path] && this.routes[path]();
9    });
10  }
11  
12  // 添加路由
13  route(path, callback) {
14    this.routes[path] = callback || function() {};
15  }
16  
17  // 跳转
18  push(path) {
19    history.pushState({}, '', path);
20    this.routes[path] && this.routes[path]();
21  }
22  
23  // 初始化
24  init() {
25    // 页面加载时执行当前路由
26    const path = location.pathname;
27    this.routes[path] && this.routes[path]();
28  }
29}
30
31// 使用示例
32const router = new HistoryRouter();
33
34router.route('/', function() {
35  document.body.innerHTML = 'History模式首页';
36});
37
38router.route('/about', function() {
39  document.body.innerHTML = 'History模式关于页面';
40});
41
42// 初始化
43router.init();
44
45// 编程式导航
46document.getElementById('about-btn').addEventListener('click', () => {
47  router.push('/about');
48});
49

History模式的优点

URL美观,没有#号。SEO友好,搜索引擎能正常抓取。

state对象可以保存页面状态。

History模式的缺点

需要服务器端配合,否则刷新会404。兼容性稍差,IE10+支持。

两种模式的深度对比

在实际项目中,我们该怎么选择呢?来看几个关键维度的对比:

兼容性方面Hash模式几乎支持所有浏览器,包括老旧的IE。History模式需要IE10+,对于需要支持老浏览器的项目,Hash是更安全的选择。

SEO优化如果你的网站需要搜索引擎收录,History模式是更好的选择。虽然现代搜索引擎已经能解析JavaScript,但History模式的URL结构更受搜索引擎欢迎。

开发体验History模式的URL更简洁,在分享链接时用户体验更好。但开发阶段需要配置服务器,稍微麻烦一些。

部署复杂度Hash模式部署简单,直接上传到任何静态托管服务就行。History模式需要服务器配置,下面我们会详细讲。

服务端配置:解决History模式的404问题

History模式最大的坑就是:如果你直接访问http://example.com/user或者刷新页面,服务器会返回404,因为这个路径在服务器上并不存在。

解决方案是让服务器对所有路径都返回同一个HTML文件:

Nginx配置

1server {
2  listen 80;
3  server_name example.com;
4  root /path/to/your/app;
5  
6  location / {
7    try_files $uri $uri/ /index.html;
8  }
9}
10

这个配置的意思是:先尝试找对应的文件,如果找不到就返回index.html。

Node.js Express配置

1const express = require('express');
2const path = require('path');
3const app = express();
4
5// 静态文件服务
6app.use(express.static(path.join(__dirname, 'dist')));
7
8// 所有路由都返回index.html
9app.get('*', (req, res) => {
10  res.sendFile(path.join(__dirname, 'dist', 'index.html'));
11});
12
13app.listen(3000);
14

Apache配置

1<IfModule mod_rewrite.c>
2  RewriteEngine On
3  RewriteBase /
4  RewriteRule ^index\.html$ - [L]
5  RewriteCond %{REQUEST_FILENAME} !-f
6  RewriteCond %{REQUEST_FILENAME} !-d
7  RewriteRule . /index.html [L]
8</IfModule>
9

实战:完整的前端路由库

现在我们把两种模式整合起来,实现一个完整的前端路由:

1class AdvancedRouter {
2  constructor(mode = 'hash') {
3    this.mode = mode;
4    this.routes = {};
5    this.current = '';
6    
7    this.init();
8  }
9  
10  init() {
11    if (this.mode === 'hash') {
12      // Hash模式监听
13      window.addEventListener('hashchange', () => {
14        this.handleChange(location.hash.slice(1));
15      });
16      // 初始化
17      this.handleChange(location.hash.slice(1) || '/');
18    } else {
19      // History模式监听
20      window.addEventListener('popstate', () => {
21        this.handleChange(location.pathname);
22      });
23      // 初始化
24      this.handleChange(location.pathname);
25    }
26  }
27  
28  handleChange(path) {
29    this.current = path;
30    const callback = this.routes[path] || this.routes['*'];
31    
32    if (callback) {
33      callback();
34    }
35  }
36  
37  // 添加路由
38  on(path, callback) {
39    this.routes[path] = callback;
40  }
41  
42  // 跳转
43  go(path) {
44    if (this.mode === 'hash') {
45      location.hash = path;
46    } else {
47      history.pushState({}, '', path);
48      this.handleChange(path);
49    }
50  }
51  
52  // 替换
53  replace(path) {
54    if (this.mode === 'hash') {
55      location.replace(location.pathname + '#' + path);
56    } else {
57      history.replaceState({}, '', path);
58      this.handleChange(path);
59    }
60  }
61}
62
63// 使用示例
64const router = new AdvancedRouter('history');
65
66router.on('/', () => {
67  showPage('home');
68});
69
70router.on('/about', () => {
71  showPage('about');
72});
73
74router.on('*', () => {
75  showPage('not-found');
76});
77
78function showPage(pageName) {
79  document.body.innerHTML = `当前页面:${pageName}`;
80}
81

性能优化技巧

路由用得不好会影响性能,这里分享几个实用技巧:

路由懒加载

1// 动态导入,只有访问时才加载
2router.on('/heavy-page', async () => {
3  const { HeavyComponent } = await import('./HeavyComponent.js');
4  render(HeavyComponent);
5});
6

路由缓存

1const cache = new Map();
2
3router.on('/expensive-page', () => {
4  if (cache.has('/expensive-page')) {
5    // 使用缓存
6    showContent(cache.get('/expensive-page'));
7    return;
8  }
9  
10  // 首次加载
11  fetchData().then(data => {
12    cache.set('/expensive-page', data);
13    showContent(data);
14  });
15});
16

常见坑点及解决方案

1. 路由循环跳转

1// 错误示例:会导致无限循环
2router.on('/login', () => {
3  if (!isLoggedIn) {
4    router.go('/login'); // 循环了!
5  }
6});
7
8// 正确做法
9router.on('/login', () => {
10  if (!isLoggedIn) {
11    return; // 停留在登录页
12  }
13  router.go('/dashboard');
14});
15

2. 路由权限控制

1// 路由守卫
2router.beforeEach = (to, from, next) => {
3  if (to.path === '/admin' && !isAdmin()) {
4    next('/login');
5  } else {
6    next();
7  }
8};
9

该用Hash还是History?

看到这里,你可能还是有点纠结。我给你一个简单的决策流程:

如果你的项目是内部系统,不需要SEO,或者要支持IE8/9,选Hash模式。

如果是面向公众的网站,需要SEO,且能控制服务器配置,选History模式。

如果是微前端架构的子应用,建议用Hash模式,避免与主应用冲突。

写在最后

前端路由看似简单,但里面有很多细节值得深究。无论是Hash的兼容性优势,还是History的优雅体验,都有各自的适用场景。

现在主流框架的路由库(Vue Router、React Router)都同时支持两种模式,原理和我们今天手写的迷你路由大同小异。

理解了底层原理,你再使用这些高级路由库时,就能更得心应手,遇到问题也知道如何调试和解决。

你在项目中用的是Hash模式还是History模式?遇到过什么有趣的问题吗?欢迎在评论区分享你的经验!


前端路由的秘密:手写一个迷你路由,看懂Hash和History的较量》 是转载文章,点击查看原文


相关推荐


Kubernetes核心技术与集群部署项目
企鹅侠客2025/10/7

从集群搭建到核心功能应用的完整流程,内容涵盖集群部署、核心组件、资源管理、安全机制、持久化、监控与应用交付等关键技术。首先介绍 Kubernetes 的架构与特性,深入讲解 kubeadm 与二进制两种集群搭建方式,包括 etcd 集群部署、Master 与 Node 组件安装、证书签发及高可用集群实现。核心技术部分系统解析 Pod 的运行机制、调度策略、健康检查与资源限制,讲解 Deployment、StatefulSet、DaemonSet、Job 等控制器的应用场景。配置管理方面介绍 Co


Nginx 配置负载均衡(详细版)
1加1等于2025/10/6

本文详细介绍关于Nginx 配置负载均衡,包括配置文件结构、多种负载均衡策略、如何修改均衡策略以及其他一些重要的配置。 本文目录 一、、Nginx 配置负载均衡1. 配置文件结构 二、Nginx 负载均衡策略1. 轮询(`默认策略`)2. 加权轮询3. IP 哈希4. 最少连接 三、修改负载均衡策略四、Nginx 负载均衡其他配置1. 健康检查2. 会话保持3. 超时设置 一、、Nginx 配置负载均衡 1. 配置文件结构 Nginx 的负载均衡配置主要


UNIX下C语言编程与实践16-UNIX 磁盘空间划分:引导块、超级块、i 节点区、数据区的功能解析
迎風吹頭髮2025/10/4

一、UNIX 磁盘空间划分的核心逻辑:为何分为四个区域? UNIX 文件系统在格式化时,会将磁盘分区(如 /dev/sda1)划分为引导块(Boot Block)、超级块(Super Block)、i 节点区(Inode Area)、数据区(Data Area)四个连续的区域。这种划分并非随意设计,而是为了实现“系统启动-文件系统管理-数据存储”的完整功能链路,确保磁盘空间的有序管理和高效访问。 核心定位:四个区域各司其职且相互依赖——引导块负责“启动系统”,超级块负责“管理文件系统全局信息


DeepSeek V3.1-Terminus、阿里 Qwen3-Max、ChatGPT Pulse 同周登场!| AI Weekly 9.22-9.28
AI信息Gap2025/10/3

卷,卷起来了! 📢 本周 AI 快讯 | 1 分钟速览🚀 1️⃣ 🚀 DeepSeek 发布 V3.1-Terminus :Agent 性能提升 28%,HLE 测试跃升全球第三,仅次于 Grok 4 和 GPT-5,SimpleQA 准确率达 96.8%。 2️⃣ 💰 阿里云栖大会七连发 :3800 亿 AI 基建投资起步,万亿参数 Qwen3-Max 对标 GPT-5,AIME 25 和 HMMT 数学测试满分 100 分。 3️⃣ 🖥️ Kimi 推出 OK Compu


[linux仓库]深入解析Linux动态链接与动态库加载:理解背后的原理与技巧
egoist20232025/10/2

🌟 各位看官好,我是egoist2023! 🌍 Linux == Linux is not Unix ! 🚀 今天来学习Linux的指令知识,并学会灵活使用这些指令。 👍 如果觉得这篇文章有帮助,欢迎您一键三连,分享更多人哦! 目录 进程如何看到动态库 进程间如何共享库 动态链接 编译器对可执行程序动手脚 动态库相对地址  程序如何和库具体映射 程序怎么进行库调用 全局偏移量表GOT 库间依赖(看看即可) 总结 进程如何看到动态库


Excel转PDF不分页
Bella_chene2025/10/2

将Excel转成PDF后,会发现存在分页的现象,理想是希望将一整个表格按实际情况缩放显示到PDF的一页上去 操作办法:打开excel表格,ctrl+P打开打印页面,点击页面设置,选择“将工作表调整为一页” 然后在右侧就可以看到效果,点击保存就可以了


ptyhon 基础语法学习(对比php)
come1123410/2/2025

非常棒!我们已经系统地对比了 Python 和 PHP 的所有主要数据类型。分类Python 类型对应 PHP 概念关键点基础intfloatstrintfloatstringPython 字符串格式化用f-string序列listtuple索引数组list可变(用[]),tuple不可变(用()映射dict关联数组键值对集合 (用{}集合set的结果元素唯一且无序特殊boolNoneboolnullFalse和None(首字母大写)这份表格可以作为您未来的速查手册。


音视频编解码全流程之用Extractor后Decodec
Everbrilliant8910/1/2025

本文介绍了音视频编解码流程中从媒体文件提取数据包后进行解码的两种实现方式:FFmpeg和MediaCodec。在FFmpeg部分,详细说明了交叉编译、数据包提取、解码器查找和分配、数据包解码等步骤,并提供了完整的代码实现。在MediaCodec部分,重点讲解了Extractor初始化、轨道选择、解码器初始化以及解码过程,同样包含详细代码示例。文章还对比了两种解码流程的异同,并展示了实际效果。所有代码均可在作者GitHub项目中查看。该系列文章为音视频开发提供了完整的流程参考,适合开发者学习音视频编解码技术。


【自动驾驶】自动驾驶概述 ⑤ ( 自动驾驶硬件概述 | 车载计算单元 IPC | 车辆线控系统 )
韩曙亮9/30/2025

一、车载计算单元 IPC1、车载计算单元 IPC 简介2、高性能计算3、高安全冗余4、高环境适应性二、车辆线控系统1、自动驾驶线控系统2、线控转向 ( Steer-by-Wire )3、线控制动 ( Brake-by-Wire )4、线控驱动 ( Throttle-by-Wire )5、线控换挡 ( Shift-by-Wire )6、线控悬挂 ( Suspension-by-Wire )7、线控系统举例说明


汽车软件开发的质量和安全管理流程
NewCarRen2025/10/10

摘要 软件开发流程是智能车辆(联网车辆和自动驾驶车辆)的核心,必须精心管理。自动化与联网功能的开发分别通过功能安全和网络安全开发流程实现,且需遵循相关标准,这些标准规定了流程、最佳实践、危害、威胁及管理策略。通过改进软件开发流程,智能车辆的人体工程学性能将得到提升。本文阐述了如何通过软件开发来管理实现自动化与联网功能的流程,以及是否可能改变管理团队的策略与软件开发流程。 1、引言 智能车辆是一种能够从周围环境中获取信息,并对信息进行处理,从而实现自主安全行驶且不造成任何伤害的车辆。此外,智

首页编辑器站点地图

Copyright © 2025 聚合阅读

License: CC BY-SA 4.0