Робимо проект на Node.js з використанням Mongoose, Express, Cluster. Частина 1

Введення
Добрий день, дорогий %username%! Сьогодні ми будемо описувати створення каркаса додаток за типом MVC на Node.js з використанням кластерів, Express.js і mongoose.
Завдання — підняти сервер який має кілька особливостей.
  • Працює в кілька асинхронних потоків.
  • Сесійний інформація буде загальною для всіх потоків.
  • Підтримка HTTPS.
  • Авторизація.
  • Легко масштабується.
Стаття написана новачком для новачків. Буду радий будь-яким зауважень!
З чого почати? Встановити Node.js (з яким йде npm). Встановити MongoDB (+ Додати у PATH).
Тепер створення NPM проекту для того що б не тягнути всі залежності в наш git!
$ npm init

Вам належить відповісти на декілька питань (можна пропустити все просто натискаючи за Enter-у). Іноді npm багует і записавши package.json не завершується.
Далі! Запишемо наш
index.js

// Project/bin/index.js
process.stdout.isTTY = true;
// Змусимо думати node.js що мій улюблений git bash це консоль!
// Дивіться https://github.com/nodejs/node/issues/3006

var cluster = require('cluster');
// завантажимо кластер
if(cluster.isMaster)
{
// якщо ми <<master>> то запустимо код з гілки майстер
require('./master');
}
else
{
// Якщо ми <<worker>> запустимо код з гілки для worker-a
require('./worker');
}

Трохи про кластери.
Що таке кластер? Кластер-це система програм які де є дві ролі: Головна роль (master) і робоча роль (worker). Є один майстер на який приходять всі запити, і n-ое кількість робочих (в коді
CPUCount
).
Якщо приходить запит до сервера, то майстер вирішує якого робочого дати цей запит. При створенні робочого node народжує процес який запускає той же код, який зараз запущений і створює IPC. Коли відбувається з'єднання TCP/IP майстер віддає
Socket
одному з робітників за певною політики (докладніше тут) через IPC.
Повернемося до коду. Що там трапилося з master-му і worker-му? Код майстра:
//Project/bin/master.js
var cluster = require('cluster');
// Завантажимо нативний модуль cluster

var CPUCount = require("os").cpus().length;
// Отримаємо кількість ядер процесора
// Створення дочірнього процесу вимагає багато ресурсів. Тому в зв'язці з 8 ядерних сервером і Nodemon-ом дає пекельні лаги при збереженні.
// Рекомендую при активній розробці ставити CPUCount в 1 інакше ви будете страждати як я....

cluster.on('disconnect', (worker, code, signal) => {
// У разі відключення IPC запустити нового робочого (ми довідаємося про це детальніше далі)
console.log(`Worker ${worker.id} died`);
// запишемо в лог відключення сервера, що розробники звернули увагу.
cluster.fork();
// Створимо робочого
});

cluster.on('online', (worker) => {
//Якщо робочий з'єднався з нами запишемо це в лог!
console.log(`Worker ${worker.id} running`);
});
// Створимо робітників у кількості CPUCount
for(var i = 0; i < CPUCount; ++i)
{
cluster.fork(); // Народити робочого! :)
}

Про arrow function, online, disconnect, шаблонні рядка
Що далі? Далі робочий! Тут ми будемо писати один код. Потім я буду говорити що ми пропустили і додавати його :) АЛЕ перед цим для початку завантажимо залежності npm!
$ npm i express apidoc bluebird body-parser busboy connect-mongo cookie-parser express-session image-type mongoose mongoose-unique-validator nodemon passport passport-local request request-promise --save

