JavaScript | Маршрутизация URL | ЧПУ-адреса

JavaScript | Маршрутизация URL | ЧПУ-адреса

Как обрабатывать HTTP/HTTPS запросы силами JavaScript на сервере? Как сделать ЧПУ при помощи JavaScript? Как понять куда кликнул пользователь, чтобы передать ему правильную разметку в браузер? Как отлавливать URL-адреса и указывать программе какой файл нужно передать с сервера на клиент?

Этими вопросами задаётся любой JavaScript’изёр, который наигрался в генерацию разметки и работы с DOM. Этому человеку уже понятны принципы формирования контента при помощи языка JavaScript — фронтэнда. Теперь он хочет использовать язык в вопросах серверной стороны — бекэнда.

Что делать? На все вопросы можно ответить одним — Node.js

 

Node.js и серверная сторона

Для решения нашей задачи нам нужно иметь:

  1. Физический сервер (ПК в компании, или выделенный виртуальный сервер от хостинг-провайдера)
  2. Сервер Node.js
  3. Пакетный менеджер NPM для Node.js
  4. PM2 — это демон-менеджер процессов, который поможет вам управлять вашим приложением и поддерживать его в сети. Он нужен для того, чтобы держать активные сервера приложений, иначе любой выход из консоли Node будет закрывать работу приложения
  5. Домен, на котором будет обрабатываться маршрутизация URL-адресов (сделаем поддомен url-routing.efim360.ru)
  6. Веб-сервер, который будет принимать запросы от маршрутизации (в нашем случае NGINX)
  7. Стартовая страница с контентом и ссылками на другие страницы
  8. Шаблоны страниц, куда будут осуществляться переходы с главной

 

Шаг 1 — Переносим готовые файлы в директорию сайта на сервере

Начинаем с самого простого и двигаемся к сложному. Всего будет 3 файла: index.html ; aa.html ; zz.html

Переносим файл index.html:

<!DOCTYPE html>
<html lang="ru">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>url-routing.efim360.ru</title>
</head>
<body>
   <h1>Главная страница</h1>
   <p><a href="/aa">Страница AA</a> <a href="/zz">Страница ZZ</a></p>
</body>
</html>

В индексном файле есть две ссылки на два адреса. Мы хотим сделать так, чтобы при нажатии на ссылку открывалась новая страница сайта с новым адресом и контентом. Мы хотим, чтобы сервер каким-то образом сопоставил URL-адрес, проверил его по условию, нашёл контент и вернул его пользователю силами JavaScript.

 

Переносим файл aa.html:

<!DOCTYPE html>
<html lang="ru">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Страница AA</title>
</head>
<body>
   <h1>Страница AA</h1>
   <a href="/">Перейти на Главную</a>
</body>
</html>

Переносим файл zz.html:

<!DOCTYPE html>
<html lang="ru">
<head>
   <meta charset="UTF-8">
   <meta name="viewport" content="width=device-width, initial-scale=1.0">
   <title>Страница ZZ</title>
</head>
<body>
   <h1>Страница ZZ</h1>
   <a href="/">Перейти на Главную</a>
</body>
</html>

3 файла на ПК:

Три файла для маршрутизации URL - JavaScript
Три файла для маршрутизации URL — JavaScript

3 файла на сервере:

Три файла на сервере
Три файла на сервере

 

Шаг 2 — На всякий случай проверяем всё ли установлено на сервере Unix(Debian)

Устанавливаем на сервер Node.js:

apt-get install nodejs

Устанавливаем на сервер менеджер пакетов NPM для Node.js:

apt-get install npm

Устанавливаем на сервер демон PM2 для работы с приложениями Node.js:

npm install pm2 -g

Устанавливаем на сервер —  веб-сервер NGINX, который будет пробрасывать запросы на наш сервер JS (который мы напишем)

apt-get install nginx-full

 

Шаг 3 — Проверяем версии установленных программ

Проверяем версию Node.js:

node -v

Проверяем версию NPM:

npm -v

Проверяем версию PM2:

pm2 -v

Проверяем версию NGINX:

nginx -v

 

Шаг 4 — создаём конфигурационный файл для нового виртуального хоста

В моём случае нужно будет пробрасывать все запросы, приходящие на NGINX. То есть первым встречает запрос NGINX, а вторым встречает запрос наш собственный сервер — app.js (тоже файл конфигурации)

Конфиг виртуального хоста NGINX:

