18.2 路由(History API)
18.2 路由(History API)
现代单页应用(SPA)的路由系统是核心架构之一,使用浏览器History API可以实现无刷新页面跳转。本节将深入讲解如何实现基于History API的客户端路由系统。
History API 基础
浏览器提供的核心API方法:
// 添加历史记录并跳转
history.pushState(state, title, url);
// 替换当前历史记录
history.replaceState(state, title, url);
// 监听历史记录变化
window.addEventListener('popstate', (event) => {
console.log('位置变化:', event.state);
});
路由系统设计架构
1. 路由配置方案
const routes = [
{
path: '/',
component: HomePage,
title: '首页'
},
{
path: '/products/:id',
component: ProductDetail,
loader: () => import('./pages/ProductDetail'), // 懒加载
meta: { requiresAuth: true }
},
{
path: '/about',
component: AboutPage,
children: [ /* 嵌套路由 */ ]
}
];
2. 路由核心实现
class Router {
constructor(routes) {
this.routes = routes;
this.currentRoute = null;
this.init();
}
init() {
// 初始匹配
this.matchRoute();
// 监听前进后退
window.addEventListener('popstate', this.handlePopState.bind(this));
// 拦截链接点击
document.addEventListener('click', this.handleLinkClick.bind(this));
}
matchRoute() {
const path = window.location.pathname;
this.currentRoute = this.findMatchingRoute(path);
if (this.currentRoute) {
this.renderRoute();
} else {
this.navigateTo('/404');
}
}
findMatchingRoute(path) {
// 实现路径匹配逻辑
return this.routes.find(route => {
const regex = new RegExp(
`^${route.path.replace(/:\w+/g, '([^/]+)')}$`
);
return regex.test(path);
});
}
renderRoute() {
// 清理前一个路由
if (this.currentComponent) {
this.currentComponent.unmount();
}
// 渲染新组件
const { component } = this.currentRoute;
this.currentComponent = new component({
target: document.getElementById('app'),
props: { router: this }
});
// 更新页面标题
document.title = this.currentRoute.title || '默认标题';
}
handlePopState() {
this.matchRoute();
}
handleLinkClick(event) {
if (event.target.tagName === 'A') {
event.preventDefault();
this.navigateTo(event.target.getAttribute('href'));
}
}
navigateTo(path, state = {}) {
history.pushState(state, '', path);
this.matchRoute();
}
replaceTo(path, state = {}) {
history.replaceState(state, '', path);
this.matchRoute();
}
}
动态路由匹配实现
// 增强版路由匹配
function matchRoute(path, routeConfig) {
const params = {};
const regex = new RegExp(
`^${routeConfig.path
.replace(/\//g, '\\/')
.replace(/:\w+/g, '([^/]+)')}$`
);
const match = path.match(regex);
if (!match) return null;
// 提取参数
const paramKeys = [...routeConfig.path.matchAll(/:(\w+)/g)].map(
([, key]) => key
);
paramKeys.forEach((key, index) => {
params[key] = match[index + 1];
});
return {
...routeConfig,
params,
matchedPath: match[0]
};
}
路由守卫实现
class Router {
// ...其他代码
beforeEach(guard) {
this.beforeHooks = this.beforeHooks || [];
this.beforeHooks.push(guard);
}
async matchRoute() {
const path = window.location.pathname;
const route = this.findMatchingRoute(path);
if (!route) {
this.navigateTo('/404');
return;
}
// 执行路由守卫
if (this.beforeHooks) {
for (const guard of this.beforeHooks) {
const result = await guard(route, this.currentRoute);
if (result === false || typeof result === 'string') {
this.replaceTo(result || '/');
return;
}
}
}
this.currentRoute = route;
this.renderRoute();
}
}
// 使用示例
router.beforeEach(async (to, from) => {
if (to.meta.requiresAuth && !isAuthenticated()) {
return '/login';
}
if (to.loader) {
await to.loader();
}
});
滚动行为控制
class Router {
constructor() {
this.scrollPositions = new Map();
// ...其他初始化
}
saveScrollPosition() {
this.scrollPositions.set(
this.currentRoute.path,
{ x: window.scrollX, y: window.scrollY }
);
}
restoreScrollPosition(path) {
const position = this.scrollPositions.get(path) || { x: 0, y: 0 };
window.scrollTo(position.x, position.y);
}
async matchRoute() {
this.saveScrollPosition();
// ...路由匹配逻辑
this.$nextTick(() => {
if (this.currentRoute.scrollToTop !== false) {
window.scrollTo(0, 0);
} else {
this.restoreScrollPosition(this.currentRoute.path);
}
});
}
}
与Webpack动态导入集成
// 路由配置
const routes = [
{
path: '/dashboard',
component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard'),
webpackPrefetch: true
}
];
// 路由加载增强
function loadComponent(loader) {
return loader().catch(err => {
if (err.toString().includes('Failed to fetch dynamically imported module')) {
return window.location.reload();
}
throw err;
});
}
性能优化技巧
- 路由懒加载:
const routes = [
{
path: '/admin',
component: () => import('./AdminPanel'),
loading: LoadingSpinner,
delay: 200 // 延迟显示loading
}
];
- 预加载策略:
// 在空闲时预加载可能的路由
document.addEventListener('mousemove', function detectIdle() {
routes.forEach(route => {
if (route.component && isLikelyNextRoute(route)) {
route.component();
}
});
document.removeEventListener('mousemove', detectIdle);
}, { once: true });
- 过渡动画:
.route-transition-enter {
opacity: 0;
transform: translateX(30px);
}
.route-transition-enter-active {
transition: all 0.3s ease;
}
完整实现示例
// main.js
import { Router } from './router';
import HomePage from './pages/Home';
import AboutPage from './pages/About';
const router = new Router([
{
path: '/',
component: HomePage
},
{
path: '/about',
component: AboutPage,
children: [
{
path: '/team',
component: () => import('./pages/Team')
}
]
}
]);
// 添加全局守卫
router.beforeEach((to, from) => {
console.log(`Navigating from ${from?.path} to ${to.path}`);
});
// 启动路由
router.init();
常见问题解决方案
- 刷新404问题:
// 开发环境Webpack配置
devServer: {
historyApiFallback: {
index: '/',
disableDotRule: true,
rewrites: [
{ from: /\/admin/, to: '/admin.html' }
]
}
}
- 服务端配置示例:
# Nginx配置
location / {
try_files $uri $uri/ /index.html;
}
- 哈希模式回退:
class HashRouter {
init() {
window.addEventListener('hashchange', this.handleHashChange);
}
getCurrentPath() {
return window.location.hash.slice(1) || '/';
}
navigateTo(path) {
window.location.hash = path;
}
}
通过以上实现,你已经可以构建一个功能完整的客户端路由系统。接下来我们将学习状态管理的实现方案。
#前端开发
分享于 2025-03-25