Перевод серии статей по основам работы с Node.js, написанной Ником Даггером. Оригинал статей находится на tech.pro.
В первой части серии статей, мы исследовали возможности Node и то, как создать базовый сервер, обслуживающий один HTML-файл.
Да, это классно, но это не делает нас крутыми — мы застряли на странице с одним статичным видом. Прежде чем начать разбираться с маршрутизацией, давайте обратимся к коду из первой части.
Реструктуризация сервера
Код, написанный в первой части очень простой. Но что, если мы хотим, чтобы выполнялись какие-либо действия до запуска сервера (например, получение маршрутов)? Давайте реструктуризируем наш предыдущий код в класс и добавим пару методов:
'use strict';
let http = require('http'),
fs = require('fs');
class Server {
static start(port) {
this.createServer(port);
}
static createServer(port) {
http.createServer(function(request, response) {
}).listen(port);
}
}
Server.start(80);
Как вы можете заметить, мы используем тот же код, чтобы запустить сервер, но теперь он представлен структурированным классом со своими статическими методами.
Хранение маршрутов
Следующим шагом будет создание файла routes.json
и его структуры. В этом файле будут храниться данные, в зависимости от которых сервер будет отвечать на запросы.
{
"/": {
"handler": "main",
"action": "index"
}
}
Первое свойство родительского объекта — это путь. Внутри пути находятся имена обработчика и метода, которые вы хотите вызвать.
Теперь, когда у нас есть файл маршрутов, нужно прочесть его перед тем, как создать сервер. Для этого мы снова используем модуль для работы с файловой системой.
Обещания (Promises)
Если вы помните, метод readFile
является асинхронным, поэтому нам нужен способ создать сервер после того, как мы прочитаем файл маршрутов. Этого можно достичь с помощью обещаний (promise). Добавим ещё один метод и назовём его getRoutes
, а затем вернем обещание:
'use strict';
let http = require('http'),
fs = require('fs');
class Server {
static start(port) {
this.createServer(port);
}
static getRoutes() {
return new Promise(function(resolve) {
});
}
static createServer(port) {
http.createServer(function(request, response) {
}).listen(port);
}
}
Server.start(80);
После того, как завершится асинхронный режим работы, мы разрешаем (resolve) наше обещание, проходящее через объект, содержащий маршруты и порт. Это позволит нам связать в цепочку getRoutes
и createServer
, которая будет выглядеть так:
this.getRoutes(port).then(this.createServer);
Реализуем чтение из файла с помощью readFile
:
'use strict';
let http = require('http'),
fs = require('fs');
class Server {
static start(port) {
this.getRoutes(port).then(this.createServer);
}
static getRoutes(port) {
return new Promise(function(resolve) {
fs.readFile('routes.json', { encoding: 'utf8' }, function(error, routes) {
if (!error) {
resolve({
port: port,
routes: JSON.parse(routes)
});
}
});
});
}
static createServer(settings) {
http.createServer(function(request, response) {
}).listen(settings.port);
}
}
Server.start(80);
Вы можете заметить, что я изменил имя аргумента у createServer
на settings
, так как теперь это объект, содержащий маршруты и порт.
Куда мы идем?
Ладно, теперь у нас есть маршруты, и мы должны с ними что-то делать. Сейчас мы займемся проверкой запрошенного URL на наличие в нашем списке маршрутов. Для этого нам понадобится раcпарсить URL и получить pathname
.
Подключаем модуль url
в начале файла (так же, как и с предыдущими модулями) и добавляем следующую строку внутри функции обратного вызова у http.createServer
:
let path = url.parse(request.url).pathname;
Ну, сейчас мы знаем куда хочет пойти браузер, но как мы можем попросить сервер отвести нас туда? Для этого у нас есть маршруты. Создадим маршрутизатор и проверим запрошенный путь.
Поиск маршрута
Создайте файл router.js
. В начале этого файла создайте класс Router
и добавьте туда статический метод find:
'use strict';
class Router {
static find(path, routes) {
}
}
module.exports = Router;
Так как мы строим очень простой маршрутизатор, теперь, все что нам нужно — это перебрать все маршруты и сравнить их с тем, что запросил браузер.
'use strict';
class Router {
static find(path, routes) {
for (let route in routes) {
if (path === route) return routes[route];
}
return false;
}
}
module.exports = Router;
Здесь мы возвращаем маршрут, конечно, если он существует. Теперь нужно использовать маршрутизатор на нашем сервере по требованию обработчика при выполнении действий или методов. Я обещаю, что это легко!
Выполнение маршрута
Вернемся назад к файлу app.js
, в котором мы должны подключить наш маршрутизатор и научить сервер искать маршрут:
'use strict';
let http = require('http'),
fs = require('fs'),
url = require('url');
let Router = require('./router');
class Server {
...
static createServer(settings) {
http.createServer(function(request, response) {
let path = url.parse(request.url).pathname;
let route = Router.find(path, settings.routes);
try {
} catch(e) {
}
}).listen(settings.port);
}
}
Хорошо, мы нашли свой маршрут. Также, мы должны добавить блок try... catch
, так как то, что мы делаем, может выбросить исключение.
Обработка запроса
Первое, что необходимо сделать — это завести директорию handlers/
и внутри неё создать файл main.js
.
Новый обработчик должен выглядеть следующим образом:
'use strict';
class Main {
static index(response) {
}
}
module.exports = Main;
Теперь вы видите, как это соотносится с нашим маршрутизатором. Поле «обработчик» в нашем JSON-файле ссылается на имя JS-файла и действие, соответствующее статическому методу внутри нашего класса.
Для того, чтобы соблюсти баланс простоты, давайте просто обратимся к коду из первой части и ответим на запрос файлом index.html
.
'use strict';
let fs = require('fs');
class Main {
static index(response) {
fs.readFile('index.html', { encoding: 'utf8' }, function(error, view) {
if (!error) {
response.writeHead(200, { 'Content-Type': 'text/html'});
response.write(view);
response.end();
}
});
}
}
module.exports = Main;
Связываем все вместе
Сейчас, когда у нас есть обработчик, пишущий ответ на запрос, нам нужен сервер, который бы его использовал.
'use strict';
let http = require('http'),
fs = require('fs'),
url = require('url');
let Router = require('./router');
class Server {
...
static createServer(settings) {
http.createServer(function(request, response) {
let path = url.parse(request.url).pathname;
let route = Router.find(path, settings.routes);
try {
let handler = require('./handlers/' + route.handler);
handler[route.action](response);
} catch(e) {
response.writeHead(500);
response.end();
}
}).listen(settings.port);
}
}
Великолепно! Теперь у нас есть сервер, перестроенный в класс, а также маршрутизатор, использующий запрошенный URL-адрес для поиска маршрута. После того, как маршрут найден, мы пытаемся подключить указанный в routes.json
обработчик и вызвать метод, передав объект ответа.
Добавим ещё один маршрут в файл routes.json
, создадим новый HTML-файл и новый метод внутри файла main.js
.
Все вместе это будет выглядеть следующим образом:
routes.json:
{
"/": {
"handler": "main",
"action": "index"
},
"/foo": {
"handler": "main",
"action": "foo"
}
}
main.js:
'use strict';
let fs = require('fs');
class Main {
static index(response) {
fs.readFile('index.html', { encoding: 'utf8' }, function(error, view) {
if (!error) {
response.writeHead(200, { 'Content-Type': 'text/html'});
response.write(view);
response.end();
}
});
}
static foo(response) {
fs.readFile('foo.html', { encoding: 'utf8' }, function(error, view) {
if (!error) {
response.writeHead(200, { 'Content-Type': 'text/html'});
response.write(view);
response.end();
}
});
}
}
module.exports = Main;
Мы должны быть очень горды своей работой, но лишь до тех пор, пока мы не захотим передать параметры в наш маршрут, например, идентификатор записи в блоге. В следующей части этой серии статей мы научим наш маршрутизатор работать с передаваемыми параметрами и расширим его.
GitHub
Для удобства чтения и рассмотрения кода я собрал весь код из статьи в репозиторий, доступный на GitHub по этой ссылке.
Навигация
- Часть 1. Сервер
- Часть 2. Базовая маршрутизация
- Часть 3. Расширенная маршрутизация