Будуємо свій full-stack на JavaScript:


Друга стаття з серії про full-stack JS розробці.
JavaScript постійно змінюється, дуже складно погнатися за останніми технологіями, адже те, що було найкращою практикою півроку тому, зараз вже застаріло. Подібні твердження багато в чому правда, але слід зазначити, що це більше відноситься до клієнтського JavaScript. Для сервера все набагато стабільніше і грунтовніше.
Стаття базується на коді проекту Contoso Express.
Список ресурсів на якому можна більш детально ознайомитися з деякими темами статті тут.
Примхи JS
Мова JavaScript відомий своїми примхами (quirks). Наприклад, знамените 0.1 + 0.2 не дорівнює 0.3 у зв'язку з тим, що в JavaScript немає вбудованого десяткового типу.
console.log(0.1 + 0.2 === 0.3); //false

Або для того, щоб коректно перевірити, що значення змінної є рядком, потрібна така конструкція:
if (typeof myVar === 'string' || myVar instanceof String) {
console.log('That is string!');
}

Деякі з цих проблем усунені ES6 версії мови. Наприклад, областю видимості змінної, оголошеної через var є вся функція (у більшості інших мов змінна має блокову область видимості). У ES6 ця проблема вирішена оголошенням змінних через let/const, var не рекомендується до використання взагалі. При цьому вона залишається в JS (назавжди) для зворотної сумісності.
Для усунення інших недоліків мови JS використовуються сторонні бібліотеки. Наприклад, для точних десяткових обчислень можна скористатися "big.js". Є безліч дрібних пакетів, кожен вирішує одну подібну задачу, але пам'ятати їх назви і підключати по одному до проекту занадто складно. Тому зручніше користуватися такими універсальними рішеннями "lodash", надають відразу цілий набір додаткових утилитных функцій.
На сайт lodash відмінна документація з прикладами, при цьому, не обов'язково дивитися всі відразу, ви можете швидко ознайомиться з тим що є і дивитися надалі тільки те, що вам конкретно потрібно.
Наприклад, перевірити що змінна має значення рядка, з lodash набагато простіше:
if (_.isString(myVar)) {
console.log('That is string!');
}

Зверніть увагу, що в lodash є підтримка виклику ланцюжків функцій, наприклад:
let arr = [2, 3, 1, 5, 88, 7, 13];

let oddSquares = _(arr)
.filter(x => x % 2 === 1) //filter odd numbers
.sort((a,b) => a > b) //sort by default sorts lexicographically: [1, 13, 3, 7]
.map(x => x*x);

console.log(oddSquares.join(',')); //1,9,25,49,169

TypeScript на сервері
Чим складніше JS код, тим більше переваг дає використання TypeScript. Для складного серверного коду користь від TS швидко стає очевидною.
Contoso параметри компіляції TS для серверної частини знаходяться у файлі "tsconfig.json".
Ось деякі, на які варто звернути увагу:
  • rootDir/outDir: весь код (всі TS файли) з rootDir, компілюються в JS файли у outputDir, при цьому структура папок зберігається. Приміром файл '/server/helpers/emailHelper.ts' буде скомпільований в '/build/server/helpers/emailHelper.js'
  • sourceMap: якщо цей прапорець встановлений, то крім JS, створюються файли source maps (myModule.ts -> myModule.js + myModule.js.map). Це дозволяє налагоджувати файли TS, точки зупину (breakpoints) виставляються і спрацьовують в ts файлі, при цьому реально виконується JS код.
  • target: TS дозволяє компілювати код під різні версії JS. Але є кілька особливостей, які підтримуються тільки при компіляції в останню версію ES6. Одна з них — async/await, яка описана нижче. Для того, щоб її використовувати для сервера, код компілюється в ES6 і використовується 6x версія Node.
Якщо у вас встановлений TypeScript глобально, ви можете скомпілювати проект консольною командою tsc (TypeScript Compiler) з кореня проекту, опціонально з watch параметром. В режимі watch після будь-якої зміни у вихідному коді буде виконана повторна компіляція
tsc --watch

Для запуску програми треба виконати (через Node) "build/server/startServer.js".
Якщо ви не хочете використовувати TS, то ви можете легко конвертувати TS-код в JS. Основне, що треба зробити:
  • перетворити ES6 imports в Node require
  • прибрати анотації типів
  • використовувати промисы замість asyn/await або підключити babel для транспиляции
Асинхронність в JS
Асинхронне (async) програмування в JS один з найскладніших моментів при переході з інших мов програмування. Я даю дуже короткий огляд, прочитати більше можна в списку ресурсів.
Висока продуктивність Node обумовлена тим, що всі довго виконуються операції не блокують основний процес. Прикладом таких операцій може бути запит в базу даних або запис інформації у файл. Після виклику операції продовжує виконуватися інший код. Коли операція завершується (в майбутньому), або успішно або з якоюсь помилкою, потрібно вказати що робити далі.
Для роботи з async кодом можна використовувати кілька шаблонів. Ці шаблони змінювалися (еволюціонували разом з розвитком Node.
  • Колбэки (callbacks) — традиційний спосіб організації async в Node. Простий для розуміння, але має ряд проблем: складно робити кілька вкладених async викликів. У цьому випадку код стає складним у написанні, підтримці і розумінні (callback hell); повернення даних, обробка і генерація помилок принципово відрізняються від того, як це відбувається в синхронному JS коді.
  • Промисы (обіцянки/promises) — сучасний стандарт для async в JS. Зараз стає повсюдно використовуваним. Промис це конструкція, яка може знаходиться в одному з 3 станів очікування/відмову/виконано (pending/rejected/resolved). Спочатку стан очікування, але в майбутньому промис зобов'язується (обіцяє) перейти в один з фінальних станів помилки або успішного виконання (з результатом). Конструкції промисов дозволяють організувати ланцюжка викликів async операцій лінійно, що робить підтримку легше і покращує розуміння коду. При цьому дані повертаються через return, помилки генера через throw, і можна використовувати один обробник помилок для декількох async операцій.
  • async/await — це майбутнє async в JS. Зараз знаходиться у фінальній стадії stage4 обговорень після чого стане новим затвердженим доповненням JS. Async/awiat надалі стирає грань між синхронним і асинхронним кодом, так що вони гармонійно поєднуються разом. Код з використанням async/await максимально просто писати і підтримувати. Схожий синтаксис існує в інших мовах, наприклад в C# і Python. Недолік async/await в тому що потрібні додаткові інструменти для його використання, такі як TypeScript або Babel.
Є ще кілька async шаблонів, модуль "async" і модуль "co" з використанням генераторів. Це проміжні еволюційні етапи перед промисами і async/await. "co" можна використовувати без додаткових перетворень вже зараз, але на мою думку краще використовувати промисы, якщо використання async/await не представляється можливим.
Для успішної роботи з JS вам необхідно знати основні аѕупс шаблони, т. к. час від часу доведеться мати справу з кожним з них. Якщо, наприклад, ви працюєте з async/await, то вам все-одно потрібні промисы для деяких операцій, таких як паралельне виконання через Promise.all, якщо ви працюєте з промисами, то іноді доводиться обертати промисами функції на колбэках.
Обробка помилок
Це ще одна область викликає утруднення при переході на Node з інших платформ. Почнемо з того, що те, як потрібно обробляти помилку, залежить від того, який асинхронний патерн ви використовуєте. При використанні колбэков, помилка передається як перший параметр колбек функції, при використанні промисов, обробляти помилки потрібно через catch в ланцюжку промисов, при синхронному виклик коду і при використанні async/await слід використовувати try/catch.
Далі, треба знати що в JavaScript можна згенерувати (через throw) помилку передавши будь-який об'єкт, але при цьому правильної практикою є використання вбудованого об'єкта Error. У найпростішому вигляді це виглядає так:
throw new Error('Param value is wrong');

При цьому ви можете створювати свій кастомный об'єкт помилки успадкувавши його від вбудованого Error об'єкта.
function AppError(errorCode, data) {
this.code = errorCode;

Error.captureStackTrace(this, this.constructor);

//merge data props into error object
return _.merge(this, options); 
}

...

throw new AppError('user_not_found', {userId: 5});

Зверніть увагу на виклик Error.captureStackTrace, це потрібно для того, щоб коректно додати stack trace в об'єкт помилки. Ви можете додавати помилку свої дані і визначати свої сигнатури для конструктора помилки.
Contoso об'єкт AppError за умовчанням приймає рядкові параметри: тип помилки і код помилки, текст помилки сплачується за кодом з зовнішнього файлу.
Ще один момент: слід відловлювати та логировать необроблені помилки, якщо цього не робити, додаток буде завершувати роботу без чіткої вказівки причини, підключати обробник для необроблених помилок потрібно якомога раніше.
process.on('uncaughtException', (err) => {
console.log(`Uncaught exception. ${err}`);
});

Вибираємо бек-енду веб фреймворк
Express — основний вибір, всі альтернативи в десятки разів менш популярні. Express слід філософії мінімалізму і гнучкості. Мінімалізм в цьому контексті означає, що без додаткового налаштування доступна дуже невелика функціональність. Наприклад, щоб додати підтримку cookies, парсити body в HTTP запитах, мати доступ до сесії на клієнта, потрібно додати відповідний модуль. Кожну використовувану можливість потрібно активувати. Гнучкість означає, що у вас є багато можливостей зміни/доповнення існуючого функціоналу.
Ці особливості Express, роблять його відмінним вибором як для веб додатків так і для окремих API серверів.
Інші опції можна розділити на два види: повністю окремі фреймворки і ті, які базуються на Express.

Окремі:

  • Koa — новий фреймворк від творців Express. З одного боку це поліпшена версія Express, в якій деякі моменти зроблені краще, з іншого боку нічим принципово не відрізняється і не має зворотної сумісності з Express.
  • Hapi — повністю окремий фреймворк, спочатку був створений і фінансово підтримується Wallmart, найбільшою мережею супермаркетів у США. На відміну від Express, вимагає менше налаштування і більше функцій доступні відразу.

Базуються на Express:

  • Loopback — фреймворк заточеный API для написання серверів, особливо для мобільних пристроїв, є DSK для iOS/Android, використовується вбудована ORM для роботи з різними БД.
  • Sails — позиціонується як хороша основа для API сервера, особливо якщо є взаємодії в реальному часі через socket.io.
  • FeathersJs — цей фреймворк має безліч адаптерів для інших пакетів / фреймворків, метою є додати більше стандартизації та полегшити процес розробки.

вибрати

Почніть з Express, якщо є час і бажання подивіться інші варіанти. Особисто у мене не було достатньо мотивації розбиратися з іншими фреймворками, тому що я швидко зміг побудувати структуру на основі Express, яка мене повністю влаштовує. Перевага цього підходу — менша залежність від специфіки фреймворку, хоч це і вимагає спочатку більше роботи.
Структура проекту
Структура проекту Contoso базується на MVC архітектурі.
Контролери (controllers) — модулі в яких знаходяться обробники маршрутів, функції, що приймають стандартні express параметри запиту і відповіді. У контролерах є логіка валідації даних і базової обробки запиту, але вони безпосередньо не звертаються до бази даних і не виконують рутинних операцій, для цього використовуються репозиторії і хэлперы.
Репозиторії (repositories) — тут знаходиться логіка доступу даних (БД) і додаткова бізнес логіка, частина бізнес-логіки може бути в контролері, але репозиторій краще для цього.
Помічники (helper) — виконують специфічні операції — відправку імейлів, обробку помилок, логування, і т. д.
Маршрутизатори (routers) — проставляють відповідності між маршрутами (URLs) і відповідними їм обробниками з контролерів. При цьому не робиться особливої різниці між маршрутами для API методами і маршрутами для веб сторінок (views).
Завдання (tasks) — утилитные скрипти для таких завдань, як створення початкової бази або імпорт даних з файлу.
Подання (views) — серверне подання (HTML шаблон) підтримують загальні шаблони (layout views) і часткові шаблони (partial views). Contoso серверні представлення використовуються для сторінок аутентифікації, інші HTML подання генеруються на клієнта.
У класичній архітектурі MVC окремо визначаються моделі, тут модель це просто об'єкт з даними, який створюється в контролері і використовується при генерації подання.
Express — гнучкий фреймворк і дозволяє організовувати код, як вам зручно. При цьому розробник сам вирішує, яка структура йому підходить. Немає єдино правильної структури, але однозначно добре, коли вона є в принципі.
Багато функцій Express і сторонніх пакетів використовуються через обгорткові модулі. Це зручно, так як дозволяє легко замінити пакет у майбутньому або замінити стандартний функціонал.
Конфігурація
Конфігурація зазвичай зберігається у фізичних файли в піддиректорії проекту. В Node, на відміну від інших платформ, JSON використовується частіше ніж XML.
Є кілька пакетів для роботи з конфігураційними значеннями, я віддаю перевагу "config" інша популярна опція "nconf".
В "config" дані вичитуються з декількох конфігураційних файлів за певними правилами. Значення за замовчуванням зберігаються у файлі "default.json", вони можуть перевизначатися значеннями з файлу "local.json". Крім формату JSON для зберігання конфігурації можна використовувати багато інші формати, такі як yaml/xml/hjson/js і т. д. Детальніше про це тут.
В репозиторій додається файл налаштувань за замовчуванням "default.json", а файл "local.json" в якому їх можна змінити виключається з репозиторію .gitignore.
Для зберігання конфігурації можна використовувати кілька стратегій:
  • налаштування розробника за замовчуванням — в конфігурації (default файл) зберігаються налаштування які підходять для локальної розробки (можна перевизначити в local файлі). В продакшені потрібно змінити настройки за замовчуванням default файлу, налаштуваннями продакшену (local файл або файл з ім'ям сервера).
  • налаштування продакшену за замовчуванням — тут навпаки, у конфігурації за замовчуванням зберігаються налаштування продакшену, а розробник локально перевизначає параметри для розробки.
Перший варіант кращий — по-перше, додавати налаштування для продакшену в репозиторій не дуже правильно в плані безпеки, по-друге, часто буває кілька середовищ для деплоймента production/uat/test, для яких потрібні різні конфігураційні значення. Для більшої безпеки можна в продакшені не використовувати default файл взагалі, повністю задавши конфігураційні значення local файл (докладніше в статті по розгортці).
Contoso конфігурація використовується через обгортковий модуль 'server/config'.
Логування
Для простого дебага, я користуюся console.log, але якщо потрібно вивести в логи помилку або якусь допоміжну інформацію слід скористатися однієї з бібліотек логування.
Є кілька популярних пакетів логування: "winston", "bunyan" і "log4js". Я користуюся найпопулярнішою "winston" за принципом "від добра добра не шукають", у мене вийшло без проблем итнегрировать winston і там є все, що мені було потрібно.
Ви можете порівняти і вибрати бібліотеку, яка вам найбільше сподобається. В додаткових ресурсах є стаття з порівнянням winston vs bunyan.
Contoso логування використовується через обгортку loggerHelper. Логи зберігаються в '/data/logs' лог для помилок і лог для діагностичних повідомлень записуються в різні файли.
Валідація даних
Дані, які потрапляють в додаток з поза, потрібно перевіряти на коректність. У разі простого веб додатки основне джерело вхідних даних веб запити: дані HTML форм або клієнтські AJAX запити.
Валідувати дані можна на різних рівнях, у Contoso валідація виконується на рівні контролера і, передбачається, що в репозиторій надходять вже коректні дані. У більш складних додатках, може мати сенс додатково перевіряти дані в репозиторії, безпосередньо перед тим, як буде измненено стан БД.
Для більш ефективної перевірки вхідних даних можна скористатися пакетом "joi". Це плагін для hapi (альтернатива express), який працює незалежно.
Дані потрапляють в контролер в тілі запиту (req.body) або в параметрах query string (req.query). Це довільний JS об'єкт в якому може бути що завгодно. Ми ж очікуємо отримати певний набір параметрів, кожен з яких має певний тип, допустимий набір значень, може бути відсутнім або повинен бути обов'язково, т. д.
Щоб повірити дані на коректність потрібно обявить Joi схему описують очікувані дані і виконати перевірку на відповідність:
let schema = {
id: Joi.number(),
number: Joi.number().required(),
title: Joi.string().required()
};

let obj = {
number: 8,
title: 'Gone with the Wind'
};

Joi.validate(obj, schema, {/*options*/}, (err, val) => {
//...
});

Contoso Joi використовується через виклик методу loadSchema в controllerHelper. Цей метод обертає Joi.validate в промис і генерує зрозумілу додатком помилку валідації.
Відправка імейлів
Для відправки імейлів, я використовую "nodemailer" він популярний і добре підтримується.
Ви можете використовувати різні режими транспорту (те, яким способом імейл буде пересилатися). За замовчуванням використовується direct режим.
Цей режим підходить для роботи над прототипами проектів, тому що не вимагає окремого SMTP сервера або стороннього сервісу відправки імейлів такого як Amazon SES або SendGrid.
Недоліком є те, що в залежності від вашого IP адреси, листи можуть опинитися в спам директорії.
Більше за різними видами транспорту тут.
Для генерації самого вмісту імейлів у вигляді HTML використовуються пакет "email templates" з "handlebars" шаблонами. Contoso імейли пересилаються через "/helpers/emailHelper", темплейти імейлів зберігаються в "/data/emails".
Автентифікація
Passport — самий популярний пакет для аутентифікації Node. Він підтримує безліч різних механізмів аутентифікації званих стратегіями. Зазвичай в проекті є локальна стратегія, яка використовує традиційний механізм доступу до системи через логін/пароль і зберігає дані в базі даних програми. Додатково ви можете надати можливість доступу до системи через SSO (single sign-on) провайдери, такі як google/facebook/twitter або використовувати особливі види аутентифікації, як наприклад Windows Active Directory.
Для локальної стратегії, крім форми, де користувач вводить логін/пароль, потрібно надати форму для реєстрації нового профілю юзера, реалізувати механізм активації нового профілю (відправити email з посиланням для активації), підтримувати випадки коли користувач забув пароль або хоче його змінити.
Існує кілька сторонніх авторизаційних платформ (Auth0 або Stormpath), які дозволяють використовувати їх інфраструктуру для всього, що пов'язано з управлінням профілями користувачів.
Це однозначно може заощадити багато часу розробника, але має мінуси, такі як додаткова вартість і залежність від стороннього сервісу (наскільки йому можна довіряти).
Ще одна хороша альтернатива Google Firebase. Нещодавно цей сервіс був кардинально оновлений. Це не просто база даних, а ціла платформа для розробки, в якій аутентифікація доступна безкоштовно і може бути використана навіть якщо це єдине, для чого ви будете використовувати Firebase.
Contoso реалізована аутентифікація з локальною стратегією і SSO з google/facebook. Для того щоб працювала SSO аутентифікації, потрібно додати свої clientID/clientSecret в конфігурацію, там же аутентифікацію опціонально можна відключити.
Що далі?
У наступній статті я розповім про особливості роботи з JS на клієнті. На клієнті JS бурхливо розвивається, постійно з'являються нові фреймворки, бібліотеки та інструменти розробки, які частково витісняють існуючі. Не встигли ви ознайомитися з черговою технологією, як вона вже застаріла. До речі, говорячи про це, днями нарешті вийшов Angular 2.0. Ласкаво просимо в клуб, новий фреймворк!
Буду радий зауважень коментарям.
Stay tuned!
Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.