你是不是也遇到过这样的场景?开发单页面应用时,页面跳转后刷新一下就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的较量》 是转载文章,点击查看原文。