server {
   listen 192.168.0.120:80;

   server_name url-routing.efim360.ru;
   root /var/www/url-routing.efim360.ru;

   index index.html index.htm;

   location / {
      proxy_pass http://192.168.0.120:8008;
      proxy_set_header Host $host;
   }
}

Мы слушаем 80 порт по адресу 192.168.0.120. То есть запросы, которые придут на 192.168.0.120:80 будут проанализированы на ИМЯ_СЕРВЕРА. Если заголовки запроса будут содержать url-routing.efim360.ru, то любой такой запрос будет проброшен на 8008 порт. На порту 8008 будут работать наш NODEJS сервер. Мы будем пробрасывать по http т.к. это внутренняя система виртуализации и нам нет смысла на этом этапе подключать SSL для HTTPS. Это касается только пересылки с 80 на 8008.

 

Я использую виртуальзацию, поэтому явно указываю локальный IP-адрес устройства, на котором работает NGINX.

Переносим на сервер в папку:

/etc/nginx/sites-available

Вид на хосте

Базовый конфиг вирт хоста NGINX на хосте
Базовый конфиг вирт хоста NGINX на хосте

 

Шаг 5 — подключаем конфиг к активным хостам NGINX

ln -s /etc/nginx/sites-available/url-routing.efim360.ru.conf /etc/nginx/sites-enabled/url-routing.efim360.ru.conf

Проверяем конфиг на ошибки:

nginx -t

Перезапускаем веб-сервер NGINX:

service nginx reload

Проверяем работоспособность домена. Открываем браузер и переходим по адресу url-routing.efim360.ru

Сертификат недействительный
Сертификат недействительный

Браузер не пускает отрисовать главную страницу сайта т. к. веб-сервер посылает специальные заголовки. Сейчас нельзя открыть url-routing.efim360.ru, поскольку сайт использует HSTS (HTTP Strict Transport Security).

NGINX так настроен специально — это механизм, принудительно активирующий защищённое соединение через протокол HTTPS.

add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;

 

Шаг 6 — подключение SSL сертификата от LetsEncrypt, чтобы сайт запустился по HTTPS

Теперь нужно «оживить» сайт и получить первые данные от главной страницы по адресу url-routing.efim360.ru.

Я использую CertBot. Ввожу простую команду для создания сертификата для нового домена на Debian:

certbot --nginx

Данная команда автоматически отредактирует конфигурационный файл виртуального хоста и пропишет 443 порт для HTTPS.

Запуск CertBot на Debian для NGINX
Запуск CertBot на Debian для NGINX

Числом 13 указываю нужный домен, для которого будут созданы сертификаты.

Запрет на работу по HTTP - проброс на HTTPS
Запрет на работу по HTTP — проброс на HTTPS

Числом 2 указываю, что все запросы будут обрабатываться только по HTTPS.

Успешное создание SSL-сертификата для домена
Успешное создание SSL-сертификата для домена

Снова проверяем домен в браузере:

Ошибка 502 nginx - Bad Gateway
Ошибка 502 nginx — Bad Gateway

 

Уже лучше. Сайт понемногу оживает. Мы получили первый ответ с кодом ошибки 502 — Bad Gateway. Что нам это даёт? Теперь мы понимаем, что наш запрос был успешно принят веб-сервером NGINX, который попытался пробросить запрос на 8008 порт. Но т.к. этот порт никто пока не слушает мы получили ошибку сервера. Ещё мы понимаем, что сайт начал работу по протоколу HTTPS — значит сертификат правильно установился в автоматическом режиме.

Теперь предлагаю посмотреть как CertBot изменил наш конфиг виртуального хоста.

server {

   server_name url-routing.efim360.ru;
   root /var/www/url-routing.efim360.ru;

   index index.html index.htm;

   location / {
      proxy_pass http://192.168.0.120:8008;
      proxy_set_header Host $host;
   }

   listen 443 ssl; # managed by Certbot
   ssl_certificate /etc/letsencrypt/live/url-routing.efim360.ru/fullchain.pem; # managed by Certbot
   ssl_certificate_key /etc/letsencrypt/live/url-routing.efim360.ru/privkey.pem; # managed by Certbot
   include /etc/letsencrypt/options-ssl-nginx.conf; # managed by Certbot
   ssl_dhparam /etc/letsencrypt/ssl-dhparams.pem; # managed by Certbot

}server {
   if ($host = url-routing.efim360.ru) {
      return 301 https://$host$request_uri;
   } # managed by Certbot

   listen 192.168.0.120:80;

   server_name url-routing.efim360.ru;
   return 404; # managed by Certbot

}

