写单页应用总遇到这些糟心事:
- 传统多页面跳转整页刷新、白屏、加载慢,移动端体验极差
- 只会用Vue/React Router库,底层原理一问三不知,出bug无从排查
- 分不清锚点#和前端Hash路由区别,不知道hashchange事件怎么用

读完本文你能收获:
全文短句分段,手机阅读无压力,所有代码复制打开浏览器就能跑。
一、先搞懂:传统多页面MPA为什么又慢又卡
1. 传统页面跳转流程
以首页index.html跳转到about.html为例:
2. 致命缺点
- 网速一般时,跳转必白屏,交互割裂
- 页面公共头部、底部重复加载,资源浪费
- 无法局部刷新,所有内容全部重绘
- 移动端流量消耗更高,流畅度差
配套演示代码(多页跳转demo): index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>MPA多页首页</title>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="index.html">首页</a></li>
<li><a href="about.html">关于我们</a></li>
</ul>
</nav>
</header>
<main>
<h1>首页内容</h1>
</main>
<footer></footer>
</body>
</html>
about.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>关于我们</title>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="index.html">首页</a></li>
<li><a href="about.html">关于我们</a></li>
</ul>
</nav>
</header>
<main>
<h1>我们</h1>
</main>
</body>
</html>
测试现象
两个文件放在同一文件夹,点击导航切换,页面会完全刷新,滚动条重置,页面闪烁白屏。
二、SPA单页应用解决方案:Hash路由核心原理
1. URL结构拆解
完整URL示例:http://www.baidu.com/u/123?a=1&b=2#/page1
- protocol:http/https 传输协议
- host:域名/IP地址
- path:/u/123 资源路径
- queryString:?a=1&b=2 查询参数
- hash:#/page1 哈希片段(#开头)
2. Hash独一无二的特性
3. 原生锚点# 和 前端Hash路由的区别
基础锚点demo(仅页面滚动,无法切换页面内容)
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>锚点基础Demo</title>
</head>
<body>
<!– 页面锚点 –>
<a name="top"></a>
<a href="#bottom">去到底部</a>
<div style="height: 200vh;background-color:yellow"></div>
<a href="#top">回到顶部</a>
<div style="height: 300vh;background-color:red"></div>
<a name="bottom"></a>
<script>
window.addEventListener("hashchange",function(e){
console.log("hash变了");
console.log("新地址:", e.newURL);
console.log("旧地址:", e.oldURL);
})
</script>
</body>
</html>
- 原生锚点:仅控制页面滚动定位,不能切换页面内容
- SPA Hash路由:约定 #/xxx 格式,监听hash变化,动态替换容器DOM,实现多页面切换
4. Hash路由完整运行流程
三、实战:手写完整可运行Hash路由类(无任何框架)
完整源码,直接复制保存html打开即可运行
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>手写HashRouter SPA</title>
<style>
nav ul { display: flex; gap: 20px; list-style: none; padding: 0; }
#container { margin-top: 30px; font-size: 20px; }
</style>
</head>
<body>
<header>
<nav>
<ul>
<li><a href="#/page1">页面一</a></li>
<li><a href="#/page2">页面二</a></li>
<li><a href="#/page3">页面三</a></li>
</ul>
</nav>
</header>
<!– 路由内容挂载容器 –>
<div id="container"></div>
<script>
class HashRouter {
constructor() {
// 存储所有路由映射:key=路由路径 value=渲染回调
this.routers = {};
// 监听hash变化,bind修正this指向为路由实例
window.addEventListener('hashchange', this.load.bind(this));
// 页面首次加载时执行一次渲染(刷新页面不空白)
window.addEventListener('DOMContentLoaded', this.load.bind(this));
}
// 注册路由:传入hash路径 + 页面渲染回调
register(hashPath, callback) {
this.routers[hashPath] = callback;
}
// 根据当前hash匹配并渲染页面
load() {
// location.hash格式 "#/page1",slice(1)切掉#,得到 /page1
const currentHash = location.hash.slice(1);
// 获取对应渲染函数
const renderFn = this.routers[currentHash];
// 存在路由则执行渲染,无匹配显示404
if(renderFn) {
renderFn.call(this);
} else {
document.getElementById('container').innerHTML = '<h2>404 页面不存在</h2>';
}
}
}
// 初始化路由实例
const router = new HashRouter();
const container = document.getElementById('container');
// 注册三条路由规则
router.register('/page1', () => container.innerHTML='<h2>页面一内容</h2><p>这里是首页展示信息</p>');
router.register('/page2', () => container.innerHTML='<h2>页面二内容</h2><p>关于我们介绍页面</p>');
router.register('/page3', () => container.innerHTML='<h2>页面三内容</h2><p>联系我们表单页面</p>');
</script>
</body>
</html>
核心代码拆解
- 创建路由注册表 this.routers 存储路径与渲染函数
- 绑定 hashchange 监听,切换页面自动执行渲染
- 增加 DOMContentLoaded 监听,解决刷新页面空白问题
register注册方法 外部调用注册路由,把路径和渲染回调存入对象,解耦路由与页面逻辑
load渲染核心方法
- location.hash.slice(1) 去除#符号,拿到纯净路由路径
- 匹配注册表内函数,无匹配返回404页面
- call(this) 保证回调内this指向路由实例,方便扩展
四、Hash路由开发高频踩坑清单
坑1:页面刷新后内容空白
原因:仅监听hashchange,页面首次加载不会触发事件 解决:构造函数增加 DOMContentLoaded 监听,页面载入自动执行一次渲染
坑2:this指向丢失,读取不到routers注册表
原因:hashchange事件回调默认this指向window 解决:使用 .bind(this) 绑定路由实例上下文
坑3:原生锚点和路由#冲突
原因:页面内存在 #top、#bottom 锚点,会触发路由渲染逻辑 解决:统一路由前缀 #/,判断路径是否以/开头区分页面锚点与路由
坑4:空hash(直接访问域名)无默认首页
优化扩展:load方法增加兜底逻辑,currentHash为空时默认渲染/page1首页
load() {
const currentHash = location.hash.slice(1) || '/page1';
const renderFn = this.routers[currentHash];
// …后续逻辑不变
}
坑5:URL带查询参数解析异常
场景:#/page1?id=100 解决方案:使用 split('?')[0] 截取纯路由路径
const currentHash = location.hash.slice(1).split('?')[0];
五、Hash路由优缺点 & 适用场景
✅ 优势
❌ 劣势
适合使用Hash路由的场景
- 内部后台管理系统、中台项目(无SEO需求)
- 静态托管页面、线上预览Demo、本地离线项目
- 老旧浏览器兼容、无权限修改后端服务配置
不推荐场景
- C端官网、需要搜索引擎收录的页面(优先History模式)



