不依赖框架,如何用 JS 实现一个完整的前端路由系统
摘要
现在几乎所有前端项目都离不开“SPA”(单页应用)。它让整个网页加载体验更顺滑、内容切换更快。而这背后的关键技术之一,就是前端路由。本篇文章从原理出发,通过手写一个基础的前端路由类,配合实际页面,带你一步步理解前端路由的机制,并扩展到参数、404 页面等实战应用。
引言
传统的网页开发是“多页应用”(MPA):每次点击链接都会重新加载一个完整的 HTML 页面。但这会让体验变得卡顿、不连续。而 SPA 则只在第一次加载 HTML,后续切换都由 JavaScript 接管,动态渲染页面内容。
现代框架(React、Vue、Angular)都内置了前端路由功能,比如 react-router
、vue-router
,但理解它们底层原理,不仅对学习框架有帮助,也能在遇到路由问题时更有思路。
手写一个最小可用的前端路由类
思路分析
核心任务:
- 拦截浏览器地址栏变化(通过
pushState()
修改地址) - 监听前进/后退(通过
popstate
事件) - 根据路径匹配回调,渲染页面内容
实现路由类:代码 + 注释
class Router {constructor() {this.routes = {}; // 用于存储 path -> callback 的映射this.notFound = null;// 监听浏览器前进/后退事件window.addEventListener('popstate', this.handlePopState.bind(this));}// 注册路径和对应的回调函数addRoute(path, callback) {this.routes[path] = callback;}// 注册404页面setNotFound(callback) {this.notFound = callback;}// 跳转到指定路径navigate(path) {history.pushState({}, '', path); // 修改地址栏,但不刷新页面this.handlePopState(); // 手动触发一次路由处理}// 处理路径变化:找到并执行对应回调handlePopState() {const path = window.location.pathname;// 如果路径存在,则执行对应的渲染函数if (this.routes[path]) {this.routes[path]();} else if (this.notFound) {this.notFound();} else {console.warn(`路径不存在: ${path}`);}}
}
实战:页面导航和内容渲染
页面HTML结构
<div id="nav"><a href="/home" onclick="goTo('/home')">首页</a><a href="/about" onclick="goTo('/about')">关于我们</a><a href="/contact" onclick="goTo('/contact')">联系我们</a><a href="/notfound" onclick="goTo('/notfound')">未知页面</a>
</div>
<hr />
<div id="content">这里是默认内容</div>
配置路由和回调
const router = new Router();// 注册路由
router.addRoute('/home', () => {document.getElementById('content').innerHTML = '<h2>欢迎来到首页</h2>';
});router.addRoute('/about', () => {document.getElementById('content').innerHTML = '<h2>我们是一个前端开发团队</h2>';
});router.addRoute('/contact', () => {document.getElementById('content').innerHTML = '<h2>请通过邮箱联系我</h2>';
});// 设置 404 页面
router.setNotFound(() => {document.getElementById('content').innerHTML = '<h2>404 页面未找到</h2>';
});// 点击链接时跳转
function goTo(path) {event.preventDefault(); // 阻止 a 标签默认行为router.navigate(path);
}// 页面刷新时保持当前路由内容
window.addEventListener('DOMContentLoaded', () => {router.handlePopState();
});
高阶功能拓展
场景一:支持路径参数(比如 /user/123
)
我们改造 Router
类,让它支持参数路由:
class Router {constructor() {this.routes = [];}addRoute(path, callback) {const paramNames = [];const regex = path.replace(/:([^\/]+)/g, (_, key) => {paramNames.push(key);return '([^\/]+)';});const pattern = new RegExp(`^${regex}$`);this.routes.push({ pattern, paramNames, callback });}navigate(path) {history.pushState({}, '', path);this.handlePopState();}handlePopState() {const path = window.location.pathname;for (const route of this.routes) {const match = route.pattern.exec(path);if (match) {const params = {};route.paramNames.forEach((key, i) => {params[key] = match[i + 1];});route.callback(params);return;}}document.getElementById('content').innerHTML = '<h2>404 Not Found</h2>';}
}
使用方式如下:
const router = new Router();router.addRoute('/user/:id', ({ id }) => {document.getElementById('content').innerHTML = `<h2>当前用户ID:${id}</h2>`;
});
现在访问 /user/123
会显示 当前用户ID:123
。
QA 问答环节
Q1:前端路由和后端路由有什么区别?
前端路由是在浏览器端用 JavaScript 控制地址栏和页面内容;后端路由是在服务器上解析 URL,返回不同页面或数据。
Q2:为什么刷新页面会 404?
因为浏览器发起了真实的 HTTP 请求,访问的是 /about
路径,而服务器没有设置“返回 index.html”,所以报错。你需要在服务器配置中添加“任意路径都返回 index.html”。
Q3:为什么不直接用 hash (#
) 路由?
早期前端路由是靠 window.location.hash
实现的(比如 /#/home
),兼容性好,但不好看也不能完全模拟真实路径。pushState
更现代、真实,还能配合服务端渲染。
总结
我们从最简单的原理出发,手写了一个可以运行的前端路由系统,并支持:
- 多路径匹配
- 浏览器前进/后退按钮
- 404 页面
- 参数化路径(如
/user/:id
)
这个 Router 类虽然功能简单,但已经足够支撑一个轻量级 SPA 应用的页面跳转。如果你刚入门前端,强烈建议自己动手实现一遍,加深理解。
如果你对嵌套路由、懒加载路由组件、路由守卫等更高级玩法感兴趣,也欢迎告诉我,我可以继续带你深入挖掘。