Навіщо нам кожен модуль?
  • Express
    — Думаю зрозуміло.
  • apidoc
    — Зручно для документування API (необов'язково)
  • Bluebird
    — Promise-и які вони є :). Він Нам знадобиться т. к. в стандартних Promise-ах на 4.х.х був баг з-за чого виникав memory-leak. Також mpromise від якого mongoose залежить більше не підтримується. Нам доведеться змусити mongoose використовувати наш Bluebird.
  • body-parser
    — Підтримка json в запитах з тілом (body)
  • busboy
    — Підтримка form-data у запитах з тілом.
  • cookie-parser
    — Простий модуль для куків (Cookies).
  • connect-mongo
    — Треба для зберігання сесій в MongoDB
  • express-session
    — Для сесій.
  • image-type
    — Для валідації картинок при завантаженні.
  • mongoose
    — Очевидно для зручного доступу до MongoDB
  • mongoose-unqiue-validator
    — Для того що б вказати що в моделі дані повинні бути унікальними (e.g username, email, etc)
  • nodemon
    — Під час розробки автоматично перезавантажується наш сервер при збереженні файлу.
  • passport
    passport-local
    — Корисні модулі для авторизації!
  • request
    request-promise
    Для тестування нашого коду!
І так? напишемо скрипт для запуску nodemon. В package.json додамо (замінимо якщо є такий field)
"scripts":{
"start":"nodemon bin/index.js"
}

Для запуску будемо використовувати
$ npm start

В подальшому ми додамо тести, документацію.
Тепер повернемося до Worker-у. Для початку запустимо Express!
var express = require('express');
// Завантажимо express
var app = express();
// Створимо новий сервер

app.get('/',(req,res,next)=>{
//Створимо новий handler який сидить по дорозі `/`
res.send('Hello, World!');
// Відправимо привіт світу!
});

// Запустимо сервер порту на 3000 і повідомимо про це в консолі.
// Всі Worker-и повинні мати один і той же порт
app.listen(3000,function(err){
if(err) console.error(err);
// Якщо є помилка повідомити про це
// Додаток закриється оскільки немає більше handler-ів
else console.log(`Running server at port 3000!`) 
// Інакше повідомити, що ми успішно з'єдналися з майстром
// І чекаємо повідомлень від клієнтів
});

Це все? Немає. Насправді є кілька речей які ми забули про налаштування Express-а. Виправимо це. Адже Нам потрібні файли для лицьової частини? (Front-end). Так додамо їх підтримку! Створимо папку
public
весь зміст якого буде доступний за адресою
/public
. У нас є два варіанти. Поставити NGINX і не ставити його. Найпростіший варіант не ставити його. Будемо використовувати те що вбудовано в express.
Альтернативний варіант використовувати NGINX в якості майстра, який ще й буде брати на себе відповідальність за статичні файли. Залишимо це на якийсь час, хоч і це допоможе з продуктивністю і масштабуванням.
Перед
app.get('/')
. Додамо наступне:
//....
var path = require('path');
// app = express(); тут ініціалізація сервера
// після
// Промонтировать файли з project/public в наш сайт за адресою /public
app.use('/public',express.static(path.join(__dirname,'../public')));
//...

Це все? ЗНОВУ НІ! Тепер до вхідних даних. Як ми будемо отримувати вхідні дані?
var bodyParser = require('body-parser');
//..
/// app.use(express.static(.........));
// JSON Парсер :)
app.use(bodyParser.json({
limit:"10kb"
}));
//...

Тепер до кукам
// JSON Парсер
// ...
// Парсер Куки!
app.use(require('cookie-parser')());
// ...

Але це не все! Далі нам потрібно змусити працювати
Mongoose
бо ми будемо працювати з сесіями! Запустимо MongoDB командою
$ mkdir database
$ mongod --dbpath database --smallfiles

Що ж тут відбувається? Ми створюємо папку database де зберігається дані сервера. Не забудьте додати теку
.gitignore
. Потім ми запускаємо MongoDB вказуючи на теку
database
як сховище. І що б файли були маленькими передаємо параметр
--smallfiles
, хоча навіть у такому випадку MongoDB буде зберігати логи розміром 200МБ в папці
./database/journal

