Свой веб-сервер на NodeJS, и ни единого фреймворка

от автора

Для многих людей JavaScript ассоциативно связан с обилием разнообразных фреймворков и библиотек. Разумеется, инструменты, которые помогают нам каждый день — это хорошо, но, мне кажется, нужно искать некий баланс между использованием инструментов и прокрастинацией, а также знать, как работают вещи, которыми ты пользуешься. Поэтому, когда я только сел разбираться с NodeJS, мне было особенно интересно написать полноценный веб-сервер, которым я мог бы пользоваться сам.

Новичку в NodeJS действительно может быть нелегко. JS — один из языков, в котором часто не существует единственного правильного решения конкретной задачи, а добавленные в ноду модули для работы с файловой системой, http сервером и прочими вещами, характерными для работы на сервере, затрудняют переход даже тем, кто пишет хороший код для браузеров. Тем не менее, я надеюсь, что вы знаете основы этого языка и его работы в серверном окружении, если нет, советую посмотреть замечательный скринкаст, который поможет разобраться в основах. И последнее — я не претендую на какой-то исключительно правильный код и буду рад услышать критику — мы все учимся, и это отличный способ получать знания.

Начнём с архитектуры

Исходная папка nodejs хранится на сервере по пути /var/www/html/. В ней и будет наш веб-сервер. Дальше всё просто: создаём в ней директорию routing, в которой будет лежать наш скрипт index.js, а также 4 папки — dynamic, static, nopage и main — для динамически генерируемых страниц, статики, страницы 404 и главной страницы. Выглядит всё это так:

nodejs
—routing
—-dynamic
—-nopage
—-static
—-main
—-index.js

Создаём наш сервер

Отлично, с архитектурой более-менее определились. Теперь создаём в исходной папке файл server.js со следующим содержимым:

// server.js // Для начала установим зависимости. const http = require('http'); const routing = require('./routing');   let server = new http.Server(function(req, res) {   // API сервера будет принимать только POST-запросы и только JSON, так что записываем   // всю нашу полученную информацию в переменную jsonString   var jsonString = '';   res.setHeader('Content-Type', 'application/json');   req.on('data', (data) => { // Пришла информация - записали.       jsonString += data;   });    req.on('end', () => {// Информации больше нет - передаём её дальше.       routing.define(req, res, jsonString); // Функцию define мы ещё не создали.   }); }); server.listen(8000, 'localhost'); 

Здорово! Теперь наш сервер будет принимать запросы, записывать JSON-данные, если они есть, но пока что будет вылетать с ошибкой, потому что у нас нет функции define в /routing/index.js. Время это исправить.

// /routing/index.js const define = function(req, res, postData) {   res.end('Hello, Habrahabr!'); } exports.define = define; 

Запускаем наш сервер:

node server.js

Заходим туда, где он слушает запросы. Если вы не меняли код, это будет localhost:8000. Ура. Ответ есть.

image
Замечательно. Только это не совсем то, что нам нужно от сервера, правда?

Ловим запросы к нашим API

Да, мы получили ответ, но пока что не слишком близки к конечной цели. Самое время писать логику для нашего роутера.

// /routing/index.js // Для начала установим зависимости. const url = require('url'); const fs = require('fs');  const define = function(req, res, postData) {   // Теперь получаем наш адрес. Если мы переходим на localhost:3000/test, то path будет 'test'   const urlParsed = url.parse(req.url, true);     let path = urlParsed.pathname.substr(1);      // Теперь записываем полный путь к server.js. Мне это особенно нужно, так как сервер будет     // висеть в systemd, и путь, о котором он будет думать, будет /etc/systemd/system/...     prePath = `/var/www/html/nodejs/`;     try {       // Здесь мы пытаемся подключить модуль по ссылке. Если мы переходим на       // localhost:8000/api, то скрипт идёт по пути /routing/dynamic/api, и, если находит там       // index.js, берет его. Я знаю, что использовать тут try/catch не слишком правильно, и потом       // переделаю через fs.readFile, но пока у вас не загруженный проект, разницу в скорости       // вы не заметите.       let dynPath = './dynamic/' + path;       let routeDestination = require(dynPath);       res.end('We have API!');     }     catch (err) {       // Не нашлось api? Грустно.       res.end("We don't have API!");     } }; exports.define = define; 

Готово. Теперь мы можем создать /routing/dynamic/api, и протестировать то, что у нас есть. Я воспользуюсь для этих целей своим готовым скриптом по адресу /dm/shortenUrl.

image

Определяем, есть ли страница

Мы научились находить скрипты, теперь нужно научиться находить статику. Первым делом пойдём в /routing/nopage и создадим там index.html. Просто создайте костяк html-страницы, и сделайте один-единственный заголовок h1 с текстом: «404». После этого возвращаемся в /routing/index.js, но теперь мы сосредоточимся на уже написанном блоке catch:

// /routing/index.js: блок catch catch (err) {       // Находим наш путь к статическому файлу и пытаемся его прочитать.       // Если вы не знаете, что это за '=>', тогда прочитайте про стрелочные функции в es6,       // очень крутая штука.       let filePath = prePath+'routing/static/'+path+'/index.html';       fs.readFile(filePath, 'utf-8', (err, html) => {         // Если не находим файл, пытаемся загрузить нашу страницу 404 и отдать её.         // Если находим — отдаём, народ ликует и устраивает пир во имя царя-батюшки.         if(err) {           let nopath = '/var/www/html/nodejs/routing/nopage/index.html';           fs.readFile(nopath, (err , html) => {             if(!err) {               res.writeHead(200, {'Content-Type': 'text/html'});               res.end(html);             }             // На всякий случай напишем что-то в этом духе, мало ли, иногда можно случайно             // удалить что-нибудь и не заметить, но пользователи обязательно заметят.             else{               let text = "Something went wrong. Please contact webmaster@forgetable.ru";               res.end(text);             }           });         }         else{           // Нашли файл, отдали, страница загружается.           res.writeHead(200, {'Content-Type': 'text/html'});           res.end(html);         }       });     } 

Воодушевляет. Теперь мы можем отдавать страницу 404, а так же html-страницы, которые мы добавляем сами в /routing/static. В моём случае страница 404 выглядит так:

image

Пара слов об API

Способ организации скриптов — личное дело каждого. На данный момент код в блоке try у меня такой:

let dynPath = './dynamic/' + path;       let routeDestination = require(dynPath);       routeDestination.promise(res,postData,req).then(         result => {           res.writeHead(200);           res.end(result);           return;         },         error => {           let endMessage = {};           endMessage.error = 1;           endMessage.errorName = error;           res.end(JSON.stringify(endMessage));           return;         }       ); 

По сути, все мои скрипты представляют собой функцию, которая передаёт замыканиями параметры и возвращает промис, и дальнейшая логика на промисах и завязана. В данном контексте такой подход кажется мне очень удобным, так отлов ошибок становится очень лёгким делом. Уже можно, в принципе, переписать эту логику на async / await, но особого смысла для себя я в этом не вижу.

Обрабатываем запросы браузера

Теперь мы уже можем пользоваться нашим сервером, и он будет возвращать страницы. Однако, если вы поместите в /routing/static/somepage ту же страницу, которая прекрасно работает, например, на апаче, вы столкнётесь с некоторыми проблемами.

Во-первых, для этого веб-сервера, как и для, наверное, всех в таком роде, нужно иначе задавать ссылки на css/js/img/… файлы. Если вам хочется подключить к странице 404 css-файл и сделать её красивой, то в случае с апачем мы создали бы в той же папке nopage файл style.css и подключили бы его, указав в тэге link следующее: ‘href=«style.css»’. Однако, теперь нам нужно писать путь иначе, а именно: "/routing/nopage/style.css".

Во-вторых, даже если мы подключим всё правильно, то ничего не произойдёт, и у нас всё ещё будет голая страница html. И вот тут мы подходим к самой последней части сегодняшней статьи — дополним скрипт, чтобы он ловил и обрабатывал запросы, которые браузер отправляет сам, читая разметку html. Ну и про favicon не забудем — возьмите фавиконку и положите её в корневую директорию нашего сервера.

Итак, переходим опять в /routing/index.js. Теперь мы будем писать код прямо перед try/catch:

// До этого мы уже получили path и prePath. Теперь осталось понять, какие запросы // мы получаем. Отсеиваем все запросы по точке, так чтобы туда попали только запросы к // файлам, например: style.css, test.js, song.mp3 if(/[.]/.test(path)) {       if(path == 'favicon.ico') {         // Если нужна фавиконка - возвращаем её, путь для неё всегда будет 'favicon.ico'         // Получается, если добавить в начале prePath, будет: '/var/www/html/nodejs/favicon.ico'.         // Не забываем про return, чтобы сервер даже не пытался искать файлы дальше.         let readStream = fs.createReadStream(prePath+path);         readStream.pipe(res);         return;       }       else{         // А вот если у нас не иконка, то нам нужно понять, что это за файл, и сделать нужную         // запись в res.head, чтобы браузер понял, что он получил именно то, что и ожидал.         // На данный момент мне нужны css, js и mp3 от сервера, так что я заполнил только         // эти случаи, но, на самом деле, стоит написать отдельный модуль для          if(/.mp3/.test(path)) {           res.writeHead(200, {             'Content-Type': 'audio/mpeg'           });         }         else if(/.css/.test(path)) {           res.writeHead(200, {             'Content-Type': 'text/css'           });         }         else if(/.js/.test(path)) {           res.writeHead(200, {             'Content-Type': 'application/javascript'           });         }         // Опять же-таки, отдаём потом серверу и пишем return, чтобы он не шёл дальше.         let readStream = fs.createReadStream(prePath+path);         readStream.pipe(res);         return;       }     } 

Фух. Всё готово. Теперь можно подключить наш css-файл и увидеть нашу страницу 404 со всеми стилями:

image

Выводы

Ура! Мы сделали свой веб-сервер, который работает, и работает хорошо. Разумеется, это только начало работы над приложением, но самое главное уже готово — на таком веб-сервере можно поднимать любые страницы, он справляется и со статикой, и с динамическим контентом, и роутинг, на мой взгляд, выглядит удобно — достаточно просто положить соответствующий файл в static или dynamic, и он тут же подхватится, и не надо писать роутинг для каждого конкретного случая.

В общем и целом, работой сервера я очень даже доволен, и сейчас отказался от апача в сторону этого решения, и в целом это был очень интересный опыт. Большое спасибо, что ты, читатель, разделил его со мной, дойдя до этого момента.
ссылка на оригинал статьи https://habrahabr.ru/post/327440/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *