Express yourself with Node.js
Згадуючи себе на місці студента, я намагався не навантажувати лекцію зайвою та важкозасвоюваною інформацією, яка не знадобиться тобі у найближчі пару місяців. З іншого боку, розбиратися у деталях - це похвально. Тому для допитливих, я прикріпляю посилання на матеріали та статті. Лекція поділена на блоки, які максимально незалежні один від одного, тому якщо, прочитавши заголовок, ти розумієш що до чого, сміливо переходь до наступного блоку.
Хай щастить!
Студент, розуміючи, що проста сторінка починає розростатися у серйозний додаток з бізнес-логікою та керуванням користувачами, а також не маючи змоги зберігати великі об'єми даних на клієнті, вирішує, що настав час backend'у.
І так як студент пише на JS, то і вибір технології стає очевидним ― Node.js. Залишилось зовсім трохи ― вивчити цю технологію.
Робота з Node.js
Теорія
Difficulty: Як реклама на YouTube ― можна пропустити. Objectives: Зрозуміти, що таке сервер, де його місце у web і як з цим жити.- Клієнт ― це може бути браузер або будь-яке інше програмне забезпечення, що відправляє запити до серверу.
- Сервер ― це програма або її частина, що взаємодіє з клієнтом за принципом запит-відповідь на основі HTTP-протоколу, відправляючи необхідні дані або файли на клієнт.Коли web-розробник каже слово "сервер", він має на увазі "веб-сервер". Те ж саме стосується і "клієнту", "дотатку" і т.д.
- HTTP-протокол ― набір правил передачі даних в інтернеті.
- HTTP-метод ― вказує на те, яку операцію необхідно виконати з даним ресурсом. До основних можна віднести GET, POST, PUT, DELETE ― їх трошки більше, але це базис, якого тобі вистачить на перший час.
- HTTP-статус ― надходить разом із відповіддю з серверу і говорить про статус запитуваної операції. Найбільш часто використовуються: 200, 301, 401, 404, 500.
Web теорія
Подивимось на прикладі, як же працює Web:- користувач відкрив браузер / запустив клієнт
- користувач відкрив посилання / по HTTP відправив GET запит на сервер
- сервер опрацював запит / визначив, що запитується файл і знайшов його
Якщо ти пишеш на JS і вже зверстав свою першу HTML-сторінку - означає, що з клієнтом ти знайомий достатньо. Тут ми будемо розглядати роботу зі сторони серверу(взагалі то ми розглядаємо сервер через те, що це як би тема лекції, але не важливо).Сервер ― "що?" і "навіщо?"
Простими словами, коли користувач вводить запит до адресного рядку, браузер запитує дані на віддаленому комп'ютері (сервері) по HTTP. У відповідь він отримує файл або дані, для відображення їх у браузері.Для збереження та обробки усіх ресурсів, що використовуються у вебі. Неважливо, будь то одна статична сторінка або цілий Facebook, усе розташовано на віддалених серверах або хостінгах, і відображається користувачу тільки по його запиту.Сервер у сучасному вебі виконує безліч функцій з обробки та збереженню інформації, працює зі статичними файлами та конфіденційними даними, до яких ніхто не повинен мати доступу, виконує ресурсовитратні розрахунки, які занадто важкі для клієнтської частини, і ще багато чого іншого. Тому у залежності від складності проекту та вирішуваних задач, для комфортної роботи та збільшенню ККД, сервер буде будуватися за певною архітектурою: монолітною або мікросервісною
Історія Node.js
Полюбляєш хоку?:Райан Даль,
2009 рік,
іскра, буря, безумство — Node.jsСаме у 2009 році була опублікована перша версія Node.js. Після були деякі перепетії з правами, але нам це не так цікаво, як причини за яких Node.js був написаний саме під JavaScript:
JavaScript — мова клієнта#1
Як результат - велике ком'юніті, що бажає писати код на сервері, використовуючи вже знайому мову.Event Loop#2
JavaScript - це однопотокова мова, що означає, що усі операції, як синхронні так і асинхронні, відбуваються у єдиному потоці, який ніколи не блокується. Це і є основою відмовостійкості Node.js.V8#3
Двигун, розроблений командою Google Chrome для поліпшення роботи JavaScript у їх браузері. Він дуже швидкий та потужний, тому що написаний на C++. Node.js використовує двигун V8 поза браузером, і це дозволяє Node бути дуже ефективним.І зараз, завдяки усім цим факторам, ми можемо створювати повноцінні веб додатки, використовуючи JavaScript як на клієнті, так і на сервері.
Підготовка
Difficulty: Як встановлення Windows ― усе просто, але щось може піти не так. Objectives: Встановити Node.js на ПКДля того, щоб почати процес інсталяції, достатньо перейти на офіційний сайт https://nodejs.org, і завантажити інсталяційний файл для своєї операційної системи, далі дотримуватись інструкцій з інсталяції.
Після інсталяції обов'язково перезавантажте комп'ютер🖥️
Недостатні права⚠️
Неправильні змінні оточення🚧
Введіть наступні команди:
# Вивести версію Node.js
node -v
# Вивести версію npm
npm -v
твої версії можуть відрізнятися у більшу сторону
Помітили npm? Ми ж його не встановлювали? Це пакетний менеджер, розроблений командою Node.js, і встановлюється у комплекті для зручної роботи з модулями. Ми гоговоримо про нього детальніше у наступному блоці, а зараз головне переконатися у його працездатності.
Пакетні менеджери
Difficulty: Як вчитися грати у карти ― зрозуміти і запам'ятати хто за що відповідає. Objectives: Розібратися у термінології та попрацювати з пакетaми за допомогою npm.- Модулі (
modules
) — це частини програми/коду, винесені в окремі блоки для подальшого перевикористання. - Пакети (
packages
) — це збірник модулів. - Пакетні менеджері ― інструмент для інсталяції, видалення, оновлення, версіонування та валідації пакетів та модулів.
Що за "пакети" і навіщо їм потрібні "менеджери"?
1. У свому проекті ти написав функцію для сортування масиву і перевикористовуєш її у декількох місцях.
Вітаю, це майже пакет/модуль2. У подальшому ти вирішив, що ця ж функція може допомогти комусь ще, тому публікуєш її на StackOverflow як відповідь.
Зараз твій пакет/модуль у відкритому доступі й інші розробники можуть просто взяти і скопіювати його.3. А що як твоє рішення більш масштабне і займає 400 рядків коду? 500 рядків? 1000?
Не дуже й то і зручно копіювати такі об'єми руками
(хоча фронтедщики зі своїми jQuery-слайдерами посрепечаються).4. Тому краще правильно оформити свою функцію та опублікувати у віддалене сховище ― npm.
Тепер іншим програмістам достатньо знати тільки назву твого пакета/модуля і використати npm, щоб дотати його до себе у проект, видалити, якщо не сподобається, або оновити до наступної версії, якщо ти її зробиш.
Ще раз - у чому різниця між
packages
іmodules
? Повірте, у цьому плутаються навіть досвідчені розробники, а різниця проста,module
- це атом,package
- це молекула. Іншими словамиpackage
складається з іншихmodules
, які є наймешною одиницею, що вирішує одну задачу. Все максимально просто!Буквально декілька років назад блок про пакетні менеджери міг зайняти час цілої лекції, в силу їх величезнох кількості, але станом на сьогодні ситуація стабілізувалася, і теперь для работи з JS у нас є 2 лідери:
В yarn доступні усі пакети з npm, тому що вони мають єдиний реєстр даних на двох, і створений він був тільки для того, щоб поліпшити роботу npm. Фактичної різниці між ними немає, крім суб'єктивних забобонів у фанатів конкретного інструмента. У майбутньому ви зможете самостійно вирішити з чим вам зручніше працювати, а сьогодні ми будемо говорити про npm, через його прив'язанність до Node.js.
Робота з npm
Щоб дізнатися повний список команд доступних для npm, можна
і потрібнопочитати документацію, я наведу в лекції базовий мінімум:# ініціалізація проекту npm init # інсталяція пакету npm install package-name # інсталяція пакету тільки для розробки npm install package-name --save-dev # видалення пакету npm uninstall package-name # інсталяція пакету конкретної версії npm install package-name@version
- На завершення хотілось би пояснити навіщо придумали "пакетні менеджери", адже ми могли би додавати код руками прямо до проекту без будь-яких проблем:
Зручніть#1
Замість копіювання коду руками, для цього є зручний інструмент.Зменшений розмір проекту#2
Якщо би ми зберігали код усіх залежностей разом із кодом проекту, то це значно збільшило б його об'єм, що ускладнило б передачу й обмін даними всередині команди під час розробкиВирішення конфліктів#3
У зв'язку з величезним різноманіттям пакетів, вони можуть конфліктувати між собою, менеджери беруть ці проблеми на себе.Оптимізація#4
Пакет Х може залежити від пакета А і пакет Y може залежити від пакета А. Якщо би ми додавали пакети X і Y до проекту руками, то у нас був би дублікат пакету А. Менеджери вирішують питання дублікатів та оптимізують кількість встановлених залежностей.Кросплатформеність#5
Для роботи на різних платформах можуть знадобитись різні пакети або їх аналоги, і менеджери можуть надати довідку рекомендацій.
Основи Node.js
Difficulty: Як правила з бейсболу ― нічого не зрозумів, але дуже цікаво. Objectives: За допомогою пакетів Node.js запустити базовий HTTP-сервер і розібратися з eventloop.Node.js - це середовище виконання для JavaScript, і ми можем запускати будь-які скрипти у ньому. Все буде працювати майже як у браузері, тому перехід с фронтенду на бекенд повинен бути максимально комфортним.
Для переходу у середовище потрібно виконати команду node
у терминалі, потім написати будь-які команди на JS. Але очевидно, що цей метод не найзручніший і краще писати код у файлах, які у подальшому можна запускати командоюnode назва_файлу.js
.
Час експериментів! Ми вже знаємо як використовувати всю потужність JS з нодами і це круто, але ми вже хочемо писати сервери. Відкриємо офіційну документацію, щоб дізнатися список пакетів, що йдуть у комплекті з Node.js, і те, як з ними працювати. Як ми бачимо, пакетів дуже багато, але для початку нам потрібен лише один - http, який відповідає за роботу з мережею на основі HTTP. З опису ми розуміємо, що методів у нього багато і розглядати кожен новачку просто немає змісту, замість цього давайте напишемо практичний приклад запуску HTTP-серверу:
/* index.js */
// імпорт модуля "http"
const http = require('http');
const port = 3000;
// коллбек на кожен HTTP-запит
const requestHandler = (request, response) => {
console.log(request.url)
console.log(request)
response.end('Ping-Pong')
};
// створення HTTP-серверу
const server = http.createServer(requestHandler);
// початок прослуховування HTTP-сервера
server.listen(port, (err) => {
if (err) {
return console.log('Помилочка вийшла', err)
}
console.log(`Сервер запущен по адресу http://localhost:${port}`)
});
Браузер завжди відправляє GET запит на вказанний URL і в доповнення запитує favicon.ico, тому що вважає будь-яку адресу сторінкою.
Логи серверу, які ми отримуємо завдяки функції requestHandler()
На правах реклами представляю Postman ― інструмент для зручної відправки HTTP-запитів на сервер. За його допомогою ми можемо легко змінювати URL, методи, заголовки та тіло запиту.
Ми зробили працюючий сервер! Ура! Він дуже простий і не розуміє різницю між GEt та POST запитом, не знає як працювати з тілом запиту і багато чого іншого, але він же перший, і ми ❤️ його просто за це, так? Давайте навчимо сервер зберігати всі логи у файл, а також розглянемо ще один пакет fs, призначений для роботи з файловою системою:
/* index.js */
// імпорт модуля "http"
const http = require('http');
// імпорт модуля "fs"
const fs = require('fs');
const port = 3000;
// створення потоку для запису у файл
const logFile = fs.createWriteStream('log.txt', { flags: 'a' });
// коллбек на кожен HTTP-запит
const requestHandler = (request, response) => {
console.log(request.url);
// запис до файлу
logFile.write(`Запрос по адресу: ${request.url}\n`);
response.end('Ping-Pong')
};
// створення HTTP-серверу
const server = http.createServer(requestHandler);
// початок прослуховування HTTP-сервера
server.listen(port, (err) => {
if (err) {
return console.log('Ошибочка вышла', err)
}
console.log(`Сервер запущен по адресу http://localhost:${port}`)
});
А що буде, якщо запис до файлу ще не закінчений, а на сервер прийде інший запит? Забігаючи наперед, скажу що операції роботи з файловою системою є асинхроними потоками(stream) і враховуючи однопотоковість JS вони повинні блокувати сервер до завершення. Але це не так, і ми можемо переконатися у цьому бомбардуючи сервер запитами, а він не відмовить і навіть не затримає жоден з них.
Кільце в центрі - це одиничний потік JS і він завжди виконує тільки одну операцію. Зліва ми бачимо чергу запитів, очікуючих свого виконання. При попаданні у потік синхроного запиту, він починає опрацьовуватися і по завершенню дає старт наступному запиту. У випадках, коли запит викликає асинхроні операції, він делегується бібліотеці libuv, яка опрацьовує його в окремому потоці, а по завершенню, викликає коллбек у ту ж саму чергу запитів.
Я намагався пояснити як можна простіше, але не впевнений що цього достатьньо, тому ось більш детальна стаття "Пояснення роботи EventLoop у JavaScript"
Знижуємо висоту і повертаємось до нашого коду. Для виконуваних ним операцій він виглядає занадто громіздким, тому що оперує занадто низькорівневими операціями. Про те, як перейти на більш високий рівень абстракцій за допомогою Express.js ми поговоримо у наступному блоці.
Основи Express.js
Difficulty: Як рівень в SuperMario 🍄 ― щоб пройти потрібно декілька раз програти. Objectives: Встановити Express.js і запустити з його допомогою HTTP-сервер.Express.js - це пакет, що надає безліч можливостей, що спрощують життя при розробці на Node.js. З його допомогою підвищується рівень абстракції і нам не потрібно займатися налаштуванням мережевого з'єднання, парсити заголовки, методи та тіло запиту, і ще багато чого іншого. Для початку давайте встановимо його в проект за допомогою npm.
npm i express
Створимо новий файл і напишемо в ньому код для базового HTTP-сервера:
/* express.js */
// імпорт express
var express = require('express')
// ініціализація express-додатку
var app = express()
// створення функції слухача для GET-запитів за адресою "/"
app.get('/', function (req, res) {
res.send('Hello World')
})
// створення функції слухача для POST-запитів за адресою "/test"
app.post('/test', function (req, res) {
res.send('Hello World POST')
})
// ввімкнення серверу
app.listen(3000)
node express.js
Все працює! Якщо спробувати відправляти запити неописані у коді (наприклад GET: /users), то ми будем отримувати у відповідь 404-сторінку. Для того, щоб написали щось подібне на нодах, нам би було необхідно приблизно 200 рядків коду. Вражає? Рухаємось далі.
Express.js розширює звичні об'єкти request та response з ноди, додаючи велику кількість корисних штук прямо з коробки. Наприклад, він парсить query-параметри в об'єкт і може брати частини URL як параметри. У response з'являються зручні методи для відповіді:
app.get('/:value', function (req, res) {
console.log(req.query); // об'єкт query-параметрів
console.log(req.params.value); // значення value, що знаходиться у URL, на вказанній позиції '/:value'
res.status(200).json({ message: 'Hello World' }); // метод "status()", встановлює статус код для відповіді, а "json()" поставить заголовки Content-Type для формату JSON
})
5 рядків, а скільки користі! Цим і підкорив Express.js світову славу. На даний момент для express нараховується величезна кількість npm-пакетів, покликаних розв'язувати різноманітні кейси. Давайте розглянемо один з таких - парсинг тіла запиту. Для цього додамо пакет (офіційно визнаний Best Practice) body-parser.
npm i body-parser
/* express.js */
// імпорт express
var express = require('express')
// імпорт body-parser
var bodyParser = require('body-parser');
// ініціализація express-приложения
var app = express()
// parse application/x-www-form-urlencoded
app.use(bodyParser.urlencoded({ extended: false }))
// parse application/json
app.use(bodyParser.json())
// створення функції слухача для GET-запитів за адресою "/"
app.get('/', function (req, res) {
console.log(req.query);
console.log(req.params);
res.send('Hello World')
})
// створення функції слухача для POST-запитів за адресою "/test"
app.post('/test', function (req, res) {
console.log(req.body);
res.send(`Hello World ${req.body.name}`)
})
// ввімкнення серверу
app.listen(3000)
node express.js
Відправляємо POST-запит, з тілом:
Дивимось логи:
Це достатня основа в Express.js для новачка, далі ми можемо подивитися на те як масштабується весь цей код в реальному житті.
Проект
Difficulty: Як наблюдати за малюючим художником ― поки дивишся прикольно. Objectives: Зрозуміти структуру типового проекту на Node.js/Express.jsСамий сік 🍎. Тут ми поговоримо про структуру проекту, що, де, куди й чому. У своїй практиці, для швидкого старту проекту, я зазвичай використовую express-generator, який створений командою Express і створює шаблонний стартер на Express.js. Він встановлюється на машину глобально, щоб бути доступним у будь-якому місці і проекті. Для цього до команди інсталяції потрібно додати флаг -g
:
npm i express-generator -g
Версія:
Довідка:
Зверніть увагу на поле scripts
, у package.json, в ньому містяться npm-скрипти для роботи з додатком. Для того щоб запустити сервер нам необхідно буде використовувати команду npm start
.
1) видаляємо папку /public з усім вмістом
2) вносимо зміни у /routes/index.js
/* /routes/index.js */
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.send('Express');
});
module.exports = router;
3) вносимо зміни в app.js
/* app.js */
var express = require('express');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var usersRouter = require('./routes/users');
var app = express();
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use('/', indexRouter);
app.use('/users', usersRouter);
module.exports = app;
Зараз у нашому проекті реалізований тільки один шар routes, який повинен відповідати ТІЛЬКИ за роботу з HTTP-запитами.
Чому так радикально? ... повинен відповідати ТІЛЬКИ за роботу .... Грамотне розділення додатку за шарами - це запорука коду, який буде легко підтримувати за рахунок зручності роботи та зрозумілої структури. Якщо шар працює тільки з HTTP-запитами, то у ньому не повинно бути ніякої бізнес-логіки або обробки даних і навпаки.
Давайте зробимо наш сервер розумним і подаруємо йому логіку, бізнес-логіку. У світі Express-додатків за них повинен відповідати шар, що називається services, і, оскільки, він не генерується автоматично, ми створимо його самі.
1) Створюємо папку /services
2) Створюємо файл /services/user.service.js
3) Додаємо функцію у файл та експортуємо її:
/* services/user.service.js */
const getName = (user) => {
if (user) {
return user.name;
} else {
return null;
}
};
module.exports = {
getName
};
4) Імпортуємо функцію з сервіса до роутера, використовуємо її для отримання імені.
/* routes/users.js */
var express = require('express');
var router = express.Router();
const { getName } = require("../services/user.service");
router.get('/', function(req, res, next) {
res.send(`Welcome!`);
});
router.post('/', function(req, res, next) {
const result = getName(req.body);
if (result) {
res.send(`Your name is ${result}`);
} else {
res.status(400).send(`Some error`);
}
});
module.exports = router;
5) Запускаємм сервер npm start
6) Відправляємо POST-запит на /users і дивимось на відповідь, що містить передане им'я
Сервер вміє щось робити, а що як ми хочему зберігати дані? Для цього існує шар repositories, і ми його зараз реалізуємо.
1) Створюємо папку /repositories
2) Створюємо файл /repositories/user.repository.js
3) Додаємо функцію у файл та експортуємо її:
/* repositories/user.repository.js */
const saveData = (data) => {
// код для збереження даних у БД
if (data) {
console.log(`${data} is saved`);
return true;
} else {
return false;
}
}
module.exports = {
saveData
};
Ми не розглядаємо Бази Даних у цій лекції, тому наш код є лише імітацією роботи з нею, насправді в цьому шарі повинна відбуватися вся робота з даними: їх отримання зі сховища, запис та оновлення.
4) Імпортуємо функцію із репозиторія у сервіс та додаємо функцію saveName()
:
/* services/user.service.js */
const { saveData } = require("../repositories/user.repository");
const getName = (user) => {
if (user) {
return user.name;
} else {
return null;
}
};
const saveName = (user) => {
if (user) {
return saveData(user.name);
} else {
return null;
}
};
module.exports = {
getName,
saveName
};
5) Імпортуємо функцію з сервіса в роутер та використовуємо її для роботи з тілом запиту:
/* routes/users.js */
var express = require('express');
var router = express.Router();
const { saveName } = require("../services/user.service");
router.get('/', function(req, res, next) {
res.send(`Welcome!`);
});
router.post('/', function(req, res, next) {
const result = saveName(req.body);
if (result) {
res.send(`Name is saved ― ${result}`);
} else {
res.status(400).send(`Some error`);
}
});
module.exports = router;
6) Запускаємо сервер npm start
7) Надсилаємо POST-запит на /users і дивимось на відповідь "Name is saved ― true", тому що функція з репозиторія повернула таке значення
І останній шар - це middlewares, приклади якого ми вже бачили в цьому проекті. Давайте розберемося як він влаштований і що він робить.
1) Створюємо папку /middlewares
2) Створюємо файл /middlewares/auth.middleware.js
3) Додаємо функцію у файл та експортуємо її:
/* middlewares/auth.middleware.js */
const isAuthorized = (req, res, next) => {
if (
req &&
req.headers &&
req.headers.authorization &&
req.headers.authorization === 'admin'
) {
next();
} else {
res.status(401).send()
}
};
module.exports = {
isAuthorized
}
middlewares — виконують операції з даними, що проходять крізь них та відправляються їх далі, або переривають потік. Функція middleware приймає три аргументи: (req, res, next). З першими двому ми вже знайомі - це об'єкти запиту та відповіді, а третій - це функція коллбек, яка говорить, що усі поточні операції закінчені і можна переходити до наступного кроку.
4) Імпортуємо мідлвер до роутеру і використовуємо його для роботи з тілом запиту:
/* routes/users.js */
var express = require('express');
var router = express.Router();
const { saveName } = require("../services/user.service");
const { isAuthorized } = require("../middlewares/auth.middleware");
router.get('/', function(req, res, next) {
res.send(`Welcome!`);
});
router.post('/', isAuthorized, function(req, res, next) {
const result = saveName(req.body);
if (result) {
res.send(`Your name is ${result}`);
} else {
res.status(400).send(`Some error`);
}
});
module.exports = router;
Щоб ініціалізувати middleware можна використовувати app.use
синтаксис, або ж у випадку з роутінгом просто передати його другим аргументом.
5) Запускаємо сервер npm start
6) Відправляємо POST-запит на /users з заголовком Authorization: user
і дивимось на відповідь 401
7) Відправляємо POST-запит на /users с заголовком Authorization: admin
і отримуємо відповідь, тому що ми пройшли перевірку
І це все, що стосується основ у структурі Node.js-застосунку. Тепер ти тру фулстек 😎
Висновок
Difficulty: Як прохолодна вода у літній день ― довгоочікуване полегшення. Objectives: Почитати, відпочити, почекати півгодини й почати лекцію спочаткуВсе, вітаю! Лекція повністю проскролена, більше нічого не потрібно читати. Швиденько переходь на вкладки, які ти повідкривав під час лекції (сподіваюсь не Facebook), і ознайомлюйся з додатковими матеріалами. Потім - дай собі відпочити. Півгодинки... так-так, дедлайни й таке інше, але це буде трохи більш ефективно, якщо ти даси перепочинок своїй голові, для того, щоб розкласти все по поличкам, а потім повертайся назад і приступай до домашнього завдання.
Бонус
Difficulty: Як на кухні ― все виходить, якщо дотримуватись інструкцій Objectives: Задеплоїти Node.js додаток на HerokuМені тут розповіли, що ви все вмієте деплоїти клієнтські додатки, так чому б не навчитися публіковати сервери на Node.js 😉?