Також у другій частині буде туторіал як підняти пропускну здатність MongoDB, та встановити його як сервіс в
systemd
під Ubuntu.
Тепер до коду. У файлі worker.js в початок файлу відразу після завантажень модулів вставимо наступне
require('./dbinit'); // Ініціалізація датабазы

Створюємо файл
dbinit.js
в папці
bin
. У який вставляємо такий код:
// Ініціалізація датабазы!
// Завантажимо mongoose
var mongoose = require('mongoose');
// Замінимо бібліотеку Обіцянок (Promise), яка йде в поставку з mongoose (mpromise)
mongoose.Promise = require('bluebird');
// На Bluebird
// Підключимося до сервера MongoDB
// Надалі адресу сервера буде завантажуватися з конфіги
mongoose.connect("mongodb://127.0.0.1/armleo-test",{
server:{
poolSize: 10
// Поставимо кількість підключень в пулі
// 10 рекомендована кількість для мого проекту.
// Вам можливо знадобиться і менше...
}
});

// У разі помилки буде викликано дана функція
mongoose.connection.on('error',(err)=>
{
console.error("Database Connection Error: " + err);
// Скажіть адміну нехай включить MongoDB сервер :)
console.error('Адмін сервер MongoDB Запусти!');
process.exit(2);
});

// Ця функція буде викликано коли буде встановлено підключення
mongoose.connection.on('connected',()=>
{
// Підключення встановлено
console.info("Succesfully connected to MongoDB Database");
// Надалі тут ми будемо запускати сервер.
});

Тепер прив'яжемо сесії до датабазе. В
bin/worker.js
додамо наступне. На початок до завантаження модулів:
var session = require('express-session'); // Сесії
var MongoStore = require('connect-mongo')(session); // Сховище сесій в монгодб

І після парсера куків:
// Тепер сесія
// поставити хендлер для сесій
app.use(session({
secret: 'Химера Хирера',
// Замініть на що-небудь
resave: false,
// Пересохранять навіть якщо немає змін
saveUninitialized: true,
// Зберігати порожні сесії
store: new MongoStore({ mongooseConnection: require('mongoose').connection })
// Використовувати монго сховище
}));

Кілька пояснень щодо черговості підключень.
express.static('/public')
. Сидить на самому початку оскільки Браузери відправляють запити на файли паралельно і вони будуть відправляти запити з порожніми сесіями і ми будемо створювати їх тисячами.
Куки парсер і сесій потрібні на початку оскільки надалі вони будуть використовувати для авторизації. Після чого йдуть парсери тіла запиту. NOTE: Останні два можна поміняти місцями. Сервіс авторизації. Він повинен йти після парсерів і сесій т. к. користується ними, але перед контролерами т. к. вони використовують інформацію про користувача. Далі йдуть контролери, до них повернемося трохи пізніше.
Тепер обробник помилок. Він повинен йти останнім т. к. в документації Експрес так написано :)
У файлі
bin/worker.js
додамо перед
app.listen(.....);
наступне
// Обробник помилок
app.use(require('./errorHandler'));

Тепер створимо файл
errorHandler.js

// Все обробники помилок повинні мати 4 параметра, інакше вони будуть звичайними контролерами
module.exports = function(err,req,res,next)
{
// err завжди встановлений бо Express.js перевіряє була передана помилка чи ні, і викликає обробники тільки якщо помилка є;
console.error(err);
// Надалі ми будемо відправляти помилки поштою, записувати в файл і так далі.
res.status(503).send(err.stack || err.message);
// Тут можна викликати next() або самим повідомити про помилку клієнту.
// У майбутньому можна зробити сторінок 503 з помилкою
};

Практично закінчили роботу з Worker-му. Але нам ще потрібно налаштує моделі і їх завантаження.
Створимо папку
models
де будуть зберігатися наші моделі. Надалі у нас будуть ще міграції з допомогою яких ми будемо мігрувати з однієї версії датабазы на нову.
Створимо в папці
models
файли
index.js
Та
user.js
. Таким чином
index.js
ми запишемо завантаження всіх моделей і їх Експорт, а файл
user.js
буде містити модель з Mongoose-а з деякими методами і функціями прив'язаних до моделі. Про моделі можна почитати на сайті Mongoose або документації.
В
index.js
записуємо:
module.exports = {
// Завантажити модель юзера (користувача)
// На *nix-ах всі файли чутливі до регістру
User:require('./User')
};
// Не забудемо точку з запЕтой!

