Введение
Добрый день, дорогой %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— Нужно для хранения сессий в MongoDBexpress-session— Для сессий.image-type— Для валидации картинок при загрузке.mongoose— Очевидно для удобного доступа к MongoDBmongoose-unqiue-validator— Для того что бы указать что в модели данные должны быть уникальными (e.g username, email, etc)nodemon— Во время разработки автоматически перезагружает наш сервер при сохранении файла.passportpassport-local— Полезные модули для авторизации!requestrequest-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/
Добавить комментарий