Express  —  популярный фреймворк Node.js, который значительно упрощает разработку и поддержку приложений. Express позволяет быстро создавать обработчики маршрутов на основе HTTP-методов и URL-адресов. Чтобы понять, как это работает, заглянем в “закулисье” Express-маршрутизации.

Пример с “Hello World”

Для начала рассмотрим пример простейшего приложения на основе Express.

const express = require('express') 
const app = express() 

app.get('/', (req, res) => { 
    res.send('Hello World!') 
}) 

app.listen(3000, () => { 
    console.log('Example app listening on port 3000') 
})

Приведенный код создаст новый HTTP-сервер, прослушивающий запросы на порту 3000. Когда сервер получит GET-запрос на URL “/”, он вернет ответ “Hello World!”.

Приложение Express

После вызова express() мы вычисляем функцию createApplication(), создающую новый объект app. Посмотрим, как она работает.

Во 2-й строке app определяется как анонимная функция, которая обертывает handle(req, res, next). Эта функция будет вызываться при каждом запросе, который получит сервер.

В 7-й строке объект app объединяется с Express-методами, такими как use(), listen(), get() и т.д.

Важным свойством объекта app является свойство Router, которое отвечает за то, какие методы будут вызываться, когда сервер получит новый запрос.

Прослушивание запросов

Если погрузиться глубже, то увидим, что Express, как правило, использует http-модуль Node.js.

При вызове app.listen() создается новый http-сервер, аргумент this  —  это функция, которая будет вызываться при каждом новом http-запросе. Это та же анонимная функция, что была создана в createApplication(). Она просто обертывает метод app.handle().

Как работает Router

Объект Router имеет стек “слоев”, каждый из которых снабжен методом handle и свойством path.

Вот эти слои.

  • Middleware (промежуточная обработка)  —  функция, имеющая сигнатуру function(req, res, next) и обладающая логикой, способной изменять req и res и в конечном итоге вызывать next()  —  для вызова следующего слоя  —  или использовать send() для возврата ответа. При передаче массива функций каждая функция будет отдельным слоем.
  • Route  —  HTTP-метод (GET, POST и т.д.) и функция с той же сигнатурой, что и функция промежуточной обработки. Функция будет срабатывать только для запроса с тем же HTTP-методом. Также можно определить route, который будет обрабатывать все HTTP-методы с помощью .all(). В случае передачи массива функций, route сохраняет заданные функции в виде стека и каждый вызов next() вызывает следующую функцию.
  • Router  —  мы можем создать новый объект router (который также будет содержать собственный стек слоев). Router можно рассматривать как “мини-приложение”, которое в конечном итоге может вызывать функции промежуточной обработки и функции route или даже другой объект router.

Подбор релевантного пути

Как Express определяет, какой слой является релевантным для конкретного запроса?

Express использует библиотеку под названием path-to-regexp для сопоставления маршрутов запроса с определенным путем. Путь может быть строкой, строковым шаблоном и регулярным выражением.

Вот несколько примеров сопоставления путей.

  1. Путь, который будет сопоставлять запрос с /index.html.
app.get('/index.html', (req, res) => {
res.send('index');
});

2. Путь, который будет сопоставлять запросы с /abc и /ac.

app.get('/ab?c', (req, res) => {
res.send('ab?c');
});

3. Путь, который будет сопоставлять запросы с abc, abbbc и так далее.

app.get('/ab+c', (req, res) => {
res.send('ab+c');
});

4. Путь, который будет сопоставлять запросы с axxxb, `aRANDOMb и так далее.

app.get('/a*b', (req, res) => {
res.send('a*b');
});

Более подробную информацию и примеры можно найти в документации path-to-regexp.

Обработка запроса

После ознакомления с базовой структурой маршрутизации Express, разберемся с тем, что происходит, когда сервер получает запрос.

Как уже говорилось, при создании нового приложения в Express и вызове метода app.listen() создается http-сервер, который будет вызывать app.handle() для каждого нового http-запроса.

  1. При получении нового запроса вызывается метод app.handle().
  2. Метод app.handle() выполняет метод handle корневого объекта router.
  3. Метод handle будет перебирать слои объекта router, пока не найдет подходящий слой с помощью path, и вызывает метод handle этого слоя. Если слой является объектом router, функция handle выполнит цикл по нему.
  4. Если будет вызван обратный вызов next(), то будет произведен поиск следующего подходящего слоя и его обработка. Процесс остановится, когда клиенту будет отправлен ответ.

Вот пример маршрутизации, объекта router и функции промежуточной обработки (middleware):

Заключение

Это был краткий обзор маршрутизации Express, показывающий, как фреймворк понимает, какая функция должна обрабатывать тот или иной запрос. Для более глубокого погружения в тему стоит ознакомиться с документацией по Express и детально изучить исходный код.

Читайте также:

Читайте нас в TelegramVK и Дзен


Перевод статьи Shay Pepper: How Express Routing Works

Предыдущая статьяПошаговое руководство по NLP: конструирование признаков текстовых данных
Следующая статья20 продвинутых проектов для освоения сложных концепций программирования