Certbot оставил комментарии там, где он внёс правки.

 

Шаг 7 — Создание простейшего сервера на Node.js

Для начала нам нужно будет понять, что Node.js вообще работает. Для этого мы создадим простейший сервер на Node.js. За основу возьмём пример на сайте стандарта — https://nodejs.org/en/about/

Файл app.js:

const http = require('http');

const hostname = '192.168.0.120';
const port = 8008;

const server = http.createServer((req, res) => {
   res.statusCode = 200;
   res.setHeader('Content-Type', 'text/plain; charset=utf-8');
   res.end(`Сайт url-routing.efim360.ru работает`);
});

server.listen(port, hostname, () => {
   console.log(`Сервер запущен на http://${hostname}:${port}/`);
});

Мы подключаем объект http. Задаём IP-адрес и порт, на котором будет работать наш JavaScript-сервер. Создаём сам сервер. На любой запрос будем отвечать строкой в браузере — «Сайт url-routing.efim360.ru работает»

Простой сервер на Node.js
Простой сервер на Node.js

Помещаем этот файл на хост в папку сайта:

Файл сервер Node.js на хосте
Файл сервер Node.js на хосте

Шаг 8 — Подключение простейшего сервера на Node.js

Переходим в директорию сайта. Запускаем сервер при помощи демона PM2:

cd /var/www/url-routing.efim360.ru

pm2 start app.js

Скрин успешного запуска сервера на Node.js

PM2 успешно запустил сервер Node.js
PM2 успешно запустил сервер Node.js

Проверяем работу нового сервера в браузере:

Простой Node.js сервер работает
Простой Node.js сервер работает

Отлично! Теперь мы знаем, что наш простейший Node.js сервер работает. То есть NGINX успешно передал запрос пользователя на app.js (сервер), который успешно подготовил ответ для пользователя и передал его.

Сейчас сервер работает таким образом, что любой заброс будет возвращать одинаковый ответ. Примеры

Запрос page1
Запрос page1
Запрос page800
Запрос page800

 

И вот теперь мы можем подойти к решению самого основного задания.

 

Шаг 9 — Сервер на Node.js с маршрутизацией URL

Мы уверены в работе нашего сервера. Мы проверили его работоспособность на простом примере конфигурационного файла и теперь можем смело переписать наше приложение (наш конфиг — Node.js сервер). Мы напишем правила обработки запросов пользователей:

// Подключение модулей
   const http = require('http');
   const fs = require('fs')
// АйПи адрес и Порт для прослушивания отдельными переменными
   const hostname = '192.168.0.120';
   const port = 8008;
// Создание сервера
   const server = http.createServer((req, res) => {
      if(req.url === '/'){ // отлавливаем запрос главной страницы
         var mainPage = fs.createReadStream(__dirname + '/pages/index.html', 'utf8')
         res.statusCode = 200;
         res.setHeader('Content-Type', 'text/html');
         mainPage.pipe(res);
      }else if(req.url === '/aa'){ // отлавливаем запрос страницы aa
         var aaPage = fs.createReadStream(__dirname + '/pages/aa.html', 'utf8')
         res.statusCode = 200;
         res.setHeader('Content-Type', 'text/html');
         aaPage.pipe(res);
      }else if(req.url === '/zz'){ // отлавливаем запрос страницы zz
         var zzPage = fs.createReadStream(__dirname + '/pages/zz.html', 'utf8')
         res.statusCode = 200;
         res.setHeader('Content-Type', 'text/html');
         zzPage.pipe(res);
      }
   });

   server.listen(port, hostname, () => {
      console.log(`Сервер запущен на http://${hostname}:${port}/`);
   });

 

Скрин из редактора:

Сервер Node.js с маршрутизацией URL
Сервер Node.js с маршрутизацией URL

Сперва мы подключили модуль для работы с файловой системой:

const fs = require('fs')

Затем мы написали условие для обработки запроса главной страницы:

if(req.url === '/'){ // отлавливаем запрос главной страницы
   var mainPage = fs.createReadStream(__dirname + '/pages/index.html', 'utf8')
   res.statusCode = 200;
   res.setHeader('Content-Type', 'text/html');
   mainPage.pipe(res);
}

