035.2 Урок 1
Сертифікат: |
Основи веброзробки |
---|---|
Версія: |
1.0 |
Розділ: |
035 Серверне програмування Node.js |
Тема: |
035.2 Основи NodeJS Express |
Урок: |
1 з 2 |
Вступ
Express.js, або просто Express, є популярним фреймворком, який працює на Node.js і використовується для написання HTTP-серверів, які обробляють запити від клієнтів вебзастосунків. Express підтримує багато способів читання параметрів, надісланих через HTTP.
===Початковий серверний сценарій
Щоб продемонструвати основні можливості Express для отримання та обробки запитів, давайте змоделюємо застосунок, який запитує певну інформацію від сервера. Зокрема, приклад сервера:
-
Забезпечує функцію
echo
, яка просто повертає повідомлення, надіслане клієнтом. -
За запитом повідомляє клієнту свою IP-адресу.
-
Використовує файли cookie для ідентифікації відомих клієнтів.
Першим кроком є створення файлу JavaScript, який буде працювати як сервер. Використовуючи npm
, створіть каталог під назвою myserver
з таким файлом JavaScript:
$ mkdir myserver $ cd myserver/ $ npm init
Для точки входу можна використовувати будь-яке ім’я файлу. Тут ми будемо використовувати ім’я файлу за замовчуванням: index.js
. У наступному лістингу показано базовий файл index.js
, який буде використовуватися як точка входу для нашого сервера:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.get('/', (req, res) => {
res.send('Request received')
})
app.listen(port, host, () => {
console.log(`Server ready at http://${host}:${port}`)
})
Деякі важливі константи для конфігурації сервера визначені в перших рядках сценарію. Перші два, express
та app
, відповідають включеному модулю express
та екземпляру цього модуля, який запускає нашу програму. Ми додамо дії, які виконує сервер, до об’єкта app
.
Дві інші константи, host
і port
, визначають хост і комунікаційний порт, які пов’язані з сервером.
Якщо у вас є загальнодоступний хост, використовуйте його ім’я замість myserver
у якості значення для host
. Якщо ви не вкажете ім’я хоста, Express за замовчуванням буде використовувати localhost
, комп’ютер, на якому працює програма. У цьому випадку сторонні клієнти не зможуть отримати доступ до застосунку, що може бути непогано для тестування, але має мало цінності у виробничому середовищі.
Потрібно вказати порт, інакше сервер не запуститься.
Цей сценарій додає лише дві процедури до об’єкта app
: дію app.get()
, яка відповідає на запити клієнтів через HTTP GET
, і виклик app.listen()
, який є необхідним для активації сервера і призначає йому хост і порт.
Щоб запустити сервер, просто виконайте команду node
, вказавши ім’я сценарію як аргумент:
$ node index.js
Щойно з’явиться повідомлення Server ready at http://myserver:8080
, сервер готовий приймати запити від HTTP-клієнта. Запити можна робити з браузера на тому ж комп’ютері, де працює сервер, або з іншого комп’ютера, який має доступ до сервера.
Усі деталі транзакції, які ми побачимо тут, відображаються у браузері, якщо ви відкриєте вікно консолі розробника. Крім того, команда curl
може використовуватися для HTTP-комунікації і дає змогу вам легше перевіряти деталі з’єднання. Якщо ви не знайомі з командним рядком оболонки, ви можете створити HTML-форму для надсилання запитів на сервер.
У наступному прикладі показано, як використовувати команду curl
у командному рядку, щоб зробити запит HTTP до щойно розгорнутого сервера:
$ curl http://myserver:8080 -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/html; charset=utf-8 < Content-Length: 16 < ETag: W/"10-1WVvDtVyAF0vX9evlsFlfiJTT5c" < Date: Fri, 02 Jul 2021 14:35:11 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Request received
Параметр -v
команди curl
відображає всі заголовки запиту та відповіді, а також іншу інформацію про відлагодження. Рядки, що починаються з >
, вказують на заголовки запиту, надіслані клієнтом, а рядки, що починаються з <
, вказують на заголовки відповіді, надіслані сервером. Рядки, що починаються з *
, є інформацією, створеною самим curl
. Вміст відповіді відображається тільки в кінці, і в цьому випадку це рядок Request received
.
URL-адреса служби, яка в цьому випадку містить ім’я та порт сервера (http://myserver:8080
), були наведені як аргументи команді curl
. Оскільки ім’я каталогу чи файлу не вказано, за замовчуванням використовується кореневий каталог /
. Коса риска відображається в якості файлу запиту в рядку > GET / HTTP/1.1
, за яким у виводі слідують ім’я хоста та порт.
Окрім відображення заголовків з’єднання HTTP, команда curl
допомагає розробці застосунків, даючи змогу надсилати дані на сервер за допомогою різних методів HTTP і в різних форматах. Ця гнучкість полегшує відлагодження будь-яких проблем і впровадження нових функцій на сервері.
Маршрути
Запити, які клієнт може зробити до сервера, залежать від того, які маршрути визначено у файлі index.js
. Маршрут встановлює HTTP-метод і визначає шлях (точніше, URI), який може бути запитаний клієнтом.
Поки що на сервері налаштовано лише один маршрут:
app.get('/', (req, res) => {
res.send('Request received')
})
Незважаючи на те, що це дуже простий маршрут, який просто повертає клієнту звичайне текстове повідомлення, його достатньо, щоб визначити найважливіші компоненти, які використовуються для структурування більшості маршрутів:
-
Метод HTTP, який обслуговується маршрутом. У нашому прикладі метод HTTP
GET
позначений властивістюget
об’єктаapp
. -
Шлях, який обслуговує маршрут. Якщо клієнт не вказує шлях для запиту, сервер використовує кореневий каталог, який є базовим каталогом, виділеним для використання вебсервером. Наступний приклад у цьому розділі використовує шлях
/echo
, який відповідає запиту, зробленому доmyserver:8080/echo
. -
Функція, що виконується, коли сервер отримує запит на цьому маршруті, зазвичай записується в скороченому вигляді як функція зі стрілкою, оскільки синтаксис
=>
вказує на визначення безіменної функції. Параметрreq
(скорочене від “request”) і параметрres
(скорочено від “response”) надають деталі про з’єднання, передані функції самим екземпляром застосунку.
Метод POST
Щоб розширити функціональність нашого тестового сервера, давайте подивимося, як визначити маршрут для методу HTTP POST
. Його використовують клієнти, коли їм потрібно надіслати на сервер додаткові дані, крім тих, які включені до заголовку запиту. Параметр --data
команди curl
автоматично викликає метод POST
і включає вміст, який буде надіслано на сервер через POST
. Рядок "POST / HTTP/1.1" у наступному виводі показує, що використовувався метод POST
. Однак на нашому сервері визначено лише метод GET
, тому виникає помилка, коли ми використовуємо curl
для відправки запиту через POST
:
$ curl http://myserver:8080/echo --data message="This is the POST request body" * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > POST / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Content-Length: 37 > Content-Type: application/x-www-form-urlencoded > * upload completely sent off: 37 out of 37 bytes * Mark bundle as not supporting multiuse < HTTP/1.1 404 Not Found < X-Powered-By: Express < Content-Security-Policy: default-src 'none' < X-Content-Type-Options: nosniff < Content-Type: text/html; charset=utf-8 < Content-Length: 140 < Date: Sat, 03 Jul 2021 02:22:45 GMT < Connection: keep-alive < <!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <title>Error</title> </head> <body> <pre>Cannot POST /</pre> </body> </html> * Connection #0 to host myserver left intact
У попередньому прикладі запуск curl
з параметром --data message="This is the POST request body"
еквівалентно відправці форми, що містить текстове поле з назвою message
, заповнене This is the POST request body
.
Оскільки сервер налаштовано лише на один маршрут для шляху /
, і цей маршрут відповідає лише методу HTTP GET
, тому заголовок відповіді містить рядок HTTP/1.1 404 Not Found
. Крім того, Express автоматично створив коротку HTML-відповідь із попередженням Cannot POST
.
Ознайомившись з тим, як генерувати запит POST
через curl
, давайте напишемо Express-програму, яка зможе успішно обробити запит.
По-перше, зверніть увагу, що поле Content-Type
у заголовку запиту говорить, що дані, надіслані клієнтом, мають формат application/x-www-form-urlencoded
. Express не розпізнає цей формат за замовчуванням, тому нам потрібно використовувати модуль express.urlencoded
. Коли ми включаємо цей модуль, об’єкт req
, що передається як параметр функції-обробника, має набір властивостей req.body.message
, що відповідає полю message
, надісланому клієнтом. Модуль завантажується з app.use
, який слід розмістити перед оголошенням маршрутів:
const express = require('express')
const app = express()
const host = "myserver"
const port = 8080
app.use(express.urlencoded({ extended: true }))
Щойно це буде зроблено, достатньо буде змінити app.get
на app.post
в існуючому маршруті, щоб виконати запити, зроблені через POST
, і відновити тіло запиту:
app.post('/', (req, res) => {
res.send(req.body.message)
})
Замість заміни маршруту іншою можливістю було б просто додати цей новий маршрут, оскільки Express визначає метод HTTP у заголовку запиту та використовує відповідний маршрут. Оскільки ми зацікавлені в тому, щоб додати більше ніж одну функціональність до цього сервера, зручно відокремити кожну з них власним шляхом, наприклад /echo
та /ip
.
до
Обробник шляхів та функцій
Після визначення того, який метод HTTP відповідатиме на запит, нам потрібно визначити конкретний шлях для ресурсу та функцію, яка обробляє та генерує відповідь клієнту.
Щоб розширити echo
функціональність сервера, ми можемо визначити маршрут за допомогою методу POST
із шляхом /echo
:
app.post('/echo', (req, res) => {
res.send(req.body.message)
})
Параметр req
функції-обробника містить усі деталі запиту, збережені як властивості. Вміст поля message
у тілі запиту доступний у властивості req.body.message
. У прикладі просто надсилається це поле, яке повертається клієнту за допомогою виклику res.send(req.body.message)
.
Пам’ятайте, що внесені вами зміни вступають в силу лише після перезавантаження сервера. Оскільки ви запускаєте сервер із вікна терміналу під час відпрацювання прикладів цього розділу, ви можете вимкнути сервер, натиснувши kbd:[Ctrl+C] у цьому терміналі. Потім перезапустіть сервер за допомогою команди node index.js
. Відповідь, отримана клієнтом на запит curl
, який ми показали раніше, тепер успішна:
$ curl http://myserver:8080/echo --data message="This is the POST request body" This is the POST request body
Інші способи передачі та повернення інформації в GET-запиті
Використання методу HTTP POST
може бути надмірним, якщо надсилатимуться лише короткі текстові повідомлення, як у нашому прикладі. У таких випадках дані можна надіслати в рядку запиту, починаючи зі знака питання. Таким чином, рядок ?message=This+is+the+message
може бути включено до шляху запиту методу HTTP GET
. Поля, які використовуються в рядку запиту, доступні серверу у властивості req.query
. Таким чином, поле з назвою message
доступне у властивості req.query.message
.
Інший спосіб надсилання даних за допомогою методу HTTP GET
– це використання параметрів маршруту Express:
app.get('/echo/:message', (req, res) => {
res.send(req.params.message)
})
Маршрут у цьому прикладі відповідає запитам, здійсненим за допомогою методу GET
з використанням шляху /echo/:message
, де :message
є заповнювачем для будь-якого слова, надісланого клієнтом із цією міткою. Ці параметри доступні у властивості req.params
. За допомогою цього нового маршруту клієнт може більш стисло запитати функцію echo
сервера:
$ curl http://myserver:8080/echo/hello hello
В інших ситуаціях від клієнта не вимагається надавати інформацію, необхідну серверу для обробки запиту. Наприклад, у сервера є інший спосіб отримати публічну IP-адресу клієнта. Ця інформація присутня в об’єкті req
за замовчуванням, у властивості req.ip
:
app.get('/ip', (req, res) => {
res.send(req.ip)
})
Тепер клієнт може запитати шлях /ip
за допомогою методу GET
, щоб знайти свою власну публічну IP-адресу:
$ curl http://myserver:8080/ip 187.34.178.12
Інші властивості об’єкта req
можуть бути змінені клієнтом, особливо заголовки запиту, доступні в req.headers
. Властивість req.headers.user-agent
, наприклад, визначає, яка програма здійснює запит. Хоча це не є звичайною практикою, клієнт може змінювати вміст цього поля, тому сервер не повинен використовувати його для надійної ідентифікації конкретного клієнта. Ще важливіше перевірити дані, явно надані клієнтом, щоб уникнути невідповідності в межах і форматах, які можуть негативно вплинути на програму.
Коригування відповіді
Як видно з попередніх прикладів, параметр res
відповідає за повернення відповіді клієнту. Крім того, об’єкт res
може змінити інші аспекти відповіді. Можливо, ви помітили, що, хоча відповіді, які ми реалізували до цього часу, є лише короткими простими текстовими повідомленнями, Content-Type
заголовку у відповідях використовує text/html; charset=utf-8
. Хоча це не перешкоджає прийняттю відповіді у вигляді звичайного тексту, буде правильніше, якщо ми перевизначимо це поле в заголовку відповіді на text/plain
з параметром res.type('text/plain')
.
Інші типи коригування відповіді включають використання файлів cookie, які дають змогу серверу ідентифікувати клієнта, який раніше здійснив запит. Файли cookie важливі для розширених функцій, таких як створення приватних сеансів, які пов’язують запити з певним користувачем. Але тут ми розглянемо простий приклад того, як використовувати файли cookie для ідентифікації клієнта, який раніше звертався до сервера.
Враховуючи модульний дизайн Express, керування файлами cookie має бути встановлено за допомогою команди npm
до використання у сценарії:
$ npm install cookie-parser
Після встановлення керування файлами cookie має бути включено до серверного сценарію. Наступне визначення має бути включено на початку файлу:
const cookieParser = require('cookie-parser')
app.use(cookieParser())
Щоб проілюструвати використання файлів cookie, давайте змінимо функцію обробника маршруту за допомогою кореневого шляху /
, який уже існує в сценарії. Об’єкт req
має властивість req.cookies
, де зберігаються файли cookie, надіслані в заголовку запиту. Об’єкт res
, з іншого боку, має метод res.cookie()
, який створює новий файл cookie для надсилання клієнту. Функція обробника в наступному прикладі перевіряє, чи існує в запиті файл cookie з іменем known
. Якщо такий файл cookie не існує, сервер припускає, що цей відвідувач зайшов вперше, і надсилає йому файл cookie з такою назвою за допомогою виклику res.cookie('known', '1')
. Ми довільно призначаємо значення 1
файлу cookie, оскільки він повинен мати певний вміст, але сервер не переглядає це значення. Цей застосунок просто припускає, що проста наявність файлу cookie вказує на те, що клієнт уже запитував цей маршрут раніше:
app.get('/', (req, res) => {
res.type('text/plain')
if ( req.cookies.known === undefined ){
res.cookie('known', '1')
res.send('Welcome, new visitor!')
}
else
res.send('Welcome back, visitor');
})
За замовчуванням curl
не використовує файли cookie в транзакціях. Але він має параметри для збереження (-c cookies.txt
) та надсилання збережених файлів cookie (-b cookies.txt
):
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 * Added cookie known="1" for domain myserver, path /, expire 0 < Set-Cookie: known=1; Path=/ < Content-Length: 21 < ETag: W/"15-l7qrxcqicl4xv6EfA5fZFWCFrgY" < Date: Sat, 03 Jul 2021 23:45:03 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome, new visitor!
Оскільки ця команда виконувала перший доступ після того, як файли cookie були реалізовані на сервері, тому клієнт не мав файлів cookie для включення в запит. Як і очікувалося, сервер не ідентифікував файл cookie у запиті, і тому включив файл cookie до заголовків відповіді, як зазначено в рядку виводу Set-Cookie: known=1; Path=/
. Оскільки ми ввімкнули файли cookie в curl
, новий запит включатиме файл cookie known=1
до заголовків запиту, даючи змогу серверу ідентифікувати наявність файлу cookie:
$ curl http://myserver:8080/ -c cookies.txt -b cookies.txt -v * Trying 192.168.1.225:8080... * TCP_NODELAY set * Connected to myserver (192.168.1.225) port 8080 (#0) > GET / HTTP/1.1 > Host: myserver:8080 > User-Agent: curl/7.68.0 >Accept: / > Cookie: known=1 > * Mark bundle as not supporting multiuse < HTTP/1.1 200 OK < X-Powered-By: Express < Content-Type: text/plain; charset=utf-8 < Content-Length: 21 < ETag: W/"15-ATq2flQYtLMYIUpJwwpb5SjV9Ww" < Date: Sat, 03 Jul 2021 23:45:47 GMT < Connection: keep-alive < * Connection #0 to host myserver left intact Welcome back, visitor
Безпека файлів cookie
Розробник повинен знати про потенційні вразливості під час використання файлів cookie для ідентифікації клієнтів, які надсилають запити. Зловмисники можуть використовувати такі методи, як міжсайтовий скриптинг (XSS, Cross-Site Scripting) та міжсайтова підробка запиту (CSRF, Cross-Site Request Forgery), щоб викрасти файли cookie у клієнта і таким чином видавати себе за нього під час надсилання запиту до сервера. Інакше кажучи, ці типи атак використовують неперевірені поля коментарів або ретельно створені URL-адреси, щоб вставити шкідливий код JavaScript на сторінку. При виконанні автентичним клієнтом цей код може скопіювати дійсні файли cookie та зберігати їх або пересилати до іншого місця призначення.
Тому, особливо в професійних застосунках, важливо встановити та використовувати більш спеціалізовані функції Express, відомі як middleware (проміжне програмне забезпечення). Модуль express-session
або cookie-session
забезпечує більш повний і безпечний контроль над сеансом і керуванням файлами cookie. Ці компоненти забезпечують додаткові елементи керування, щоб запобігти переадресації файлів cookie від їх початкового емітента.
Вправи до посібника
-
Як вміст поля
comment
, надісланого в рядку запиту методу HTTPGET
, можна прочитати у функції обробника? -
Напишіть маршрут, який використовує метод
GET
HTTP і шлях/agent
, щоб повертати клієнту вміст заголовкаuser-agent
. -
Express.js має функцію під назвою параметри маршрута, де такий шлях, як
/user/:name
, може використовуватися для отримання параметраname
, надісланого клієнтом. Як можна отримати доступ до параметраname
у функції обробника маршруту?
Дослідницькі вправи
-
Якщо ім’я сервера –
myserver
, то який Express-маршрут отримає повідомлення з форми нижче?<form action="/contact/feedback" method="post"> ... </form>
-
Під час розробки сервера програміст не може прочитати властивість
req.body
, навіть перевіривши, що клієнт правильно надсилає контент за допомогою методу HTTPPOST
. Яка ймовірна причина цієї проблеми? -
Що станеться, якщо сервер має маршрут, встановлений на шлях
/user/:name
, а клієнт здійснює запит до/user/
?
Підсумки
У цьому уроці пояснюється, як писати Express-скрипти для отримання та обробки HTTP-запитів. Express використовує концепцію маршрутів для визначення ресурсів, доступних клієнтам, що дає вам велику гнучкість у створенні серверів для будь-якого типу вебзастосунків. Цей урок охоплює наступні концепції та процедури:
-
Маршрути, які використовують методи HTTP
GET
і HTTPPOST
. -
Як дані форми зберігаються в об’єкті
request
. -
Як використовувати параметри маршруту.
-
Налаштування заголовків відповідей.
-
Базове керування файлами cookie.
Відповіді до вправ посібника
-
Як вміст поля
comment
, надісланого в рядку запиту методу HTTPGET
, можна прочитати у функції обробника?Поле
comment
доступне у властивостіreq.query.comment
. -
Напишіть маршрут, який використовує метод
GET
HTTP і шлях/agent
, щоб повертати клієнту вміст заголовкаuser-agent
.app.get('/agent', (req, res) => { res.send(req.headers.user-agent) })
-
Express.js має функцію під назвою параметри маршрута, де такий шлях, як
/user/:name
, може використовуватися для отримання параметраname
, надісланого клієнтом. Як можна отримати доступ до параметраname
у функції обробника маршруту?Параметр
name
доступний у властивостіreq.params.name
.
Відповіді до дослідницьких вправ
-
Якщо ім’я сервера –
myserver
, то який Express-маршрут отримає повідомлення з форми нижче?<form action="/contact/feedback" method="post"> ... </form>
app.post('/contact/feedback', (req, res) => { ... })
-
Під час розробки сервера програміст не може прочитати властивість
req.body
, навіть перевіривши, що клієнт правильно надсилає контент за допомогою методу HTTPPOST
. Яка ймовірна причина цієї проблеми?Програміст не включив модуль
express.urlencoded
, який дає змогу Express витягувати тіло запиту. -
Що станеться, якщо сервер має маршрут, встановлений на шлях
/user/:name
, а клієнт робить здійснює запит до/user/
?Сервер видасть відповідь
404 Not Found
, оскільки для маршруту потрібно, щоб клієнт надав параметр:name
.