SPA白屏卡顿痛点|1套手写Hash路由|彻底掌握前端路由底层原理
写单页应用总遇到这些糟心事:
- 传统多页面跳转整页刷新、白屏、加载慢,移动端体验极差
- 只会用Vue/React Router库,底层原理一问三不知,出bug无从排查
- 分不清锚点#和前端Hash路由区别,不知道hashchange事件怎么用
读完本文你能收获:
- 搞懂MPA多页应用与SPA单页应用核心差异
- 吃透Hash路由底层运行机制、浏览器URL规则
- 两段完整可直接运行的实战代码(锚点demo+手写路由类)
- 开发踩坑避坑清单、Hash路由优缺点与适用场景
- 从零手写简易路由库,不用框架也能实现页面无刷新切换
全文短句分段,手机阅读无压力,所有代码复制打开浏览器就能跑。
一、先搞懂:传统多页面MPA为什么又慢又卡
1. 传统页面跳转流程
以首页index.html跳转到about.html为例:
- 点击a标签链接,浏览器读取完整URL
- 向服务端发起全新HTTP请求
- 服务器返回对应html静态文件
- 浏览器清空当前页面,完整重新渲染DOM、样式、脚本
- 浏览器历史记录新增一条
2. 致命缺点
- 网速一般时,跳转必白屏,交互割裂
- 页面公共头部、底部重复加载,资源浪费
- 无法局部刷新,所有内容全部重绘
- 移动端流量消耗更高,流畅度差
配套演示代码(多页跳转demo):
index.html
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>MPA多页首页</title></head><body><header><nav><ul><li><ahref="index.html">首页</a></li><li><ahref="about.html">关于我们</a></li></ul></nav></header><main><h1>首页内容</h1></main><footer></footer></body></html>about.html
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>关于我们</title></head><body><header><nav><ul><li><ahref="index.html">首页</a></li><li><ahref="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独一无二的特性
- 修改hash部分,浏览器不会向服务器发送任何请求
- hash变更会自动新增浏览器历史记录,前进后退按钮可用
- 触发原生
hashchange事件,JS可监听URL变化 - hash仅作用于浏览器本地,服务端接收请求时会自动忽略#及后面内容
3. 原生锚点# 和 前端Hash路由的区别
基础锚点demo(仅页面滚动,无法切换页面内容)
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="viewport"content="width=device-width, initial-scale=1.0"><title>锚点基础Demo</title></head><body><!-- 页面锚点 --><aname="top"></a><ahref="#bottom">去到底部</a><divstyle="height:200vh;background-color:yellow"></div><ahref="#top">回到顶部</a><divstyle="height:300vh;background-color:red"></div><aname="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路由完整运行流程
- 导航a标签href写
#/page1,点击后URL更新,页面不刷新 - 浏览器触发
hashchange事件 - JS捕获当前hash值,切割#获取路由标识
- 匹配提前注册好的路由回调函数
- 替换#container容器内HTML,实现局部渲染
- 前进/后退浏览器自动修改hash,重复执行渲染逻辑
三、实战:手写完整可运行Hash路由类(无任何框架)
完整源码,直接复制保存html打开即可运行
<!DOCTYPEhtml><htmllang="en"><head><metacharset="UTF-8"><metaname="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><ahref="#/page1">页面一</a></li><li><ahref="#/page2">页面二</a></li><li><ahref="#/page3">页面三</a></li></ul></nav></header><!-- 路由内容挂载容器 --><divid="container"></div><script>classHashRouter{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)切掉#,得到 /page1constcurrentHash=location.hash.slice(1);// 获取对应渲染函数constrenderFn=this.routers[currentHash];// 存在路由则执行渲染,无匹配显示404if(renderFn){renderFn.call(this);}else{document.getElementById('container').innerHTML='<h2>404 页面不存在</h2>';}}}// 初始化路由实例constrouter=newHashRouter();constcontainer=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>核心代码拆解
- constructor构造函数
- 创建路由注册表
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(){constcurrentHash=location.hash.slice(1)||'/page1';constrenderFn=this.routers[currentHash];// ...后续逻辑不变}坑5:URL带查询参数解析异常
场景:#/page1?id=100
解决方案:使用split('?')[0]截取纯路由路径
constcurrentHash=location.hash.slice(1).split('?')[0];五、Hash路由优缺点 & 适用场景
✅ 优势
- 零服务端配置,静态服务器、GitHub Pages、本地打开html均可运行
- 兼容性极强,最低兼容IE8,老项目、后台管理系统首选
- hash变更不会发送请求,不会出现404刷新报错
- 浏览器前进、后退、历史记录原生支持
❌ 劣势
- URL带有#符号,视觉上不够简洁美观
- 不利于SEO优化,搜索引擎抓取会忽略#后路径
- 无法使用锚点定位页面,需要自己做兼容处理
适合使用Hash路由的场景
- 内部后台管理系统、中台项目(无SEO需求)
- 静态托管页面、线上预览Demo、本地离线项目
- 老旧浏览器兼容、无权限修改后端服务配置
不推荐场景
- C端官网、需要搜索引擎收录的页面(优先History模式)
六、全文核心总结
- MPA多页应用每次跳转全页面刷新,白屏卡顿,SPA用前端路由解决该痛点
- Hash路由依靠URL#哈希片段 + hashchange事件实现无刷新页面切换
- #后的内容不会传递给服务端,无需后端配置,部署成本极低
- 手写路由核心三步:监听hash变化、注册路由映射、动态替换容器DOM
- 后台、静态Demo优先Hash;面向公网、需SEO选择History路由