【前端面试小册】浏览器-第10节:前端路由原理与实战,Hash与History模式
一、现实世界类比 🗺️
想象你在一个大型购物中心:
-
传统多页面应用:就像每个店铺都是独立的建筑
- 从 A 店到 B 店要走出去,重新进入(刷新页面)
- 每次都要重新过安检(重新加载资源)
- 速度慢,体验差
-
单页面应用(SPA):就像所有店铺在同一栋楼里
- 从 A 店到 B 店只需换楼层(切换组件)
- 不用走出大楼(不刷新页面)
- 速度快,体验好
-
前端路由:就像电梯和楼层指示牌
- Hash 模式:像贴在墙上的地图(#/floor1)
- History 模式:像真实的楼层编号(/floor1)
二、为什么需要前端路由?
💡 传统多页面 vs 单页面应用
// ===== 传统多页面应用(MPA)=====
const TraditionalMPA = {
特点: {
页面结构: '每个URL对应一个完整的HTML页面',
跳转方式: '浏览器请求新页面,刷新整个页面',
资源加载: '每次跳转都重新加载HTML、CSS、JS'
},
缺点: [
'页面跳转慢(重新加载资源)',
'白屏时间长',
'用户体验差(不流畅)',
'无法保留页面状态'
],
例子: `
点击链接:/page1 → /page2
1. 浏览器发起HTTP请求到服务器
2. 服务器返回完整的page2.html
3. 浏览器刷新,重新加载所有资源
4. 页面显示(白屏时间长)❌
`
};
// ===== 单页面应用(SPA)=====
const ModernSPA = {
特点: {
页面结构: '只有一个HTML页面,通过JS渲染不同组件',
跳转方式: '改变URL,不刷新页面,只切换组件',
资源加载: '首次加载所有资源,后续无需重新加载'
},
优点: [
'页面切换快(无需重新加载)',
'用户体验好(流畅)',
'可以保留页面状态',
'前后端分离(API驱动)'
],
例子: `
点击链接:/page1 → /page2
1. 前端路由拦截,改变URL
2. 根据新URL匹配对应组件
3. 卸载旧组件,渲染新组件
4. 页面更新(无刷新,快速)✅
`
};
三、前端路由的两种模式
模式1:Hash 模式(#)⭐⭐⭐
// ===== Hash 模式原理 =====
const HashMode = {
URL格式: 'https://example.com/#/page1',
特点: {
标识符: 'URL中的 # 符号',
监听事件: 'hashchange',
兼容性: '所有浏览器都支持(IE8+)',
服务器配置: '无需配置(# 后内容不会发送到服务器)'
},
优点: [
'兼容性好',
'无需服务器配置',
'实现简单'
],
缺点: [
'URL不美观(有#)',
'SEO不友好',
'无法使用锚点定位'
]
};
// ===== 实现 Hash 路由 =====
class HashRouter {
constructor() {
this.routes = {}; // 存储路由配置
this.currentUrl = ''; // 当前路径
// 监听hashchange事件
window.addEventListener('hashchange', this.handleHashChange.bind(this));
// 监听页面加载
window.addEventListener('load', this.handleHashChange.bind(this));
}
// 注册路由
route(path, callback) {
this.routes[path] = callback;
}
// 处理 hash 变化
handleHashChange() {
// 获取当前 hash(去掉 #)
this.currentUrl = location.hash.slice(1) || '/';
console.log('路由变化:', this.currentUrl);
// 执行对应的回调
if (this.routes[this.currentUrl]) {
this.routes[this.currentUrl]();
} else {
// 404
this.routes['/404'] && this.routes['/404']();
}
}
// 编程式导航
push(path) {
location.hash = path;
}
// 替换当前路由(不产生历史记录)
replace(path) {
location.replace(location.origin + location.pathname + '#' + path);
}
// 后退
back() {
history.back();
}
// 前进
forward() {
history.forward();
}
}
// ===== 使用示例 =====
const router = new HashRouter();
// 注册路由
router.route('/', function() {
document.getElementById('app').innerHTML = '<h1>首页</h1>';
});
router.route('/about', function() {
document.getElementById('app').innerHTML = '<h1>关于页面</h1>';
});
router.route('/user/:id', function() {
const id = router.currentUrl.match(/\/user\/(\d+)/)[1];
document.getElementById('app').innerHTML = `<h1>用户${id}</h1>`;
});
router.route('/404', function() {
document.getElementById('app').innerHTML = '<h1>404 Not Found</h1>';
});
// 编程式导航
// router.push('/about');
// router.back();
<!-- ===== HTML 示例 ===== -->
<!DOCTYPE html>
<html>
<head>
<title>Hash Router</title>
</head>
<body>
<nav>
<a href="#/">首页</a>
<a href="#/about">关于</a>
<a href="#/user/123">用户123</a>
</nav>
<div id="app"></div>
<script src="router.js"></script>
</body>
</html>
<!--
URL 变化示例:
1. 点击"首页" → https://example.com/#/
2. 点击"关于" → https://example.com/#/about
3. 点击"用户123" → https://example.com/#/user/123
特点:# 后面的内容不会发送到服务器 ✅
-->
模式2:History 模式(HTML5)⭐⭐⭐⭐⭐
// ===== History 模式原理 =====
const HistoryMode = {
URL格式: 'https://example.com/page1',
特点: {
标识符: '正常的URL路径',
监听事件: 'popstate(仅监听浏览器前进/后退)',
兼容性: 'IE10+',
服务器配置: '必须配置(所有路径返回index.html)'
},
优点: [
'URL美观(无#)',
'SEO友好',
'可以使用锚点定位'
],
缺点: [
'需要服务器配置',
'兼容性稍差',
'实现稍复杂'
]
};
// ===== 实现 History 路由 =====
class HistoryRouter {
constructor() {
this.routes = {};
this.currentUrl = '';
// 监听popstate(浏览器前进/后退)
window.addEventListener('popstate', this.handlePopState.bind(this));
// 监听页面加载
window.addEventListener('load', this.handlePopState.bind(this));
// 拦截所有链接点击
this.bindLinks();
}
// 注册路由
route(path, callback) {
this.routes[path] = callback;
}
// 处理 popstate 事件
handlePopState() {
this.currentUrl = location.pathname;
console.log('路由变化:', this.currentUrl);
this.render();
}
// 渲染页面
render() {
if (this.routes[this.currentUrl]) {
this.routes[this.currentUrl]();
} else {
// 404
this.routes['/404'] && this.routes['/404']();
}
}
// 拦截所有 a 标签
bindLinks() {
document.addEventListener('click', (e) => {
if (e.target.tagName === 'A') {
e.preventDefault(); // 阻止默认跳转
const href = e.target.getAttribute('href');
this.push(href);
}
});
}
// 编程式导航
push(path) {
// pushState 不会触发 popstate 事件
history.pushState(null, '', path);
this.currentUrl = path;
this.render();
}
// 替换当前路由
replace(path) {
history.replaceState(null, '', path);
this.currentUrl = path;
this.render();
}
// 后退
back() {
history.back();
}
// 前进
forward() {
history.forward();
}
// 跳转指定步数
go(n) {
history.go(n);
}
}
// ===== 使用示例 =====
const router = new HistoryRouter();
router.route('/', function() {
document.getElementById('app').innerHTML = '<h1>首页</h1>';
});
router.route('/about', function() {
document.getElementById('app').innerHTML = '<h1>关于页面</h1>';
});
router.route('/user/123', function() {
document.getElementById('app').innerHTML = '<h1>用户123</h1>';
});
router.route('/404', function() {
document.getElementById('app').innerHTML = '<h1>404 Not Found</h1>';
});
<!-- ===== HTML 示例 ===== -->
<!DOCTYPE html>
<html>
<head>
<title>History Router</title>
</head>
<body>
<nav>
<a href="/">首页</a>
<a href="/about">关于</a>
<a href="/user/123">用户123</a>
</nav>
<div id="app"></div>
<script src="router.js"></script>
</body>
</html>
<!--
URL 变化示例:
1. 点击"首页" → https://example.com/
2. 点击"关于" → https://example.com/about
3. 点击"用户123" → https://example.com/user/123
特点:URL 美观,无 # ✅
-->
四、服务器配置(History 模式必需)
Nginx 配置
server {
listen 80;
server_name example.com;
root /usr/share/nginx/html;
index index.html;
location / {
# 尝试访问文件,不存在则返回 index.html
try_files $uri $uri/ /index.html;
# 或者使用 if 判断
# if (!-e $request_filename) {
# rewrite ^/(.*) /index.html last;
# }
}
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
Node.js (Express) 配置
const express = require('express');
const path = require('path');
const app = express();
// 静态资源
app.use(express.static(path.join(__dirname, 'dist')));
// 所有路由都返回 index.html
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
app.listen(3000, () => {
console.log('Server running on port 3000');
});
Apache 配置
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
RewriteRule ^index\.html$ - [L]
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_FILENAME} !-d
RewriteRule . /index.html [L]
</IfModule>
五、Vue Router 实战
Vue Router 基础使用
// ===== 安装 =====
// npm install vue-router@4
// ===== router/index.js =====
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router';
import Home from '@/views/Home.vue';
import About from '@/views/About.vue';
// 路由配置
const routes = [
{
path: '/',
name: 'Home',
component: Home,
meta: { title: '首页' }
},
{
path: '/about',
name: 'About',
component: About,
meta: { title: '关于' }
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue'), // 懒加载
props: true, // 路由参数作为组件 props
meta: { requiresAuth: true } // 需要登录
},
{
path: '/404',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
},
{
path: '/:pathMatch(.*)*', // 捕获所有路由
redirect: '/404'
}
];
// 创建路由实例
const router = createRouter({
// History 模式
history: createWebHistory(),
// 或者 Hash 模式
// history: createWebHashHistory(),
routes,
// 滚动行为
scrollBehavior(to, from, savedPosition) {
if (savedPosition) {
return savedPosition;
} else {
return { top: 0 };
}
}
});
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 设置标题
document.title = to.meta.title || 'Vue App';
// 权限验证
if (to.meta.requiresAuth && !isLoggedIn()) {
next('/login');
} else {
next();
}
});
// 全局后置钩子
router.afterEach((to, from) => {
console.log(`从 ${from.path} 跳转到 ${to.path}`);
});
export default router;
// ===== main.js =====
import { createApp } from 'vue';
import App from './App.vue';
import router from './router';
createApp(App)
.use(router)
.mount('#app');
// ===== App.vue =====
<template>
<div id="app">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-link :to="{ name: 'User', params: { id: 123 } }">
用户123
</router-link>
</nav>
<!-- 路由出口 -->
<router-view />
</div>
</template>
// ===== 组件中使用 =====
<script setup>
import { useRouter, useRoute } from 'vue-router';
const router = useRouter();
const route = useRoute();
// 获取路由参数
console.log('用户ID:', route.params.id);
// 获取查询参数
console.log('查询参数:', route.query);
// 编程式导航
function goToAbout() {
router.push('/about');
// 或
// router.push({ name: 'About' });
}
// 带查询参数
function goToUser() {
router.push({
path: '/user/123',
query: { tab: 'profile' }
});
// URL: /user/123?tab=profile
}
// 后退
function goBack() {
router.back();
}
</script>
六、React Router 实战
React Router 基础使用
// ===== 安装 =====
// npm install react-router-dom
// ===== App.jsx =====
import {
BrowserRouter,
// HashRouter, // Hash 模式
Routes,
Route,
Link,
Navigate,
useNavigate,
useParams,
useLocation
} from 'react-router-dom';
import Home from './pages/Home';
import About from './pages/About';
import User from './pages/User';
import NotFound from './pages/NotFound';
function App() {
return (
<BrowserRouter>
{/* 或使用 HashRouter */}
{/* <HashRouter> */}
<nav>
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Link to="/user/123">用户123</Link>
</nav>
<Routes>
<Route path="/" element={<Home />} />
<Route path="/about" element={<About />} />
<Route path="/user/:id" element={<User />} />
<Route path="/404" element={<NotFound />} />
<Route path="*" element={<Navigate to="/404" replace />} />
</Routes>
</BrowserRouter>
);
}
// ===== 组件中使用 =====
// pages/User.jsx
import { useParams, useNavigate, useLocation } from 'react-router-dom';
function User() {
const navigate = useNavigate();
const { id } = useParams(); // 获取路由参数
const location = useLocation(); // 获取当前路径
// 获取查询参数
const searchParams = new URLSearchParams(location.search);
const tab = searchParams.get('tab');
// 编程式导航
function goToAbout() {
navigate('/about');
}
// 带查询参数
function goToHome() {
navigate('/?page=1');
}
// 后退
function goBack() {
navigate(-1);
}
return (
<div>
<h1>用户 {id}</h1>
<p>Tab: {tab}</p>
<button onClick={goToAbout}>去关于页面</button>
<button onClick={goBack}>返回</button>
</div>
);
}
七、Hash vs History 对比表
| 特性 | Hash 模式 | History 模式 |
|---|---|---|
| URL 格式 | example.com/#/page |
example.com/page |
| 美观度 | ⭐⭐ | ⭐⭐⭐⭐⭐ |
| 兼容性 | IE8+ | IE10+ |
| SEO | ❌ 不友好 | ✅ 友好 |
| 服务器配置 | ✅ 无需配置 | ❌ 必须配置 |
| 锚点定位 | ❌ 不能用 | ✅ 可以用 |
| 监听事件 | hashchange |
popstate |
| 原理 | location.hash |
history.pushState |
| 推荐场景 | 快速开发、无后端 | 正式项目、需要SEO |
八、总结与记忆口诀 📝
核心记忆
两种模式:
- Hash 模式:URL 有 #,无需服务器配置
- History 模式:URL 美观,需要服务器配置
实现原理:
- Hash:监听
hashchange,改变location.hash - History:监听
popstate,使用history.pushState
记忆口诀
Hash 有井号,History 无
Hash 简单配,History 要服务器
Hash 兼容好,History 更美观
九、面试加分项 🌟
前端面试提升点
- ✅ 能清晰讲解 Hash 和 History 的区别
- ✅ 理解前端路由的实现原理
- ✅ 知道 History 模式需要服务器配置的原因
- ✅ 能手写简单的路由系统
业务代码提升点
- ✅ 使用 History 模式(更专业)
- ✅ 合理使用路由懒加载(减少首屏时间)
- ✅ 使用路由守卫控制权限
- ✅ 处理 404 和路由重定向
架构能力增强点
- ✅ 设计路由权限系统
- ✅ 实现路由级别的代码分割
- ✅ 优化路由切换动画
- ✅ 实现嵌套路由和动态路由
记住:前端路由是 SPA 的核心,掌握好路由才能做好单页应用! 🗺️
#前端面试小册##前端##银行##滴滴##阿里#前端面试小册 文章被收录于专栏
每天更新3-4节,持续更新中... 目标:50天学完,上岸银行总行!