插件窝 干货文章 如何用纯JavaScript实现单页面应用的路由功能,避免重复编写代码?

如何用纯JavaScript实现单页面应用的路由功能,避免重复编写代码?

路由 matchedRoute path const 1013    来源:    2025-03-26

使用纯JavaScript实现单页面应用(SPA)路由

在单页面应用中实现路由功能可以避免页面刷新,提供更流畅的用户体验。下面我将介绍一种模块化的实现方式,可以避免代码重复。

核心路由实现方案

1. 基础路由类实现

class Router {
  constructor(routes) {
    this.routes = routes || [];
    this.currentRoute = null;
    this.root = '/';

    // 监听路由变化
    window.addEventListener('popstate', () => this.handleRouteChange());
    document.addEventListener('DOMContentLoaded', () => this.handleRouteChange());
  }

  // 添加路由
  addRoute(path, callback) {
    this.routes.push({ path, callback });
    return this;
  }

  // 路由变化处理
  handleRouteChange() {
    const path = window.location.pathname.replace(this.root, '') || '/';
    const matchedRoute = this.matchRoute(path);

    if (matchedRoute) {
      this.currentRoute = matchedRoute;
      matchedRoute.callback(matchedRoute.params);
    } else {
      // 默认路由或404处理
      const defaultRoute = this.routes.find(r => r.path === '*');
      if (defaultRoute) defaultRoute.callback();
    }
  }

  // 路由匹配
  matchRoute(path) {
    for (const route of this.routes) {
      if (route.path === '*') continue;

      const paramNames = [];
      const regexPath = route.path.replace(/:(\w+)/g, (_, paramName) => {
        paramNames.push(paramName);
        return '([^/]+)';
      });

      const match = path.match(new RegExp(`^${regexPath}$`));
      if (match) {
        const params = {};
        paramNames.forEach((name, index) => {
          params[name] = match[index + 1];
        });
        return { ...route, params };
      }
    }
    return null;
  }

  // 导航到新路由
  navigate(path) {
    window.history.pushState({}, '', this.root + path);
    this.handleRouteChange();
  }

  // 设置根路径
  setRoot(root) {
    this.root = root;
    return this;
  }
}

2. 使用示例

// 初始化路由
const router = new Router()
  .setRoot('/app') // 设置基础路径
  .addRoute('/', () => {
    // 首页逻辑
    document.getElementById('content').innerHTML = '<h1>Home Page</h1>';
  })
  .addRoute('/about', () => {
    // 关于页面逻辑
    document.getElementById('content').innerHTML = '<h1>About Us</h1>';
  })
  .addRoute('/user/:id', (params) => {
    // 用户详情页
    document.getElementById('content').innerHTML = `<h1>User Profile: ${params.id}</h1>`;
  })
  .addRoute('*', () => {
    // 404处理
    document.getElementById('content').innerHTML = '<h1>Page Not Found</h1>';
  });

// 在HTML中使用
// <a href="/app/about" onclick="router.navigate('/about'); return false;">About</a>

优化建议

1. 模块化路由配置

将路由配置单独提取到模块中:

// routes.js
export const routes = [
  {
    path: '/',
    name: 'home',
    component: '<h1>Home Page</h1>'
  },
  {
    path: '/about',
    name: 'about',
    component: '<h1>About Us</h1>'
  },
  {
    path: '/user/:id',
    name: 'user',
    component: params => `<h1>User Profile: ${params.id}</h1>`
  },
  {
    path: '*',
    component: '<h1>Page Not Found</h1>'
  }
];

2. 增强型路由类

import { routes } from './routes.js';

class EnhancedRouter extends Router {
  constructor() {
    super(routes);
    this.components = {};
  }

  // 注册组件
  registerComponent(name, component) {
    this.components[name] = component;
  }

