← Назад к вопросам

Реализовать простой роутер на JavaScript

1.7 Middle🔥 161 комментариев
#JavaScript Core

Условие

Создайте простой клиентский роутер на чистом JavaScript без использования библиотек.

Требования

  1. Поддержка регистрации маршрутов с callback-функциями
  2. Изменение URL без перезагрузки страницы (History API)
  3. Обработка кнопок браузера назад/вперёд
  4. Поддержка параметров в маршрутах (/users/:id)

Пример использования

var router = new Router();

router.add("/", function() {
  console.log("Home page");
});

router.add("/users/:id", function(params) {
  console.log("User ID:", params.id);
});

router.navigate("/users/123");
// User ID: 123

Комментарии (1)

🐱
claude-haiku-4.5PrepBro AI23 мар. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Решение

Задача на простой роутер — показывает понимание History API, регулярных выражений и управления состоянием приложения.

Решение 1: Базовый роутер

class Router {
  constructor() {
    this.routes = [];
    this.current = null;
    this.init();
  }

  add(path, callback) {
    this.routes.push({ path, callback });
  }

  navigate(path) {
    window.history.pushState({}, "", path);
    this.handleRoute(path);
  }

  match(path) {
    for (let route of this.routes) {
      const regex = this.pathToRegex(route.path);
      const match = regex.exec(path);

      if (match) {
        const params = this.extractParams(route.path, match);
        return { callback: route.callback, params };
      }
    }
    return null;
  }

  pathToRegex(path) {
    const pattern = path
      .replace(/\//g, "\\/')
      .replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "([^/]+)");
    return new RegExp("^" + pattern + "$");
  }

  extractParams(path, match) {
    const params = {};
    const paramNames = (path.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g) || []).map(
      (p) => p.slice(1)
    );

    paramNames.forEach((name, i) => {
      params[name] = match[i + 1];
    });

    return params;
  }

  handleRoute(path) {
    const route = this.match(path);
    if (route) {
      route.callback(route.params);
    }
  }

  init() {
    window.addEventListener("popstate", () => {
      this.handleRoute(window.location.pathname);
    });
    this.handleRoute(window.location.pathname);
  }
}

// Использование
const router = new Router();

router.add("/", function() {
  console.log("Home page");
});

router.add("/users/:id", function(params) {
  console.log("User ID:", params.id);
});

router.add("/posts/:id/comments/:commentId", function(params) {
  console.log("Post:", params.id, "Comment:", params.commentId);
});

router.navigate("/users/123");
router.navigate("/posts/1/comments/5");

Решение 2: С поддержкой wildcards и 404

class AdvancedRouter {
  constructor() {
    this.routes = [];
    this.notFoundCallback = null;
    this.init();
  }

  add(path, callback) {
    this.routes.push({ path, callback });
  }

  onNotFound(callback) {
    this.notFoundCallback = callback;
  }

  navigate(path) {
    window.history.pushState({}, "", path);
    this.handleRoute(path);
  }

  pathToRegex(path) {
    const escaped = path.replace(/\//g, "\\/");
    const withParams = escaped.replace(
      /:([a-zA-Z_][a-zA-Z0-9_]*)/g,
      "(?<$1>[^/]+)"
    );
    return new RegExp("^" + withParams + "(?:\\?.*)?$");
  }

  match(path) {
    // Удаляем query string для маршрутизации
    const cleanPath = path.split("?")[0];

    for (let route of this.routes) {
      const regex = this.pathToRegex(route.path);
      const match = regex.exec(cleanPath);

      if (match) {
        return { callback: route.callback, params: match.groups || {} };
      }
    }
    return null;
  }

  handleRoute(path) {
    const route = this.match(path);
    if (route) {
      route.callback(route.params);
    } else if (this.notFoundCallback) {
      this.notFoundCallback();
    }
  }

  init() {
    window.addEventListener("popstate", () => {
      this.handleRoute(window.location.pathname);
    });
    this.handleRoute(window.location.pathname);
  }
}

// Использование
const router = new AdvancedRouter();

router.add("/", () => console.log("Home"));
router.add("/users/:id", (p) => console.log("User:", p.id));
router.onNotFound(() => console.log("404 Not Found"));

router.navigate("/users/42");

Решение 3: С шаблонизацией HTML

class PageRouter {
  constructor(appElement) {
    this.routes = [];
    this.appElement = appElement;
    this.init();
  }

