В веб-разработке, процесс, который отвечает за определение обработчика для конкретной запрашиваемой страницы, называется маршрутизация. Чаще говорят "роутинг". Посмотрим на пример:
https://ru.hexlet.io/code_reviews/4172
https://ru.hexlet.io/courses/programming-basics
https://ru.hexlet.io/account/profile/edit
Каждый адрес из примера выше представляет собой конкретный маршрут (роут). Причём их можно разделить по типу: статические и динамические.
Статические маршруты
Характеризуются тем, что адрес совпадает с самим маршрутом. Например, account/profile/edit
. Несмотря на то, что адрес один, у разных пользователей он будет отображать разные данные, зависящие от того, кто сейчас авторизован.
Динамические маршруты
А что, если у нас есть адреса, которые обозначают одно и тоже, но содержат параметр. Типичный пример /users/5
. Без особого труда можно понять, что по этой ссылке мы получим информацию о пользователе с номером 5
. Но тогда возникает вопрос: если у нас в базе тысячи пользователей, нам придётся определять тысячи маршрутов?
К счастью, нет, здесь нам на помощь приходят регулярные выражения. Создаётся один маршрут, который выглядит примерно так: ^/users/(\w+)
. А дальше нужно просто сопоставить это регулярное выражение со строкой запроса. Другими словами, мы определили один единственный маршрут, который покрывает подобные ссылки:
/users/4
/users/1234
/users/3
/users/robocop
После этого момента становится понятно, что процесс роутинга – это чуть сложнее, чем просто большой if/switch
. В будущем мы начнём работать с фреймворками, в которых роутинг является одной из основных подсистем. Это справедливо для всех web-фреймворков на всех языках.
Как правило, фреймворки предоставляют более высокоуровневый способ работы с роутами. То есть, вы пишете не сырые регулярные выражения, а строки с плейсхолдерами: /users/:id
. Эти строки внутри заменяются на регулярные выражения и сопоставляются с запрашиваемыми адресами. Плейсхолдеры, в подавляющем большинстве фреймворков, заменяются на группу \w+
. Эта группа не включает в себя /
и требует обязательного наличия хотя бы одного символа. Это значит, что следующие маршруты не подходят под маршрут /users/:id
:
/users
/users/4/photos
/users/my/d
В нашем сервере мы реализовали следующий способ работы роутера. Отдельно описывается объект, содержащий правила роутинга (как ключи) и обработчики маршрутов (как значения):
const router = {
'GET': {
'/': (req, res, matches) => {
/* ... */
},
'/search.json': (req, res, matches) => {
/* ... */
},
},
}
Обратите внимание на важную деталь. Метод http
тоже является частью роутинга. Для GET
- и POST
-запросов на /users
будет использоваться два роута. Это связано с большим количеством причин, одной из которых является семантика http
. А вот параметры запроса (например, /users?name=Tirion
) не являются частью процесса маршрутизации (при таком роутинге, как выше). Они уже используются внутри обработчиков на ваше усмотрение.
Также имеет значение порядок, в котором заданы маршруты. Поэтому первыми должны идти статические и наиболее конкретизированные маршруты, а в конце более общие. Это правило справедливо в том случае, если есть пересечения.
Ещё остаётся работа с ошибками, такими как "страница не найдена". Здесь уже возможны варианты. Некоторые фреймворки обрабатывают эту ситуацию отдельно, проверяя, что ни один маршрут не совпал с запрошенным адресом, и выполняя специальный обработчик для этой ситуации. В других достаточно определить в самом конце маршрут *
, в который попадет всё, что не попало в другие места. И из обработчика этого роута можно делать всё, что нужно для правильного отображения ошибки.
Пример простого способа обрабатывать маршруты:
export default http.createServer((request, response) => {
// извлекаем из адреса часть без query params
const url = new URL(request.url, `http://${request.headers.host}`);
const { pathname } = url;
const routes = router[request.method];
// Обходим маршруты с помощью find,
// чтобы остановиться после того, как маршрут найден
const result = Object.keys(routes).find((str) => {
const regexp = new RegExp(`^${str}$`);
// Проверяем совпадение с маршрутом (записанным в виде регулярного выражения)
const matches = pathname.match(regexp);
// Маршрут не найден, двигаемся дальше
if (!matches) {
return false;
}
// Выполнение обработчика
routes[str](request, response, matches);
return true;
});
// Особая обработка ситуации когда не было найдено соответствующего маршрута
if (!result) {
response.writeHead(404);
response.end();
}
});
Соглашения
В web-разработке существует понятие CRUD
(в русскоязычной среде говорят "круд"), которое расшифровывается как CREATE
, READ
, UPDATE
, DELETE
. И самое простое и базовое, что делают разработчики — это круды. Например, любой административный интерфейс (админка сайта) — это большой набор разных крудов для всевозможных сущностей: круд постов в блог, круд товаров и так далее.
Существуют соглашения о том, как грамотно делать круды с точки зрения маршрутизации. Они включают в себя особенности семантики http
.
GET /users # список
POST /users # создание
GET /users/10 # просмотр
PATCH /users/10 # обновление
DELETE /users/10 # удаление
GET /users/10/photos # список фотографий
Остались вопросы? Задайте их в разделе «Обсуждение»
Вам ответят команда поддержки Хекслета или другие студенты