  // 渲染组件
  renderComponent(component, params) {
    const contentEl = document.getElementById('content');
    if (typeof component === 'function') {
      contentEl.innerHTML = component(params);
    } else {
      contentEl.innerHTML = component;
    }
  }

  // 重写路由处理
  handleRouteChange() {
    const path = window.location.pathname.replace(this.root, '') || '/';
    const matchedRoute = this.matchRoute(path);

    if (matchedRoute) {
      this.currentRoute = matchedRoute;
      if (matchedRoute.name && this.components[matchedRoute.name]) {
        this.renderComponent(this.components[matchedRoute.name], matchedRoute.params);
      } else {
        this.renderComponent(matchedRoute.component, matchedRoute.params);
      }
    } else {
      const defaultRoute = this.routes.find(r => r.path === '*');
      if (defaultRoute) this.renderComponent(defaultRoute.component);
    }
  }
}

3. 使用HTML5 History API的完整示例

// 完整路由实现
class SPARouter {
  constructor(options = {}) {
    this.routes = options.routes || [];
    this.mode = options.mode || 'history';
    this.root = options.root || '/';
    this.beforeEach = options.beforeEach || null;
    this.afterEach = options.afterEach || null;

    this.init();
  }

  init() {
    if (this.mode === 'history') {
      window.addEventListener('popstate', () => this.handleRoute());
    } else {
      window.addEventListener('hashchange', () => this.handleRoute());
    }
    this.handleRoute();
  }

  getFragment() {
    let fragment = '';
    if (this.mode === 'history') {
      fragment = decodeURI(window.location.pathname);
      fragment = fragment.replace(this.root, '') || '/';
    } else {
      const match = window.location.href.match(/#(.*)$/);
      fragment = match ? match[1] : '/';
    }
    return fragment;
  }

  matchRoute(fragment) {
    for (const route of this.routes) {
      if (route.path === '*') continue;

      const paramNames = [];
      const regexPath = route.path.replace(/:(\w+)/g, (_, paramName) => {
        paramNames.push(paramName);
        return '([^/]+)';
      });

      const match = fragment.match(new RegExp(`^${regexPath}$`));
      if (match) {
        const params = {};
        paramNames.forEach((name, index) => {
          params[name] = match[index + 1];
        });
        return { ...route, params };
      }
    }
    return null;
  }

  async handleRoute() {
    const fragment = this.getFragment();
    const matchedRoute = this.matchRoute(fragment) || this.routes.find(r => r.path === '*');

    if (!matchedRoute) return;

    // 路由守卫
    if (this.beforeEach) {
      const shouldProceed = await this.beforeEach(matchedRoute, this.currentRoute);
      if (shouldProceed === false) return;
    }

    this.currentRoute = matchedRoute;

    try {
      if (typeof matchedRoute.callback === 'function') {
        await matchedRoute.callback(matchedRoute.params);
      }

      // 路由后置钩子
      if (this.afterEach) {
        await this.afterEach(matchedRoute);
      }
    } catch (error) {
      console.error('Route execution error:', error);
    }
  }

  navigate(path) {
    if (this.mode === 'history') {
      window.history.pushState(null, null, this.root + path);
    } else {
      window.location.href = `${window.location.href.replace(/#(.*)$/, '')}#${path}`;
    }
    this.handleRoute();
  }

  replace(path) {
    if (this.mode === 'history') {
      window.history.replaceState(null, null, this.root + path);
    } else {
      window.location.replace(`${window.location.href.replace(/#(.*)$/, '')}#${path}`);
    }
    this.handleRoute();
  }
}

最佳实践建议

  1. 路由懒加载:对于大型应用,可以实现组件的懒加载
  2. 路由守卫:实现认证检查等路由守卫功能
  3. 滚动行为:记录和恢复滚动位置
  4. 过渡效果:添加页面切换的过渡动画
  5. 错误处理:统一的错误处理机制

这种实现方式避免了重复代码,提供了良好的扩展性,可以满足大多数单页面应用的路由需求。