Перевод серии статей по основам работы с Node.js, написанной Ником Даггером. Оригинал статей находится на tech.pro.
Во второй части этой серии мы научились делать очень простой маршрутизатор, хранить маршруты в json-файле и при запросе сравнивать их с запрашиваемым URL. Это здорово, и даже работает для простых приложений, но что же делать, если мы хотим передать параметры в маршруте? Вы знаете, что есть запросы, содержащие идентификаторы, например, /news/123
.
Однако, прежде чем начать, вернемся к нашему коду и немного почистим его.
Реструктуризация представлений
Для начала, создайте директорию views/
(если вы этого ещё не сделали) и переместите туда HTML-файлы. Это поможет сохранить проект чистым и разделить различные части приложения друг от друга.
Теперь, когда файлы перемещены, необходимо создать класс представления View (внутри файла view.js
) и его статический метод render. Это поможет отделить рендер представлений от обработчиков. Такая идеология называется «разделение проблем» и помогает сохранить читаемость и семантику кода.
'use strict'
class View {
static render(response, path, data) {
}
}
module.exports = View;
Вы можете заметить, что мы передаем объект ответа, путь (или имя) представления, а также любые предназначенные для него данные. Данные будут использованы в четвертой части этой серии, а сейчас давайте просто будем знать о их существовании.
Переместим код из обработчиков в только что созданный метод класса. Это будет выглядеть так: (обратите внимание на передачу пути до используемого шаблона)
'use strict'
let fs = require('fs');
class View {
static render(response, path, data) {
fs.readFile('views/' + path, { encoding: 'utf8' }, function(error, view) {
if (!error) {
response.writeHead(200, { 'Content-Type': 'text/html'});
response.write(view);
response.end();
}
});
}
}
module.exports = View;
Круто, теперь у нас есть класс View
, который рендерит наше представление. Добавим вызов этого модуля в наш обработчик main.js
:
'use strict';
let View = require('../view');
class Main {
static index(response) {
View.render(response, 'index.html');
}
...
}
А сейчас, приступаем к самому интересному!
Параметры в маршруте
Это очень простая часть. Для начала мы должны определить маршруты, имеющие параметры. В файле routes.json
добавим следующий маршрут:
{
"/": {
"handler": "main",
"action": "index"
},
"/foo": {
"handler": "main",
"action": "foo"
},
"/foo/{id}": {
"handler": "main",
"action": "getFoo"
}
}
Я же говорил, что это очень просто. Дальше мы вытащим параметр с помощью регулярных выражений.
Поиск корректного маршрута
У нас есть параметры в маршрутах и обработать их не так-то просто. Ведь здесь не получится просто их перебрать, как мы делали это раньше.
Мы должны сначала проверить содержит ли маршрут параметры и только затем найти правильный маршрут в файле routes.json
.
Во-первых, давайте создадим регулярное выражение, которое вытащит параметры, указанные в маршруте.
let pattern = new RegExp('\{(.*)\}');
И только теперь, когда у нас есть шаблон, мы должны сопоставить его с нашим маршрутом в цикле (чтобы увидеть, есть ли у маршрута параметры):
'use strict';
class Router {
static find(path, routes) {
let pattern = new RegExp('\{(.*)\}');
for (let route in routes) {
if (route.match(pattern)) {
} else if (path === route) {
return routes[route];
}
}
return false;
}
}
module.exports = Router;
Далее нам нужно разделить маршрут до каждого «каталога» и сравнить его с длиной разделенной версии запрашиваемого пути. Это позволит нам узнать совместим ли запрашиваемый путь с тем, что получен в цикле.
...
if (route.match(pattern)) {
route = route.split('/');
if (route.length === path.split('/').length) {
}
}
...
Когда мы знаем, действительно ли маршрут совместим с нашим путем, нам нужно в цикле перебрать массив маршрута (route
) и узнать, какой из «каталогов» является параметром.
...
if (route.match(pattern)) {
route = route.split('/'), path = path.split('/');
if (route.length === path.length) {
for (let i = 0; i < route.length; i++) {
if (route[i].match(pattern)) {
}
}
}
}
...
Наверное, вы уже заметили, что наш код становится грязным. Мы бы могли перенести часть кода в отдельный метод, но сейчас мы просто хотим заставить это работать.
Давайте проанализируем происходящее:
- Мы проверяем, содержит ли маршрут параметры
- Мы проверяем, совместим ли маршрут с запрашиваемым путем
- Мы в цикле перебираем его «каталоги»
- Мы проверяем, является ли «каталог» параметром
До сих пор довольно просто.
А что произойдет, если найдется один параметр? Мы должны сохранить его до конца работы. Для этого создадим объект parameters
выше цикла.
...
if (route.match(pattern)) {
route = route.split('/'), path = path.split('/');
if (route.length === path.length) {
for (let i = 0; i < route.length; i++) {
if (route[i].match(pattern)) {
parameters[route[i].match(pattern).pop()] = path[i];
}
}
}
}
...
Здесь мы используем Array.prototype.pop
, потому что каждое совпадение возвращает массив, но нам нужна лишь его последняя часть, которая и будет использоваться в качестве имени параметра.
Что произойдет, если «каталог» не является параметром, и что, если текущий «каталог» не соответствует пути «каталога»? Первый должен продолжить цикл, а последний каталог должен выйти из него (из-за неправильного совпадения).
...
if (route.match(pattern)) {
route = route.split('/'), path = path.split('/');
if (route.length === path.length) {
let parameters = {};
for (let i = 0; i < route.length; i++) {
if (route[i].match(pattern)) {
parameters[route[i].match(pattern).pop()] = path[i];
} else if (route[i] === path[i]) {
continue;
} else {
break;
}
}
}
}
...
Теперь, когда наш цикл завершился, все еще есть шанс, что все эти вещи были проверены у неправильного маршрута. В данном случае, мы бы не хотели его вернуть, поэтому добавим ещё одну проверку после цикла for
:
if (Object.keys(parameters).length) {
return {
route: routes[route.join('/')],
data: parameters
}
}
Вы также можете заметить, что мы возвращаем объект, содержащий текущий маршрут и извлеченные параметры.
Маршрутизатор должен выглядеть так:
'use strict';
class Router {
static find(path, routes) {
let pattern = new RegExp('\{(.*)\}');
for (let route in routes) {
if (route.match(pattern)) {
route = route.split('/'), path = path.split('/');
if (route.length === path.length) {
let parameters = {};
for (let i = 0; i < route.length; i++) {
if (route[i].match(pattern)) {
parameters[route[i].match(pattern).pop()] = path[i];
} else if (route[i] === path[i]) {
continue;
} else {
break;
}
}
if (Object.keys(parameters).length) {
return {
route: routes[route.join('/')],
data: parameters
}
}
}
} else if (path === route) {
return {
route: routes[route]
}
}
}
return false;
}
}
module.exports = Router;
Давайте резюмировать то, что здесь происходит:
- Сначала мы проходим циклом по всем маршрутам
- Проверяем каждый маршрут на наличие параметров
- Далее мы проверяем, совместим ли маршрут с запрошенным путем
- Если совместим, то в цикле перебираем его «каталоги»
- Если «каталог» — параметр, сохраняем и возвращаем его
На самом деле, не так уж и сложно!
Используем данные в обработчике
У нас есть объект, который мы вернули, но что же нам с ним делать? Давайте вернемся к файлу app.js
и изменим метод createServer
.
Для начала переименуем переменную route
в found
. Далее нужно изменить handler
на require('./handlers/' + found.route.handler)
.
Теперь, когда мы вызываем обработчик действия/метода, мы должны также передавать данные вместе с объектом ответа.
Все вместе это должно выглядеть так:
...
static createServer(settings) {
http.createServer(function(request, response) {
let path = url.parse(request.url).pathname;
let found = Router.find(path, settings.routes);
try {
let handler = require('./handlers/' + found.route.handler);
handler[found.route.action](response, found.data);
} catch(e) {
response.writeHead(500);
response.end();
}
}).listen(settings.port);
}
...
Мы можем получить доступ к нашим параметрам в обработчике. Перейдем к обработчику main.js
и добавим метод getFoo
, на который ссылается routes.json
.
Выведем полученные данные:
class Main {
...
static getFoo(response, data) {
console.log(data) // { id: ___ }
View.render(response, 'foo.html', data);
}
...
}
Вы видите, что мы передаем наши данные в представление, но так как его нет, то ничего не произойдет. Мы будем рассматривать это в следующей части, уже очень скоро.
Сейчас, по крайней мере, мы можем обрабатывать более сложные GET-запросы путем передачи параметров в наши маршруты. Обрабатывать POST-запросы мы будем уже в четвертой части.
GitHub
Для удобства чтения и рассмотрения кода я собрал весь код из статьи в репозиторий, доступный на GitHub по этой ссылке.
Навигация
- Часть 1. Сервер
- Часть 2. Базовая маршрутизация
- Часть 3. Расширенная маршрутизация