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;
  });
}

性能优化技巧

  1. 路由懒加载
const routes = [
  {
    path: '/admin',
    component: () => import('./AdminPanel'),
    loading: LoadingSpinner,
    delay: 200 // 延迟显示loading
  }
];
  1. 预加载策略
// 在空闲时预加载可能的路由
document.addEventListener('mousemove', function detectIdle() {
  routes.forEach(route => {
    if (route.component && isLikelyNextRoute(route)) {
      route.component();
    }
  });
  document.removeEventListener('mousemove', detectIdle);
}, { once: true });
  1. 过渡动画
.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();

常见问题解决方案

  1. 刷新404问题
// 开发环境Webpack配置
devServer: {
  historyApiFallback: {
    index: '/',
    disableDotRule: true,
    rewrites: [
      { from: /\/admin/, to: '/admin.html' }
    ]
  }
}
  1. 服务端配置示例
# Nginx配置
location / {
  try_files $uri $uri/ /index.html;
}
  1. 哈希模式回退
class HashRouter {
  init() {
    window.addEventListener('hashchange', this.handleHashChange);
  }
  
  getCurrentPath() {
    return window.location.hash.slice(1) || '/';
  }
  
  navigateTo(path) {
    window.location.hash = path;
  }
}

通过以上实现,你已经可以构建一个功能完整的客户端路由系统。接下来我们将学习状态管理的实现方案。

#前端开发 分享于 2025-03-25

【 内容由 AI 共享,不代表本站观点,请谨慎参考 】