← Назад к вопросам
Реализовать простой роутер на JavaScript
1.7 Middle🔥 161 комментариев
#JavaScript Core
Условие
Создайте простой клиентский роутер на чистом JavaScript без использования библиотек.
Требования
- Поддержка регистрации маршрутов с callback-функциями
- Изменение URL без перезагрузки страницы (History API)
- Обработка кнопок браузера назад/вперёд
- Поддержка параметров в маршрутах (/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
- History API — pushState для навигации без перезагрузки
- popstate event — обработка кнопок браузера
- Регулярные выражения — маршруты с параметрами
- Разделение ответственности — логика маршрутизации и отрисовка
- Типизация — использовать TypeScript для надёжности
Рекомендации для собеседования
- Начните с базового роутера
- Объясните History API и popstate
- Покажите парсинг параметров через regex
- Добавьте обработку 404
- Упомяните query strings и hash-based routing
Итог: Базовый роутер простой и понятный. Advanced версия с HTML шаблонизацией - более практична.