А
user.js
записуємо:
// Завантажимо mongoose т. к. нам потрібно кілька класів або типів для нашої моделі
var mongoose = require('mongoose');
// Створюємо нову схему!
var userSchema = new mongoose.Schema({
// Логін
username:{
type:String, // тип: String
required:[true,"usernameRequired"],
// Це поле обов'язково. Якщо його немає вивести помилку з текстом usernameRequired
maxlength:[32,"tooLong"],
// Максимальна довжина 32 Unicode символ (Unicode symbol != byte)
minlength:[6,"tooShort"],
// Занадто короткий Логін!
match:[/^[a-z0-9]+$/,"usernameIncorrect"],
// Мій любимй формат! ЗАБОРОНИТИ НИЖНЄ ТИРЕ!
unique:true // Воно повинно бути унікальним
},
// Пароль
password:{
type:String, // тип String
// Надалі ми додамо сюди хешування
maxlength:[32,"tooLong"],
minlength:[8, "tooShort"],
match:[/^[A-Za-z0-9]+$/,"passwordIncorrect"],
required:[true,"passwordRequired"]
// Думаю тут все вже очевидно
},
// Тут будуть і інші поля, але зараз ще рано їх сюди ставити!
});

// Тепер підключимо плагіни (модулі)

// Компілюємо і Експортуємо модель
module.exports = mongoose.model('User',userSchema);

Тепер розберемося з образами (Спроба перевести
view
) та контролерами. Створимо дві папки:
controllers
та
views
. Тепер виберемо потрібну нам бібліотеку для фонового (промальовування, відображення, компіляція, заповнення) образів. Для мене вкрай простим виявилася mustache. Але для того що б було легко змінювати движок рендеринга я використовую консолідація.
$ npm i консолідація mustache --save

Консолдейт вимагає що б движки використовувані проектом були встановлені, тому не забудьте після того як поміняєте движок його встановити. Тепер вставимо замінимо весь
app.get('/');
на

// Використовуємо движок усов
app.engine('html', cons.mustache);
// встановити движок рендеринга
app.set('view engine', 'html');
// папка з образами
app.set('views', __dirname + '/../views');

app.get('/',(req,res,next)=>{
//Створимо новий handler який сидить по дорозі `/`
res.render('index',{title:"Hello, world!"});
// Відправимо рендер образу під ім'ям index
});

Тепер в папці
views
додаємо наш index.html куди записаваем
{{title}}

Заходимо на
127.0.0.1:3000
і бачимо
Hello, World!
. Перейдемо до контролерів! Видалимо рядка
app.get(.................)
. Тепер нам належить завантажити контролери. (Які знаходяться в папці
controllers
). Замість нашого віддаленого коду вставляємо наступне.
app.use(require('./../controllers')); // Монтуємо контролери!

файл
controllers/index.js
записуємо
var app = require('express')();

app.use(require('./home'));
module.exports = app;

А в файл
controllers/home.js
записуємо:
var app = require('express')();

app.get('/',(req,res,next)=>{
//Створимо новий handler який сидить по дорозі `/`
res.render('index',{title:"Hello, world!"});
// Відправимо рендер образу під ім'ям index
});
module.exports = app;

На цьому кінець першої частини! Дякую за увагу. Багато чого залишилося без нашої уваги, і треба буде це виправити у другій частині. Тут є багато спірних моментів. Так само тут багато помилок, які будуть виправлені у другій частині. Код трохи пізніше буде викладений на github. Суть проекту пояснюється у другій частині.
Джерело: Хабрахабр

0 коментарів

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