Условие (req.url === ‘/’). Строгое сравнение. Мы сравнили путь запроса пользователя на наличие одного символа косой черты, что означает главную страницу сайта. Для таких запросов мы обращаемся к файловой системе и создаём «поток чтения» из файла на сервере, который имеет «серверный путь» в директории сайта — «/page/index.html»

Этот «поток чтения» передаётся пользователю кусочками (чанками — chanks). То есть наш файл главной страницы на сервере будет отправлен частями пользователю. Если у пользователя быстрое соединение, то он практически не увидит «кусочной» загрузки, а сразу увидит главную страницу сайта url-routing.efim360.ru

Аналогичным образом обрабатываются запросы страниц «aa» и «zz». Запрос попадает на наш JavaScript-сервер, а затем отбирается нужная страница по URI-путю.

Обновляем файл на хосте (перезаписываем его). Для запуска обновлённого сервера нужно перезапустить файл в демоне PM2:

cd /var/www/url-routing.efim360.ru

pm2 restart app.js

Скрин из консоли операционной системы:

Перезапускаем наш сервер на Node.js
Перезапускаем наш сервер на Node.js

Проверяем работу сайта:

url-routing.efim360.ru

Скрин главной страницы

Главная страница
Главная страница

Супер! Мы смогли обработать наш первый запрос пользователя, который обратился к главной странице. На хосте в основной директории сайта не существует статического файла с расширением html, а значит NGINX никак не мог бы отдать файл index.html

Проверяем работу клика на страницу AA:

Страница AA
Страница AA

Кликаем «Перейти на Главную». Проверяем работу клика на страницу ZZ:

Страница ZZ
Страница ZZ

Переходы идеально отработали. Мы наблюдаем смену URL-адресов в адресной строке браузера.

 

Итог

Мы успешно выполнили поставленную задачу. Мы научились обрабатывать ЧПУ (Человеко Понятные Урлы) силами языка JavaScript и серверной стороной на Node.js.

На простом примере мы разобрались с темой маршрутизации URL. Теперь мы знаем, каким образом можно отлавливать нужный адрес в строке браузера и отдавать под него целевой контент.

 

Что дальше?

Это был простой пример. И я очень надеюсь, что вы всё поняли. Он не учитывает различные ошибки сервера. Если сейчас обратиться к «несуществующей» странице (адресу), то браузер надолго «задумается» и будет крутить круглую стрелочку. Наш текущий сервер никогда не ответит на «несуществующий» адрес, т.к. мы не написали правило обработки таких адресов.

Второй момент, который ярко бросается в глаза — это ручное прописывание путей. Сразу наворачивается вопрос — А где тут программирование? Всё делать руками? А если страниц на сайте 6 миллионов? Ответ — конечно же, не делать всё руками.

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

Условный пример. Предположим у нас в базе есть некоторый Массив со стульями (Объекты JavaScript):

[
   {art:"Стул белый", descr:"Это идеальный стул для светлых квартир"},
   {art:"Стул красный", descr:"Яркий стул для ярких людей"},
   {art:"Стул 40 см", descr:"Короткий стул для малышей"},
   {art:"Стул кожаный", descr:"Премиум стул для дорогих интерьеров"}
]

Мы ловим запрос из браузера пользователя:

Stul-krasnyj

Этот запрос попадает в функцию-обработчик, которая возвращает ответы. Запускается «прогон» по массиву. На каждой итерации прогона на каждом элементе массива выдёргивается значение по ключу «art». Это значение транслитируется и сравнивается с запросом. Если сравнение 100%, тогда возвращается ответ из значения по ключу «descr».

Фантазировать можно очень много — практически бесконечно. Главная задача в этом деле — писать понятную документацию проекта, чтобы понимать как он работает (где что генерируется и обрабатывается 🙂 ). Но это уже тема отдельных разговоров, касаемо паттернов проектирования и работы с абстракциями.

 

Информационные ссылки

JavaScript | Транслитерация с русского на английский

JavaScript | Объекты (Object) — https://efim360.ru/javascript-obekty-object/

Стандарт Node.jshttps://nodejs.org/en/

Менеджер пакетов для Node.js (NPM) — https://www.npmjs.com

PM2 — это демон-менеджер процессов, который поможет вам управлять вашим приложением и поддерживать его в сети 24/7. — https://pm2.keymetrics.io

Веб-сервер NGINX — https://nginx.org/ru/