你是不是也遇到过这样的场景?开发单页面应用时,页面跳转后刷新一下就404,或者URL里带着难看的#号,被产品经理吐槽不够优雅?
别担心,今天我就带你彻底搞懂前端路由的两种模式,手把手教你实现一个迷你路由,并告诉你什么场景该用哪种方案。
读完本文,你能获得一套完整的前端路由知识体系,从原理到实战,再到生产环境配置,一次性全搞定!
为什么需要前端路由?
想象一下,你正在开发一个后台管理系统。传统做法是每个页面都对应一个HTML文件,切换页面就要重新加载,体验特别差。
而前端路由让你可以在一个页面内实现不同视图的切换,URL变化了但页面不刷新,用户体验流畅得像原生APP一样。
手写迷你路由:50行代码看懂原理
我们先来实现一个最简单的路由,这样你就能彻底明白路由是怎么工作的了。
javascript
// 定义我们的迷你路由类
class MiniRouter {
constructor() {
// 保存路由配置
this.routes = {};
// 当前URL的hash
this.currentUrl = '';
// 监听hashchange事件
window.addEventListener('hashchange', this.refresh.bind(this));
}
// 添加路由配置
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 路由刷新
refresh() {
// 获取当前hash,去掉#号
this.currentUrl = location.hash.slice(1) || '/';
// 执行对应的回调函数
if (this.routes[this.currentUrl]) {
this.routes[this.currentUrl]();
}
}
// 初始化
init() {
window.addEventListener('load', this.refresh.bind(this), false);
}
}
// 使用示例
const router = new MiniRouter();
router.init();
// 配置路由
router.route('/', function() {
document.body.innerHTML = '这是首页';
});
router.route('/about', function() {
document.body.innerHTML = '这是关于页面';
});
router.route('/contact', function() {
document.body.innerHTML = '这是联系我们页面';
});
这段代码虽然简单,但包含了路由的核心逻辑:监听URL变化,然后执行对应的函数来更新页面内容。
现在你可以在浏览器里试试,访问http://your-domain.com/#/about
就能看到效果了!
Hash模式:简单粗暴的解决方案
Hash模式是利用URL中#号后面的部分来实现的。比如http://example.com/#/user
,#/user
就是hash部分。
Hash模式的实现原理
javascript
// 监听hash变化
window.addEventListener('hashchange', function() {
const hash = location.hash.slice(1); // 去掉#号
console.log('当前hash:', hash);
// 根据不同的hash显示不同内容
switch(hash) {
case '/home':
showHomePage();
break;
case '/about':
showAboutPage();
break;
default:
showNotFound();
}
});
// 手动改变hash
function navigateTo(path) {
location.hash = path;
}
// 使用示例
navigateTo('/about'); // URL变成 http://example.com/#/about
Hash模式的优点
兼容性极好,能支持到IE8。不需要服务器端任何配置,因为#号后面的内容不会发给服务器。
部署简单,直接扔到静态服务器就能用。
Hash模式的缺点
URL中带着#号,看起来不够优雅。SEO支持不好,搜索引擎对#后面的内容理解有限。
History模式:优雅的专业选择
History模式利用了HTML5的History API,让URL看起来和正常页面一样,比如http://example.com/user
。
History API的核心方法
javascript
// 跳转到新URL,但不刷新页面
history.pushState({}, '', '/user');
// 替换当前URL
history.replaceState({}, '', '/settings');
// 监听前进后退
window.addEventListener('popstate', function() {
// 这里处理路由变化
handleRouteChange(location.pathname);
});
手写History路由
javascript
class HistoryRouter {
constructor() {
this.routes = {};
// 监听popstate事件(浏览器前进后退)
window.addEventListener('popstate', (e) => {
const path = location.pathname;
this.routes[path] && this.routes[path]();
});
}
// 添加路由
route(path, callback) {
this.routes[path] = callback || function() {};
}
// 跳转
push(path) {
history.pushState({}, '', path);
this.routes[path] && this.routes[path]();
}
// 初始化
init() {
// 页面加载时执行当前路由
const path = location.pathname;
this.routes[path] && this.routes[path]();
}
}
// 使用示例
const router = new HistoryRouter();
router.route('/', function() {
document.body.innerHTML = 'History模式首页';
});
router.route('/about', function() {
document.body.innerHTML = 'History模式关于页面';
});
// 初始化
router.init();
// 编程式导航
document.getElementById('about-btn').addEventListener('click', () => {
router.push('/about');
});
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配置
nginx
server {
listen 80;
server_name example.com;
root /path/to/your/app;
location / {
try_files $uri $uri/ /index.html;
}
}
这个配置的意思是:先尝试找对应的文件,如果找不到就返回index.html。
Node.js Express配置
javascript
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);
Apache配置
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>
实战:完整的前端路由库
现在我们把两种模式整合起来,实现一个完整的前端路由:
javascript
class AdvancedRouter {
constructor(mode = 'hash') {
this.mode = mode;
this.routes = {};
this.current = '';
this.init();
}
init() {
if (this.mode === 'hash') {
// Hash模式监听
window.addEventListener('hashchange', () => {
this.handleChange(location.hash.slice(1));
});
// 初始化
this.handleChange(location.hash.slice(1) || '/');
} else {
// History模式监听
window.addEventListener('popstate', () => {
this.handleChange(location.pathname);
});
// 初始化
this.handleChange(location.pathname);
}
}
handleChange(path) {
this.current = path;
const callback = this.routes[path] || this.routes['*'];
if (callback) {
callback();
}
}
// 添加路由
on(path, callback) {
this.routes[path] = callback;
}
// 跳转
go(path) {
if (this.mode === 'hash') {
location.hash = path;
} else {
history.pushState({}, '', path);
this.handleChange(path);
}
}
// 替换
replace(path) {
if (this.mode === 'hash') {
location.replace(location.pathname + '#' + path);
} else {
history.replaceState({}, '', path);
this.handleChange(path);
}
}
}
// 使用示例
const router = new AdvancedRouter('history');
router.on('/', () => {
showPage('home');
});
router.on('/about', () => {
showPage('about');
});
router.on('*', () => {
showPage('not-found');
});
function showPage(pageName) {
document.body.innerHTML = `当前页面:${pageName}`;
}
性能优化技巧
路由用得不好会影响性能,这里分享几个实用技巧:
路由懒加载
javascript
// 动态导入,只有访问时才加载
router.on('/heavy-page', async () => {
const { HeavyComponent } = await import('./HeavyComponent.js');
render(HeavyComponent);
});
路由缓存
javascript
const cache = new Map();
router.on('/expensive-page', () => {
if (cache.has('/expensive-page')) {
// 使用缓存
showContent(cache.get('/expensive-page'));
return;
}
// 首次加载
fetchData().then(data => {
cache.set('/expensive-page', data);
showContent(data);
});
});
常见坑点及解决方案
1. 路由循环跳转
javascript
// 错误示例:会导致无限循环
router.on('/login', () => {
if (!isLoggedIn) {
router.go('/login'); // 循环了!
}
});
// 正确做法
router.on('/login', () => {
if (!isLoggedIn) {
return; // 停留在登录页
}
router.go('/dashboard');
});
2. 路由权限控制
javascript
// 路由守卫
router.beforeEach = (to, from, next) => {
if (to.path === '/admin' && !isAdmin()) {
next('/login');
} else {
next();
}
};
该用Hash还是History?
看到这里,你可能还是有点纠结。我给你一个简单的决策流程:
如果你的项目是内部系统,不需要SEO,或者要支持IE8/9,选Hash模式。
如果是面向公众的网站,需要SEO,且能控制服务器配置,选History模式。
如果是微前端架构的子应用,建议用Hash模式,避免与主应用冲突。
写在最后
前端路由看似简单,但里面有很多细节值得深究。无论是Hash的兼容性优势,还是History的优雅体验,都有各自的适用场景。
现在主流框架的路由库(Vue Router、React Router)都同时支持两种模式,原理和我们今天手写的迷你路由大同小异。
理解了底层原理,你再使用这些高级路由库时,就能更得心应手,遇到问题也知道如何调试和解决。
你在项目中用的是Hash模式还是History模式?遇到过什么有趣的问题吗?欢迎在评论区分享你的经验!