Перевод серии статей по основам работы с 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 по этой ссылке.

Навигация