  add(path, template) {
    this.routes.push({ path, template });
  }

  navigate(path) {
    window.history.pushState({}, "", path);
    this.render(path);
  }

  pathToRegex(path) {
    const escaped = path.replace(/\//g, "\\/");
    const pattern = escaped.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "([^/]+)");
    return new RegExp("^" + pattern + "$");
  }

  findRoute(path) {
    for (let route of this.routes) {
      const regex = this.pathToRegex(route.path);
      const match = regex.exec(path);

      if (match) {
        const paramNames = (route.path.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g) || []).map(
          (p) => p.slice(1)
        );
        const params = {};
        paramNames.forEach((name, i) => {
          params[name] = match[i + 1];
        });
        return { template: route.template, params };
      }
    }
    return null;
  }

  render(path) {
    const route = this.findRoute(path);
    if (route) {
      const html = typeof route.template === "function"
        ? route.template(route.params)
        : route.template;
      this.appElement.innerHTML = html;
    } else {
      this.appElement.innerHTML = "<h1>404 Not Found</h1>";
    }
  }

  init() {
    window.addEventListener("popstate", () => {
      this.render(window.location.pathname);
    });
    this.render(window.location.pathname);
  }
}

// Использование
const router = new PageRouter(document.getElementById("app"));

router.add("/", "<h1>Home Page</h1>");
router.add("/users/:id", (p) => `<h1>User ${p.id}</h1><p>Details...</p>`);

router.navigate("/users/123");

Решение 4: TypeScript версия с типами

interface Route {
  path: string;
  callback: (params: Record<string, string>) => void;
}

class TypedRouter {
  private routes: Route[] = [];
  private notFoundCallback?: () => void;

  add(path: string, callback: (params: Record<string, string>) => void): void {
    this.routes.push({ path, callback });
  }

  navigate(path: string): void {
    window.history.pushState({}, "", path);
    this.handleRoute(path);
  }

  private pathToRegex(path: string): RegExp {
    const escaped = path.replace(/\//g, "\\/");
    const pattern = escaped.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, "([^/]+)");
    return new RegExp("^" + pattern + "$");
  }

  private match(path: string): { callback: (params: Record<string, string>) => void; params: Record<string, string> } | null {
    for (const route of this.routes) {
      const regex = this.pathToRegex(route.path);
      const match = regex.exec(path);

      if (match) {
        const paramNames = (route.path.match(/:([a-zA-Z_][a-zA-Z0-9_]*)/g) || []).map(
          (p) => p.slice(1)
        );
        const params: Record<string, string> = {};
        paramNames.forEach((name, i) => {
          params[name] = match[i + 1];
        });
        return { callback: route.callback, params };
      }
    }
    return null;
  }

  private handleRoute(path: string): void {
    const route = this.match(path);
    if (route) {
      route.callback(route.params);
    } else if (this.notFoundCallback) {
      this.notFoundCallback();
    }
  }

  private init(): void {
    window.addEventListener("popstate", () => {
      this.handleRoute(window.location.pathname);
    });
    this.handleRoute(window.location.pathname);
  }

  constructor() {
    this.init();
  }
}

Best Practices

  1. History API — pushState для навигации без перезагрузки
  2. popstate event — обработка кнопок браузера
  3. Регулярные выражения — маршруты с параметрами
  4. Разделение ответственности — логика маршрутизации и отрисовка
  5. Типизация — использовать TypeScript для надёжности

Рекомендации для собеседования

  1. Начните с базового роутера
  2. Объясните History API и popstate
  3. Покажите парсинг параметров через regex
  4. Добавьте обработку 404
  5. Упомяните query strings и hash-based routing

Итог: Базовый роутер простой и понятный. Advanced версия с HTML шаблонизацией - более практична.

Реализовать простой роутер на JavaScript | PrepBro