035.2 Урок 2
Сертифікат: |
Основи веброзробки |
---|---|
Версія: |
1.0 |
Розділ: |
035 Серверне програмування Node.js |
Тема: |
035.2 Основи NodeJS Express |
Урок: |
2 з 2 |
Вступ
Вебсервери мають дуже різноманітні механізми для отримання відповідей на запити клієнтів. Для деяких запитів вебсерверу достатньо надати статичну необроблену відповідь, оскільки запитуваний ресурс однаковий для будь-якого клієнта. Наприклад, коли клієнт запитує зображення, доступне для всіх, серверу достатньо надіслати файл, що містить зображення.
Але коли відповіді генеруються динамічно, може знадобитися структурувати їх краще, ніж прості рядки, записані в сценарії сервера. У таких випадках вебсерверу зручно генерувати повний документ, який може бути інтерпретований та відтворений клієнтом. У контексті розробки вебзастосунків документи HTML зазвичай створюються як шаблони і зберігаються окремо від серверного сценарію, який вставляє динамічні дані у заздалегідь визначені місця у відповідному шаблоні, а потім надсилає відформатовану відповідь клієнту.
Вебзастосунки часто споживають як статичні, так і динамічні ресурси. HTML-документ, навіть якщо він був створений динамічно, може мати покликання на статичні ресурси, як-от CSS-файли та зображення. Щоб продемонструвати, як Express допомагає впоратися з таким викликом, ми спочатку налаштуємо приклад сервера, який доставляє статичні файли, а потім реалізуємо маршрути, які генерують структуровані відповіді на основі шаблонів.
Статичні файли
Першим кроком є створення файлу JavaScript, який працюватиме як сервер. Будемо дотримуватись того ж шаблону, який розглядався в попередніх уроках, щоб створити простий Express-застосунок: спочатку створіть каталог під назвою server
, а потім встановіть базові компоненти за допомогою команди npm
:
$ mkdir server $ cd server/ $ npm init $ npm install express
Для точки входу можна використовувати будь-яке ім’я файлу, але тут ми будемо використовувати ім’я файлу за замовчуванням: index.js
. У наступному лістингу показано основний файл index.js
, який буде використовуватися як відправна точка для нашого сервера:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Вам не потрібно писати явний код, щоб надіслати статичний файл. Express має проміжне програмне забезпечення для цієї мети, яке називається express.static
. Якщо вашому серверу потрібно надіслати статичні файли клієнту, просто завантажте проміжне програмне забезпечення express.static
на початку сценарію:
app.use(express.static('public'))
Параметр public
зазначає каталог, де зберігаються файли, які клієнт може запитати. Шляхи, які запитують клієнти, не повинні містити каталог public
, а лише ім’я файлу або шлях до файлу відносно каталогу public
. Щоб запитати файл public/layout.css
, наприклад, клієнт здійснює запит до /layout.css
.
Форматований вивід
Тоді як надсилання статичного вмісту є простим, для динамічно згенерованого вмісту це інакше. Створення динамічних відповідей із короткими повідомленнями дає змогу легко тестувати програми на початкових етапах розробки. Наприклад, нижче наведено тестовий маршрут, який просто повертає клієнту повідомлення, надсилаючи його методом HTTP POST
. Відповідь може просто реплікувати вміст повідомлення у вигляді простого тексту, без будь-якого форматування:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Подібний маршрут є хорошим прикладом для використання під час вивчення Express і для діагностичних цілей, коли достатньо «сирої» відповіді, надісланої за допомогою res.send()
. Але робочий сервер повинен бути здатним виробляти більш складні відповіді. Зараз ми перейдемо до розробки такого більш складного типу маршруту.
Наш новий застосунок замість того, щоб просто повертати вміст поточного запиту, підтримує повний перелік повідомлень, надісланих у попередніх запитах кожним клієнтом, і на запит повертає список кожного повідомлень клієнта. Відповідь, що об’єднує всі повідомлення є одним із варіантів, але інші форматовані режими виводу є більш доречними, особливо коли відповіді стають більш складними.
Щоб отримувати та зберігати клієнтські повідомлення, надіслані під час поточного сеансу, найперше нам потрібно включити додаткові модулі для обробки файлів cookie та даних, надісланих за допомогою методу HTTP POST
. Єдина мета наведеного нижче прикладу сервера – реєструвати повідомлення, надіслані через POST
, і відображати раніше надіслані повідомлення, коли клієнт надсилає запит GET
. Отже, для шляху /
є два маршрути. Перший маршрут виконує запити, здійснені за допомогою методу HTTP POST
, а другий виконує запити, здійснені за допомогою методу HTTP GET
:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.static('public'))
const cookieParser = require('cookie-parser')
app.use(cookieParser())
const { v4: uuidv4 } = require('uuid')
app.use(express.urlencoded({ extended: true }))
// Масив для зберігання повідомлень
let messages = []
app.post('/', (req, res) => {
// Only JSON enabled requests
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Знайти файл cookie в запиті
let uuid = req.cookies.uuid
// Якщо немає uuid cookie, створимо його
if ( uuid === undefined )
uuid = uuidv4()
// Додаємо повідомлення на початку масиву повідомлень
messages.unshift({uuid: uuid, message: req.body.message})
// Збираємо всі попередні повідомлення для uuid
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
// Оновлення дати закінчення терміну дії файлів cookie
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Повертаємо JSON-відповідь
res.json(user_entries)
})
app.get('/', (req, res) => {
// Лише запити з підтримкою JSON
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
// Визначаємо наявність файлу cookie в запиті
let uuid = req.cookies.uuid
// Власні повідомлення клієнта
let user_entries = []
// Якщо немає uuid cookie, створимо його
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
// Collect messages for uuid
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
// Оновити дату закінчення терміну дії файлів cookie
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
// Повертаємо JSON-відповідь
res.json(user_entries)
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Ми залишили конфігурацію статичних файлів у верхній частині, тому що незабаром буде корисно надавати статичні файли, такі як layout.css
. На додаток до проміжного програмного забезпечення cookie-parser
, представленого в попередньому розділі, приклад також містить проміжне програмне забезпечення uuid
для створення унікального ідентифікаційного номера, що передається як файл cookie кожному клієнту, який надсилає повідомлення. Якщо вони ще не встановлені в каталозі прикладу сервера, ці модулі можна встановити за допомогою команди npm install cookie-parser uuid
.
Глобальний масив під назвою messages
зберігає повідомлення, надіслані всіма клієнтами. Кожен елемент у цьому масиві складається з об’єкта з властивостями uuid
та message
.
Що дійсно нового в цьому скрипті, так це метод res.json()
, який використовується в кінці двох маршрутів для створення відповіді у форматі JSON з масивом, що містить повідомлення, уже надіслані клієнтом:
// Повертаємо JSON-відповідь
res.json(user_entries)
JSON – це звичайний текстовий формат, який дає змогу згрупувати набір даних в одну структуру, яка є асоціативною: тобто вміст виражається як ключі та значення. JSON особливо корисний, коли ми збираємось відповіді обробляти клієнтом. Використовуючи цей формат, об’єкт або масив JavaScript можна легко відновити на стороні клієнта з усіма властивостями та індексами оригінального об’єкта на сервері.
Оскільки ми структуруємо кожне повідомлення в JSON, ми відхиляємо запити, які не містять application/json
у accept
заголовку:
// Лише запити з підтримкою JSON
if ( req.headers.accept != "application/json" )
{
res.sendStatus(404)
return
}
Запит, здійснений за допомогою звичайної команди curl
для вставки нового повідомлення, не буде прийнятий, оскільки curl
за замовчуванням не визначає application/json
у заголовку accept
:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt Not Found
Параметр -H "accept: application/json"
змінює заголовок запиту, щоб вказати формат відповіді, який цього разу буде прийнято та відповісти у зазначеному форматі:
$ curl http://myserver:8080/ --data message="My first message" -c cookies.txt -b cookies.txt -H "accept: application/json" ["My first message"]
Отримання повідомлень за допомогою іншого маршруту здійснюється у такий самий спосіб, але цього разу за допомогою методу HTTP GET
:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -H "accept: application/json" ["Another message","My first message"]
Шаблони
Відповіді у таких форматах, як JSON, зручні для обміну даними між програмами, але основна мета більшості серверів вебзастосунків – створювати HTML-контент для використання людиною. Вбудовування HTML-коду до коду JavaScript не є гарною ідеєю, оскільки змішування мов в одному файлі робить програму більш сприйнятливою до помилок і шкодить підтримці коду.
Express може працювати з різними шаблонізаторами, які виділяють HTML для динамічного контенту; повний список можна знайти на https://expressjs.com/en/resources/template-engines.html [сайті з Express-шаблонізаторами]. Одним із найпопулярніших шаблонізаторів є Embedded JavaScript (EJS), який дає змогу створювати HTML-файли зі спеціальними тегами для вставки динамічного контенту.
Як і інші компоненти Express, EJS необхідно встановити до каталогу, де працює сервер:
$ npm install ejs
Далі двигун EJS має бути встановлений як засіб візуалізації за замовчуванням у сценарії сервера (на початку файлу index.js
, перед визначеннями маршруту):
app.set('view engine', 'ejs')
Відповідь, згенерована за шаблоном, надсилається клієнту за допомогою функції res.render()
, яка отримує в якості параметрів ім’я файлу шаблону та об’єкт, що містить значення, які будуть доступні з шаблону. Маршрути, використані в попередньому прикладі, можна переписати, щоб генерувати як HTML-відповіді, так і JSON-відповіді:
app.post('/', (req, res) => {
let uuid = req.cookies.uuid
if ( uuid === undefined )
uuid = uuidv4()
messages.unshift({uuid: uuid, message: req.body.message})
let user_entries = []
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
app.get('/', (req, res) => {
let uuid = req.cookies.uuid
let user_entries = []
if ( uuid === undefined ){
uuid = uuidv4()
}
else {
messages.forEach( (entry) => {
if ( entry.uuid == req.cookies.uuid )
user_entries.push(entry.message)
})
}
let expires = new Date(Date.now());
expires.setDate(expires.getDate() + 30);
res.cookie('uuid', uuid, { expires: expires })
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
})
Зверніть увагу, що формат відповіді залежить від заголовка accept
, знайденого в запиті:
if ( req.headers.accept == "application/json" )
res.json(user_entries)
else
res.render('index', {title: "My messages", messages: user_entries})
Відповідь у форматі JSON надсилається лише в тому випадку, якщо клієнт цього явно вимагає. В іншому випадку відповідь генерується з шаблону index
. Той самий масив user_entries
забезпечує як вихідні дані JSON, так і шаблон, але об’єкт, який використовується як параметр для останнього, також має властивість title: "My messages"
, яка буде використовуватися як заголовок всередині шаблону.
Шаблони HTML
Як і статичні файли, файли, що містять шаблони HTML, знаходяться у власному каталозі. За замовчуванням EJS передбачає, що файли шаблонів знаходяться в каталозі views/
. У прикладі використовувався шаблон під назвою index
, тому EJS шукає файл views/index.ejs
. Нижче наведено вміст простого шаблону views/index.ejs
, який можна використовувати із цим прикладом коду:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title><%= title %></title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="/layout.css">
</head>
<body>
<div id="interface">
<form action="/" method="post">
<p>
<input type="text" name="message">
<input type="submit" value="Submit">
</p>
</form>
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
</div>
</body>
</html>
Перший спеціальний тег EJS — це елемент <title>
у розділі <head>
:
<%= title %>
Під час процесу візуалізації цей спеціальний тег буде замінено значенням властивості title
об’єкта, переданого як параметр функції res.render()
.
Більшість шаблонів складається зі звичайного HTML-коду, тому шаблон містить HTML-форму для надсилання нових повідомлень. Тестовий сервер відповідає на методи HTTP GET
і POST
для того самого шляху /
, звідси атрибути action="/"
і method="post"
у тегу форми.
Інші частини шаблону є поєднанням HTML-коду та тегів EJS. EJS має теги для певних цілей у шаблоні:
<% … %>
-
Вставки контролю потоку. Цей тег безпосередньо не вставляє контент, але його можна використовувати зі структурами JavaScript, щоб вибрати, повторити або приховати розділи HTML. Приклад запуску циклу:
<% messages.forEach( (message) => { %>
<%# … %>
-
Визначає коментар, вміст якого ігнорується синтаксичним аналізатором. На відміну від коментарів, написаних у HTML, ці коментарі не видно клієнту.
<%= … %>
-
Вставляє екранований вміст змінної. Важливо уникнути невідомого вмісту, щоб уникнути виконання коду JavaScript, яке може відкрити лазівки для атак міжсайтового скриптингу (XSS). Приклад:
<%= title %>
<%- … %>
-
Вставляє вміст змінної без екранування.
Поєднання HTML-коду та тегів EJS є очевидним у фрагменті, де клієнтські повідомлення відображаються у вигляді списку HTML:
<ul>
<% messages.forEach( (message) => { %>
<li><%= message %></li>
<% }) %>
</ul>
У цьому фрагменті перший тег <% … %>
запускає оператор forEach
, який перебирає всі елементи масиву message
. Розділювачі <%
та %>
дають змогу керувати фрагментами HTML. Новий елемент списку HTML, <li><%= message %></li>
, буде створено для кожного елемента messages
. Із цими змінами сервер надішле відповідь у HTML, коли буде отримано запит, подібний до наступного:
$ curl http://myserver:8080/ --data message="This time" -c cookies.txt -b cookies.txt <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>My messages</title> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="stylesheet" href="/layout.css"> </head> <body> <div id="interface"> <form action="/" method="post"> <p> <input type="text" name="message"> <input type="submit" value="Submit"> </p> </form> <ul> <li>This time</li> <li>in HTML</li> </ul> </div> </body> </html>
Розділення між кодом для обробки запитів і кодом для представлення відповіді робить код більш чистим і дає змогу команді розробників розділити розробку застосунку між людьми з різними спеціалізаціями. Вебдизайнер, наприклад, може зосередитися на файлах шаблонів у views/
та відповідних таблицях стилів, які надаються як статичні файли, що зберігаються в каталозі public/
на сервері прикладу.
Вправи до посібника
-
Як налаштувати
express.static
, щоб клієнти могли запитувати файли в каталозіassets
? -
Як можна визначити тип відповіді, вказаний у заголовку запиту, у межах Express-маршруту?
-
Який метод параметра маршруту
res
(response) генерує відповідь у форматі JSON з масиву JavaScript під назвоюcontent
?
Дослідницькі вправи
-
За замовчуванням файли шаблонів Express знаходяться в каталозі
views
. Як можна змінити це налаштування, щоб файли шаблонів зберігалися вtemplates
? -
Припустимо, що клієнт отримує відповідь HTML без заголовка (тобто
<title></title>
). Після перевірки шаблону EJS розробник знаходить тег<title><% title %></title>
у розділіhead
файлу. Яка ймовірна причина проблеми? -
Використайте теги EJS-шаблону, щоб записати HTML-тег
<h2></h2>
із вмістом змінної JavaScripth2
. Цей тег має відображатися лише, якщо зміннаh2
не порожня.
Підсумки
У цьому уроці розглядаються основні методи Express.js для генерування статичних і відформатованих, проте динамічних відповідей. Щоб налаштувати HTTP-сервер для статичних файлів, потрібно небагато зусиль, а система EJS-шаблонів забезпечує простий спосіб створення динамічного вмісту з файлів HTML. Цей урок охоплює наступні концепції та процедури:
-
Використання
express.static
для відповідей у вигляді статичних файлів. -
Як створити відповідь у відповідності до поля content type в заголовку запиту.
-
JSON-структуровані відповіді.
-
Використання EJS-тегів у шаблонах на основі HTML.
Відповіді до вправ посібника
-
Як налаштувати
express.static
, щоб клієнти могли запитувати файли в каталозіassets
?Слід додати до сценарію сервера виклик до
app.use(express.static('assets'))
. -
Як можна визначити тип відповіді, вказаний у заголовку запиту, у межах Express-маршруту?
Клієнт встановлює прийнятні типи в полі заголовка
accept
, яке зіставляється з властивістюreq.headers.accept
. -
Який метод параметра маршруту
res
(response) генерує відповідь у форматі JSON з масиву JavaScript під назвоюcontent
?Метод
res.json()
:res.json(content)
.
Відповіді до дослідницьких вправ
-
За замовчуванням файли шаблонів Express знаходяться в каталозі
views
. Як можна змінити це налаштування, щоб файли шаблонів зберігалися вtemplates
?Каталог можна визначити в початкових налаштуваннях сценарію за допомогою
app.set('views', './templates')
. -
Припустимо, що клієнт отримує відповідь HTML без заголовка (тобто
<title></title>
). Після перевірки шаблону EJS розробник знаходить тег<title><% title %></title>
у розділіhead
файлу. Яка ймовірна причина проблеми?Тег
<%= %>
має використовуватися для охоплення вмісту змінної, як у<%= title %>
. -
Використайте теги EJS-шаблону, щоб записати HTML-тег
<h2></h2>
із вмістом змінної JavaScripth2
. Цей тег має відображатися лише, якщо зміннаh2
не порожня.<% if ( h2 != "" ) { %> <h2><%= h2 %></h2> <% } %>