Делаем проект на 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, Добавим кеширования.

    • +11
    • 13k
    • 8
    Поделиться публикацией
    Похожие публикации
    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама
    Комментарии 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);
              

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