Делаем проект на 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 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. Но для того что бы было легко менять движок рендеринга я использую consolidate.

$ npm i consolidate 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;

На этом конец первой части! Спасибо за внимание. Многое осталось без нашего внимания, и надо будет это исправить во второй части.

ссылка на оригинал статьи https://habrahabr.ru/post/314394/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *