1 декабря 2016 в 11:39

Делаем проект на Node.js с использованием Mongoose, Express, Cluster. Часть 2.1

Введение


Здраствуйте, дорогие хабровчане! Сегодня у нас в основном будут маленькие изменения, но изменений много. В этой части мы будем:


  • Создавать свой логгер
  • Записывать в лог запросы и время их обработки
  • Исправлять ошибки, которые мы допустили в первой части.
  • Разбираться с авторизациеей
  • Разбираться с некоторыми классами
  • Конфиги!

Логи


Для логов мы будем использовать самодельный модуль. Создадим папку logger. В нем будет файл index.js.


var stackTrace = require('stack-trace');  // Для получения имени родительского модуля
var util = require('util'); //util.inspect()
var path = require('path'); //path.relative() path.sep
var projectname = require('../package').name; //package.json -> project name

module.exports = class Logger // Класс логера :)
{
    constructor()
    {
        function generateLogFunction(level) // Функция генератор функий логгера :)
        {
            return function(message,meta)
            {
                //var d = Date.now(); // Будем потом записовать время вызова
                var mes = this.module + " -- ";
                mes += level + " -- ";
                mes += message; // прицепить сообщение
                if(meta) mes += "  " + util.inspect(meta) + " "; // Записать доп инфу (Object||Error)
                mes += '\n'; // Конец строки :)

                this.write(mes);
                // Записать во все потоки наше сообщение
            }
        };

        this.trace = stackTrace.get()[1]; // Получить стек вызова
        this.filename = this.trace.getFileName(); // Получить имя файла которое вызвало конструктор
        this.module = projectname + path.sep + path.relative('.',this.filename); // Записать име модуля
        this.streams = [process.stdout]; // Потоки в которые мы будем записовать логи
        // В дальнейшем здесь будет стрим к файлу
        this.log = generateLogFunction('Log'); // Лог поведения
        this.info = generateLogFunction('Info'); // Лог информативный
        this.error = generateLogFunction('Error'); // Лог ошибок
        this.warn = generateLogFunction('Warning'); // Лог предупреждений
    }
    write(d)
    {
        this.streams.forEach((stream)=>{
            stream.write(d);
        });
    }
}

А теперь про синтаксис использования.


var logger = new require('./logger')();
//...
logger.info('Hello, world');

Почему мы используем new? Для того что бы получить имя файла в котором был создан логер. Ибо запуск stack-trace каждый раз когда мы пишем в лог будет использовать много ресурсов. Заменим все console на logger. Оставлю все на волю вашего IDE :)


NOTE: В папке doc и node_modules есть файлы использующие console. Будьте осторожны!


Так-же заменим в файле worker.js console.error на throw. Вот так:


app.listen(3000,function(err){
    if(err) throw err;
    // Если есть ошибка сообщить об этом
    logger.log(`Running server at port 3000!`) 
    // Иначе сообщить что мы успешно соединились с мастером
    // И ждем сообщений от клиентов
});

Почему мы не используем winston и другие модули для работы с логами? Ответ прост: winston показывает маленькую производительность. И не только винстон. Тоже самое касается многих модулей. Как оказалось после некоторого тестирования наш самодельный модуль показывает 4-8 раза больше производительности чем многие другие модули :)


Время обработки запроса


Для того что бы видеть какие запросы пришли на сервер и сколько времени заняла ее обработка мы напишем свой middleware. В папке bin создадим файл rt.js


var Logger = require('../logger');
var logger = new Logger();

module.exports = function(req,res,next)
{
    // Засечь начало
    var beginTime = Date.now();
    // В конце ответа
    res.on('finish',()=>{
        var d =  Date.now();// получить дату в мс
        logger.log('Reponse time: ' + (d - beginTime),{
            url:req.url, // записать в лог куда пришел запрос (Включает urlencode string :)
            time:(d - beginTime) // сколько прошло времени
        });
    });
    // Передать действие другому обработчику
    next();
}

А в worker.js до каких либо обработчиков добавим:


// Время ответа
app.use(require('./rt'));

Здесь мы используем собственный модуль потому что все остальные модули НЕ умеют логировать только гарантированно отправленные (хотя бы до ОС) запросы.


Разница приложения от роутера


В контроллере мы видели express() для создания мини приложения и потом мы монтировали его в проект с помощью app.use() но express не рекомендуют так делать. Мы заменим express() на new express.Router() в файле контроллера:



// Пример:
var Router = require('express').Router;
var app = new Router();

// app.use(....)
// app.get(....)
// etc

Какие проблемы возникнут с express()? Самое важное. Мы не можем изменить настройки во всем приложении. К тому же не можем использовать app.locals. И еще по какой-то не понятной причине оно НЕ передает куки (Почему так?).


Конфигурация


Создадим папку config. В папке у нас будет файл index.js, где мы будем получать все настройки, складывать, парсить и даже грабить корованы вставлять при необходимости нужные поля, если они отсутствуют.


module.exports = require('./config');

А в файле config.json:


{
    "port":8080,
    "mongoUri":"mongodb://127.0.0.1/armleo-test"
}

NOTE: Если нет включенных сетей в windows то localhost не работает и надо использовать 127.0.0.1
В файле worker.js добавим в начале:


var config = require('../config');

А последние строки превращаются в зайца следующее:


// Запустим сервер на порту 3000 и сообщим об этом в консоли.
// Все Worker-ы  должны иметь один и тот же порт
app.listen(config.port,function(err){
    if(err) throw err;
    // Если есть ошибка сообщить об этом
    logger.log(`Running server at port ${config.port}!`);
    // Иначе сообщить что мы успешно соединились с мастером
    // И ждем сообщений от клиентов
});

Мы ведь помним, что нам еще нужно изменить строки в dbinit.js? Так сделаем это.


// 10 line bin/dbinit.js
// Подключимся к серверу MongoDB
var config = require('../config');
mongoose.connect(config.mongoUri,{
    server:{
        poolSize: 10
        // Поставим количество подключений в пуле
        // 10 рекомендуемое количество для моего проекта.
        // Вам возможно понадобится и то меньше...
    }
});

ES6


Мы заменим все нужные var на const и let. Маленькое изменение, но оно мне нравится!


Авторизация


Теперь к авторизации! Для авторизации будем использовать Passport.js. В нашем проекте ПОКА не нужна регистрация ибо пользователь один и будет добавлен нами в датабазу в ручную. Создадим контроллер auth.js. В контроллере нам нужны парсеры входных данных:


let app = new (require('express').Router)();

const models = require('./../models');

const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;

app.use(passport.initialize());
app.use(passport.session());

passport.use(new LocalStrategy(
    function(username, password, done) {
        models.User.findOne({ username: username }, function (err, user) {
        if (err) { return done(err); }
        if (!user) {
            return done(null, false, { message: 'Incorrect username.' });
        }
        if (user.password != password) {
            return done(null, false, { message: 'Incorrect password.' });
        }
        return done(null, user);
        });
    }
));

passport.serializeUser(function(user, done) {
    done(null, user._id);
});

passport.deserializeUser(function(id, done) {
    models.User.findById(id, function(err, user) {
        done(err, user);
    });
});

app.post('/login',
    passport.authenticate('local', {
        successRedirect: '/',
        failureRedirect: '/login'
    })
);
app.get('/login',function(req,res,next)
{
    if(req.user) return res.redirect('/');

    res.render('login',{
        user:req.user
    });
});

module.exports = app;

Для авторизации мы в views/login.html добавим форму.


<form action="/login" method="POST">
    <input name="username"/>
    <br/>
    <input name="password"/>
    <br/>
    <input type="submit"/>
</form>

Для пользователей придумаем модель.


Посты!


Установим модули:


npm i mongoose-url-slugs --save

Создадим модель постов в папке models файл post.js:


// Загрузим mongoose т.к. нам требуется несколько классов или типов для нашей модели
const mongoose = require('mongoose');
const URLSlugs = require('mongoose-url-slugs');
// Создаем новую схему!
let postSchema = new mongoose.Schema({
    title:{
        type:String, // тип: String
        required:[true,"titleRequired"],
        // Данное поле обязательно. Если его нет вывести ошибку с текстом titleRequired
        // Максимальная длинна 32 Юникод символа (Unicode symbol != byte)
        minlength:[6,"tooShort"],
        unique:true // Оно должно быть уникальным
    },
    text:{
        type:String, // тип String

        required:[true,"textRequired"]
        // Думаю здесь все тоже очевидно
    },
    // Здесь будут и другие поля, но сейчас еще рано их сюда ставить!
    // Например коментарии
    // Оценки
    // и тд

    // slug:String
});

// Теперь подключим плагины (внешние модули)

// Подключим генератор на основе названия
postSchema.plugin(URLSlugs('title'));

// Компилируем и Экспортируем модель
module.exports = mongoose.model('Post',postSchema);

А в файле models/index.js:


module.exports = {
    // Загрузить модель юзера (пользователя)
    // На *nix-ах все файлы чувствительны к регистру
    User:require('./user'),
    Post:require('./post')
};
// Не забудем точку с запятой!

Создадим контроллер для создания/редактирования постов! В файле controllers/index.js:


const Logger = require('../logger');
const logger = new Logger();

let app = new (require('express').Router)();

app.use(require('./auth'));
app.use(require('./home'));
app.use(require('./post'));

module.exports = app;

А в файле controllers/post.js:


let app = new (require('express').Router)();
const models = require("../models");

app.get('/post', function(req,res,next)
{
    if(!req.user) return res.redirect('/login');
    res.render('addpost',{
        user:req.user
    });
});

app.post('/post', function(req, res, next)
{
    if(!req.user) return res.redirect('/login');
    let post = new models.Post(req.body);
    post.save()
        .then(()=>{
            res.redirect('/post/' + post.slug);
        }).catch(next);
});

app.get('/post/:slug',(req, res, next)=>{
    models.Post.findOne({
        slug:req.params.slug
    }).exec().then((post)=>{
        if(!post) res.redirect('/#notfound');
        res.render('post',{
            user:req.user,
            post
        });
    }).catch(next);
});

module.exports = app;

И соответсвенно образы! Создания views/addpost.html


<form method="POST" action="/post">
    <input name="title"/>
    <br/>
    <input name="text"/>
    <br/>
    <input type="submit"/>
</form>

Отображения views/post.html


{{#post}}
    <h1>{{title}}</h1>
    <br/>
    {{text}}
{{/post}}

Немного доделаем образ views/index.html


{{#user}}
    Hello {{username}}
{{/user}}
{{^user}}
    Login <a href="/login">here!</a>
{{/user}}
{{#posts}}
<br/><a href="/post/{{slug}}">{{title}}</a>
{{/posts}}

Доделаем контроллер controllers/home.js:


let app = new (require('express').Router)();
const models = require('../models');

app.get('/',(req,res,next)=>{
    //Создадим новый handler который сидит по пути `/`
    models.Post.find({}).exec().then((posts)=>{

        res.render('index',{
            user:req.user,
            posts
        });
        // Отправим рендер образа под именем index
    }).catch(next);
});

module.exports = app;

В bin/worker.js добавим парсер urlencode:


app.use(bodyParser.urlencoded());

GitHub


Наш проект вы можете найти на гитхабе вот тут


Конец второй части!


На этом конец второй части. В след. частях мы выделим немного времени HTTPS, сервисам в Ubuntu, Будем Хешировать пароль, Каптче (recaptcha) и комментариям с некоторой статистикой и введем поддержку markdown для постов, Заменим MongoDB для сессий на Redis, Добавим кеширования.

Arman Avetisyan @Armleo
карма
13,0
рейтинг 0,0
Похожие публикации
Самое читаемое Разработка

Комментарии (8)

  • 0
    Спасибо за публикацию. Скажите пожалуйста, а почему часть 2.1? Это исправленный вариант второй части? Буквально вчера второй части еще не было :)
    • 0

      1 часть.
      2.1 часть
      2.2 часть.
      3 часть.
      Возможно будут другие части. (Например дополнения)

  • 0
    если уж проводили рефакторинг, то в роутах уберите проверку «if(!req.user) return res.redirect('/login');»
    и вынесите ее в отдельный middleware

    пример:

    const AuthCheck = require('path/to/authcheck.js');
    ........
    
    app.get('/post', AuthCheck, function(req,res,next)
    {
       .....
    });
    
    app.post('/post', AuthCheck, function(req, res, next)
    {
        .....
    });
    


    где файл authcheck.js с вашей проверкой…

    'use strict';
    
    module.exports = function(req, res, next)
    {
    	if(!req.user)
    	{
    		return res.redirect('/login');
    	}
    	next();
    };
    
    • 0
      или еще проще вызывать так в вашем файле post.js:
      app.use(AuthCheck);
      
      app.get('/post', function(req,res,next)
      {
         .....
      });
      
      app.post('/post', function(req, res, next)
      {
          .....
      });
      
      
      • 0

        Будет. Как видите статья разделена на две части. К счастью оно будет во второй части.

  • 0
    Какие проблемы возникнут с express()? Самое важное. Мы не можем изменить настройки во всем приложении.

    о каких настройках приложения вы говорите? не встречал проблем с этим
    К тому же не можем использовать app.locals.

    от чего же не можете?
    в доках все написано
    app.locals
    res.locals

    И еще по какой-то не понятной причине оно НЕ передает куки (Почему так?).

    может просто надо поставить cookieParser!?
    • 0

      Речь про setCookie. Чуть позже напишу про это подробнее.

      • 0
        для установки куки достаточно потом вызывать:

        res.cookie(name, value, options);
        

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.