Изучение Node.js от начала до конца на практике. Часть 1

Предыстория


Различной документации по Node.js его модулях огромное количество, всякого рода готовых решений тоже хватает, но начав писать сайт сталкиваешься с проблемой: «А с чего начать?». Хочу вам рассказать свой опыт изучения Node.js на практике. Задача стоит довольно простая и понятная — GPS Трекер с интернет сервисом, отображающим наши передатчики на карте, рисующим маршрут перемещения и т.д., на сколько разгуляется фантазия. Проект не коммерческий и пишется во благо человечества для себя.

Обустраиваем рабочее место


Работать мне приходится и на работе и дома, работа ни как не связана с сайтостроительством и это ни как не должно мешать рабочему процессу. По этому, выбирая IDE, выбор пал на Cloud9 IDE. Дома для удобства использовался WebStorm. Все данные сайта нужно где-то хранить, после изучения теоретической части было решено на практике познакомиться с этим видом СУБД. Что бы не привязываться к рабочему месту, в роли СУБД была выбрана MongoDB и бесплатный хостинг для базы на mongohq.com.
Итак, у нас есть пустой проект и пустая база. Можно приступать.
1. Web application framework, одним из самых распространенных является express.
2. Сайт было решено написать полностью на HTML5, по этому шаблонный движок был выбран EJS.
3. MongoDB driver, их существует целый ряд, но мой выбор остановился на mongodb.
4. Верификация вводимых пользователем данных, node-validator.

Структура сайта


BigBrother -\
      - controllers (все объекты для работы с БД)
      - public -\ (статически файлы (css, js, images))
            - css
            - js
            - images
      - routers (маршруты сайта)
      - views (шаблоны страниц)
      - config.js (настройки, например, подключения к БД)
      - server.js (сам сервер)

Первый шаг: Авторизация


Материалом для изучения служил топик и задача стояла типичная, дать возможность пользователям регистрироваться, восстанавливать пароль, запоминать вход на сайте, осуществить контроль почты и само собой вход.

1. Лицо сайта

Идея: в углу экрана кнопка, нажимаем на нее, появляется модальное окно с возможностью регистрироваться/войти на сайт с частичной проверкой вводимых данных(в дальнейшем можно усложнить проверку).
Таблица стилей style.css
@import url(http://fonts.googleapis.com/css?family=Tenor+Sans&subset=latin,cyrillic);

body{
    font-family: 'Tenor Sans', sans-serif;
}

#mainmap{
    width : 100%;
}

#topmenu{
    width: 100%;
    height: 80px;
    background-color: white;
}

#topmenu #user{
    background-color: rgb(228, 228, 228);
    cursor: pointer;
    position: absolute;
    top: 20px;
    right: 20px;
    vertical-align: middle;
    text-align: center;
    padding: 10px;
    border: 1px solid gray;
}

#topmenu #user:hover{
    background-color: rgb(188, 188, 188);    
    border: 1px solid gray;
}

.window{
    position: fixed;   
    top: 0px;
    left: 0px;
    width: 100%;
    height: 100%;
    background-color: rgba(40,40,40,0.5);
    z-index: 9999;
    color: rgb(80,80,80);
    display: none;
}

.window .back{
   position: absolute;   
   top: 0px;
   left: 0px;
   width: 100%;
   height: 100%; 
   z-index: 0;
}

.window .wrap{
    position: fixed;
    width: 500px;
    height: 400px;
    top: 50%;
    left: 50%;
    margin: -200px -250px;
    background-color: white;
    border: 1px solid silver;
    padding: 10px;
    z-index: 1;
}

.window .wrap .header{
    font-size: 25px;
    color: rgb(40,40,40);
    width: 100%;
    border-bottom: 1px solid gray;
    padding-bottom: 5px;
}

.window .wrap .header .active{    
    color: rgb(40,90,40);   
    background-color: rgb(220,200,200);
}

.window .wrap .header div{
    display: inline;
    cursor: pointer;
    background-color: rgb(230,240,240);
    padding: 2px;
}

.window .wrap .header div:hover{
   color: rgb(40,40,90);  
   background-color: rgb(230,230,230);
}

.window .wrap .msg{
    display: none;
    position: absolute;
    top: 41px;
    left: 10px;
    width: 480px;
    background-color: rgb(220,100,100);
    padding: 10px;
    color: black;
}

.window .wrap .line{    
    margin-top: 10px;
    margin-left: 50px;   
}

.window .wrap .line .label{   
    font-size: 20px; 
}

.window .wrap .line .edit input[type='text'],
.window .wrap .line .edit input[type='email'],
.window .wrap .line .edit input[type='password']{       
    width: 400px;
    height: 25px;
    margin-top: 5px;
    border: 1px solid silver;
    font-size: 20px;
}

.window .wrap .line .edit input[type='text'],
.window .wrap .line .edit input[type='email'],
.window .wrap .line .edit input[type='password']:focus{ 
    border: 1px solid gray;
}

.window .wrap .line .edit .error{ 
    border-color: red;   
}

.window .wrap .buttons{
    position: absolute;
    width: 100%;
    height: 40px;
    left: 0;
    bottom: 0;
    background-color: rgb(240,240,240); 
    color: rgb(40,40,40);
}

.window .wrap .buttons .button{    
    float: right;
    padding: 5px;
    border: 1px solid gray;
    margin: 5px;
    cursor: pointer;
}

.window .wrap .buttons .button:hover{
    background-color: rgb(188, 188, 188);    
    border: 1px solid gray;
}


Главная страница index.ejs
 <!DOCTYPE html>
<html>
<head>
 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" >
 <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1" >
 <!--                               CSS                 !-->
 <link rel="stylesheet" href="css/reset.css">
 <link rel="stylesheet" href="css/style.css">
 <!--                           Utils                   !-->
  <script src="http://yandex.st/jquery/1.8.2/jquery.min.js"></script>
  <script src="js/jquery.cookie.js"></script>
  
  <script src="js/core.js"></script>
 
<title>BigBrother - <%= title %></title>
</head>
<body>
    <div id="topmenu">
        <div id="user">
            Login
        </div>
    </div>
    <div class="window" id="login">
        <div class="wrap">
            <div class="header">
                <div id="pagelogin" class="active">Login</div> /
                <div id="pageregister" class="">Registration</div>
            </div>
            <div class="line" style="margin-top: 80px">
                <div class="label">Email:</div>
                <div class="edit"><input type="email" id="email"/></div>
            </div>
            <div class="line" style="margin-top: 10px">
                <div class="label">Password:</div>
                <div class="edit"><input type="password" id="password"/></div>
            </div>            
            <div class="line" style="margin-top: 10px" id="confirmationpassworddiv">
                <div class="label">Confirmation password:</div>
                <div class="edit"><input type="password" id="confirmationpassword"/></div>
            </div>
            <div class="line" style="margin-top: 10px">
                <div class="label">Stay online:</div>
                <div class="edit"><input type="checkbox" id="stayonline"/></div>
            </div>
            <div class="buttons">                
                <div class="button" id="logincancel">cancel</div>
                <div class="button" id="loginsbmt">login</div>
            </div>
            <div class="msg">
            
            </div>
        </div>
        <div class="back"></div>
    </div>  
    <script type="text/javascript">
        jQuery(window).load(function(){
              Init();
        });
    </script>
</body>
</html>


События и обработчики core.js
function Init(){
  /*                            LOGIN WINDOW                    */
  //Init vars
  var user = jQuery('#user');
  var loginwindow = jQuery('#login');
  var loginemail = jQuery('#email');
  var loginpassword = jQuery('#password');
  var confirmationpassword = jQuery('#confirmationpassword');
  var pagelogin = jQuery('#pagelogin');
  var pageregister = jQuery('#pageregister');
  var confirmationpassworddiv = jQuery('#confirmationpassworddiv');
  var loginmsg = jQuery(jQuery('.msg',loginwindow)[0]);
  var stayonline = jQuery('#stayonline');
  confirmationpassworddiv.hide();
  loginmsg.hide();
  //Set events
  confirmationpassword.keypress(inputkeypress);
  loginpassword.keypress(inputkeypress);
  loginemail.keypress(inputkeypress);
  user.click(function(){    
    loginwindow.fadeIn('fast');
  });
  jQuery('#logincancel').click(function(){
    hidewindow();  
  });
  jQuery('#loginsbmt').click(function(){      
      var isLogin = pagelogin.hasClass('active');
      var error = false;
      var errormsg = '';
      if (loginemail.val()===''){
          error = true;
          loginemail.addClass('error');
          errormsg += 'Type your email';
      }else loginemail.removeClass('error'); 
      if (!isLogin){
        if (loginpassword.val()===''){
          error = true;
          loginpassword.addClass('error');
          errormsg += '<br/>Type your password';
        }else loginpassword.removeClass('error');  
        if (confirmationpassword.val()===''){
          error = true;
          confirmationpassword.addClass('error');
          errormsg += '<br/>Type your confirmation password';
        }else confirmationpassword.removeClass('error');  
        if (confirmationpassword.val()!=loginpassword.val()){
            error = true;    
            confirmationpassword.addClass('error');
            errormsg += '<br/>Password not same';
        }
      }
      if (!error) {        
        if (!isLogin)
            registeruser();    
        else
            loginuser();
      }else{
        loginmsg.html(errormsg);
        loginmsg.show();
      }
  });
  jQuery('.back',loginwindow).click(function(){
    hidewindow();  
  });
  pagelogin.click(function(){
    pagelogin.addClass('active');
    pageregister.removeClass('active');
    confirmationpassworddiv.hide();  
    loginmsg.hide();
    jQuery('#loginsbmt').html('login');
  });
  pageregister.click(function(){
    pagelogin.removeClass('active');
    pageregister.addClass('active');
    confirmationpassworddiv.show();  
    loginmsg.hide();
    jQuery('#loginsbmt').html('register');
  });
  //Check login state
  checklogin();
  //Other function
  function inputkeypress(){
    if (jQuery(this).val()!=='')
        jQuery(this).removeClass('error');
  }
  //Hide login window and clear state
  function hidewindow(){    
    loginwindow.fadeOut('fast',function(){
        loginmsg.hide();
        confirmationpassword.removeClass('error');
        loginpassword.removeClass('error'); 
        loginemail.removeClass('error'); 
        pagelogin.click();
    });  
  }
  //Do login
  function loginuser(){    
    jQuery.ajax({
      type: "POST",
      url: "/auth",
      data: { email: loginemail.val(), password: loginpassword.val(), stayonline: stayonline.val()==='1'}
    }).done(function( msg ) {
      loginpassword.val('');
      if (msg.error){
        loginmsg.html(msg.msg);
        loginmsg.show();  
      }else{          
        jQuery.cookie('sessionid',msg.sessionid);
        loginmsg.html(msg.msg);
        loginmsg.show();  
        setTimeout(function() {
            hidewindow();
            checklogin();
        }, 1000);
      }
    });  
  }
  //Check login state
  function checklogin(){
    user.html('loading...');
    jQuery.ajax({
      type: "GET",
      url: "/auth"
    }).done(function( msg ) {
        if (msg.error) {
            user.html('login');
            user.unbind('click');
            user.click(function(){loginwindow.fadeIn('fast');});
        } else {
            user.html(msg.displayname);
            user.unbind('click');
            user.click(logout);
        }
            
    });   
  }
  //Do log out
  function logout(){
    jQuery.ajax({
      type: "DELETE",
      url: "/auth"
    }).done(function(msg){
        if (msg.error){
            alert(msg.msg);
            return;
        }
        user.html('login');
        user.unbind('click');
        user.click(function(){    
            loginwindow.fadeIn('fast');
        });
    });  
  }
  //Register new user
  function registeruser(){
   
    jQuery.ajax({
      type: "POST",
      url: "/auth/register",
      data: { email: loginemail.val(), password: loginpassword.val(), confirmationpassword:  confirmationpassword.val(), stayonline: stayonline.val()==='1'}
    }).done(function( msg ) {
      loginpassword.val('');
      confirmationpassword.val('');        
      if (msg.error){
        loginmsg.html(msg.msg);
        loginmsg.show();  
      }else{
        loginmsg.html(msg.msg);
        loginmsg.show();  
        setTimeout(function() {
            hidewindow();
            checklogin();
        }, 1000);
      }
    });
  }
  /*                            LOGIN WINDOW                    */
};



2. Серверная часть

Дня начала создадим контроллер БД. Его задача — подключаться к базе, плюс еще некоторые часто используемые процедуры будут храниться там, /controllers/db.js:
exports.opendb = function(settings, callback){    
    var mongo = require('mongodb'),
      Server = mongo.Server,
      Db = mongo.Db;
    
    var server = new Server(settings.host, settings.port, {auto_reconnect: settings.auto_reconnect});
    var db = new Db(settings.db, server);
    
    db.open(function(err, db) {
      if(!err) {
        db.authenticate(settings.username, settings.password, function(){callback(false, db);});
      } else callback(true, db);
    });    
};

exports.criptpassword = function(string){
    var crypto = require('crypto');
    return crypto.createHash('md5').update(string+global.saldo).digest("hex");
};

Что бы не плодить огромную кучу переменных при работе создадим в папке controllers файл index.js. В этом случае если мы пишем
global.controllers = require('./controllers');

в переменной будет подключен именно наш index.js:
exports.db = require('./db');
exports.users = require('./users');
exports.stayonlinesessions = require('./stayonlinesessions');

Следующим этапом создадим роутер для стартовой страницы /routers/index.js
exports.index = function(req, res){
    res.render('index',{title: 'Home'});   
};
exports.auth = require('./auth');

Сессии пользователей мы будем хранить в нашей базе, используем для этого пакет connect-mongo.
И собственно наш server.js.
Инициализируем глобальные модули и переменные
//Modules
var express = require("express");
var app = express();
var MongoStore = require('connect-mongo')(express);
var dbsettings = require('./config').settings; //Server configuration
global.saldo = 'fewfwef352tFRWEQF';
global.controllers = require('./controllers'); //Controllers
var routers = require('./routers'); //Routers
var viewEngine = 'ejs'; 

Настраиваем сервер express
// Configuration
app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', viewEngine);
  app.use(express.cookieParser());
  app.use(express.session({ 
      secret: 'fegwegwe',
      store: new MongoStore(dbsettings),
      cookie: { path: '/', httpOnly: true, maxAge: 1000*60*60*24 }
    }));
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
  
});

Тут есть маленькое отступление и сложность с которой я столкнулся. Если инициализацию сессий указывать после определения маршрутов, то сессии по какой-то причине на работают, по этому сессии инициализируем раньше.
Правильно

// Configuration
app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', viewEngine);
  /*---------------------------------------------------*/
  app.use(express.cookieParser());
  app.use(express.session({ 
      secret: 'fegwegwe',
      store: new MongoStore(dbsettings),
      cookie: { path: '/', httpOnly: true, maxAge: 1000*60*60*24 }
    }));
  /*---------------------------------------------------*/
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
});


Неправильно

// Configuration
app.configure(function(){
  app.set('views', __dirname + '/views');
  app.set('view engine', viewEngine);  
  app.use(express.bodyParser());
  app.use(express.methodOverride());
  app.use(app.router);
  app.use(express.static(__dirname + '/public'));
  /*---------------------------------------------------*/
  app.use(express.cookieParser());
  app.use(express.session({ 
      secret: 'fegwegwe',
      store: new MongoStore(dbsettings),
      cookie: { path: '/', httpOnly: true, maxAge: 1000*60*60*24 }
    }));
  /*---------------------------------------------------*/
});

Настраиваем маршруты
app.get('/',routers.index); //Стартовая страница
app.post('/auth/register',routers.auth.register); //Регистрация пользователей
app.post('/auth',routers.auth.login); //Вход на сайт
app.get('/auth',routers.auth.getlogin); //Проверить состояния входа: залогинился или нет
app.del('/auth',routers.auth.logout); //Выход с сайта

Подключаемся к базе, в случае успеха запускаем сервер
//Connect to db and start
global.controllers.db.opendb(dbsettings, function(error,db){
    if (!error){
        global.db = db;
        app.listen(process.env.PORT);
    } else console.log('Error connect to db');
});

Пробуем запустить, смотрим на главную страницу, оно работает.
Теперь опишем контроллер отвечающий за пользователей сайта: /controllers/users.js
Регистрация пользователя:
exports.register = function(email, password, callback){
    global.db.collection('users', function(err, collection) {
        if (err){
            callback(true);
            return;
        }
        collection.findOne({email: email.toLowerCase()}, function(eror, item){
            if (item === null){
                collection.insert({email: email.toLowerCase(), password: global.controllers.db.criptpassword(password), emailchack: true, roles: []},{safe:true},function(error, result){
                    if (err){
                        callback(true);
                        return;
                    }   
                    callback(false, result[0]);
                });
            }else  callback(true, null, 'User already exists');    
            
        });
    });     
};

Получаем пользователя по ID
exports.getuser = function(id, callback){
    global.db.collection('users', function(err, collection) {
        if (err){
            callback(true);
            return;
        }
        var ObjectID = require('mongodb').ObjectID;
        if (typeof id === 'string') id = new ObjectID(id);
        collection.findOne({_id: id}, function(eror, item){
            if (err){
                callback(true);
                return;
            }    
            callback(false, item);
        });
    });
};

Проверяем возможно ли войти пользователю
exports.checkuser = function(email, password, callback){

    global.db.collection('users', function(err, collection) {
        if (err){
            callback(true);
            return;
        }
        collection.findOne({email: email.toLowerCase(), password: global.controllers.db.criptpassword(password)}, function(error,item){
            if (item === null) item = {};
            callback(error, item.emailchack, item);    
        });
    });    
};

Если пользователь ставит галочку «запомнить меня» мы будем добавлять в куки хешированный идентификатор пользователя и хранить все будем в контроллере документов «stayonlinesessions». Напишем контроллер отвечающий за сессии /controllers/stayonlinesessions.js
 exports.savesession = function(user_id, callback){

    global.db.collection('stayonlinesessions', function(err, collection) {
        if (err){
            callback(true);
            return;
        }
        var hash = global.controllers.db.criptpassword(user_id.toString());
        collection.insert({user_id: user_id, hash: hash, createdate: new Date()},{safe:true},function(error, result){
            if (err){
                callback(true);
                return;
            }   
            callback(false, hash);
        });
    });    
};

exports.getsession = function(hash, callback){
    global.db.collection('stayonlinesessions', function(err, collection) {
        if (err){
            callback(true);
            return;
        }
        collection.findOne({hash: hash}, function(error,item){
            if (err || item === null){
                callback(true);
                return;
            }   
            global.controllers.users.getuser(item.user_id, function(error, user){
                if (err || user === null){
                    callback(true);
                    return;
                }
                callback(false, user);
            });
        });        
    });   
};

exports.delsession = function(hash, callback){
    global.db.collection('stayonlinesessions', function(err, collection) {
        if (err){
            callback(true);
            return;
        }        
        collection.remove({hash: hash}, {safe: true}, function(err,removed){
            if (err || !removed){
                callback(true);
                return;
            }   
            callback(false);
        });        
    }); 
};

Итак, у нас есть контроллеры которые отвечают за регистрацию и сессии, теперь нужно реализовать маршруты по авторизации. За эти функции у нас будет отвечать /routers/auth.js.
Регистрация пользователей
exports.register = function(req, res){
    
    var email = req.body.email;
    var password = req.body.password;
    var confirmationpassword = req.body.confirmationpassword;
    var stayonline = req.body.stayonline;
    //Проверяем введенные данные
    var check = require('validator').check;
    if (!check(email).len(6, 64).isEmail() ||
        !check(password).notNull() ||
        !check(password).equals(confirmationpassword)){
        res.send({
            error: true,
            msg: 'Check your'
        });
        return;
    }
   //Регистрируем нового пользователя
    global.controllers.users.register(email, password, function(error, user, msg){
        if (error){
            if (typeof msg == 'undefined' || msg===null ) msg = 'Register error'; 
            res.send({
                error: true,
                msg: msg
            });    
            return;
        }
        //Сразу авторизовываемся
        req.session.authorized = true;
        req.session.user_id = user._id;
        req.session.username = user.email;  
        //Если стоит галочка "Запомнить меня" то записываем сессию и передаем ее номер
        if (stayonline){
            global.controllers.stayonlinesessions.savesession(user._id, function(error, hash){
                res.send({
                    error: false,
                    msg: 'Success register email: '+email,
                    sessionid: hash
                });    
            });
        }else{
            res.send({
                error: false,
                msg: 'Success register email: '+email
            });   
        }
    });   
};

Вход на сайт
exports.login = function(req, res){
    var email = req.body.email;
    var password = req.body.password;  
    var stayonline = req.body.stayonline;
    global.controllers.users.checkuser(email, password, function(error, canlogin, user){
        if (error || !canlogin){
            res.send({
                error: true,
                msg: 'Check your email or password'
            });
            return;    
        }
        req.session.authorized = true;
        req.session.user_id = user._id;
        req.session.username = user.email;        
        if (stayonline){
            global.controllers.stayonlinesessions.savesession(user._id, function(error, hash){
                res.send({
                    error: false,
                    msg: 'Success login email: '+user.email,
                    sessionid: hash
                });    
            });
        } else {
            
            res.send({
                error: false,
                msg: 'Success login email: '+user.email
            });
        }
    });
};

Выход с сайта
exports.logout = function(req, res){
    if (!req.session.authorized){
        res.send({
            error: true,
            msg: 'You are not loggined'
        });
        return;      
    }
    req.session.authorized = false;
    delete req.session.username;
    delete req.session.user_id;
    //Если номер сессии указан в куках, то удаляем его
    if (typeof req.cookies.sessionid !== 'undefined' && req.cookies.sessionid !== ''){
        global.controllers.stayonlinesessions.delsession(req.cookies.sessionid, function(error){
            if (error){
                console.log('Session was not deleted');    
                return;
            }            
        });   
    }
    res.send({
        error: false
    });
};

Получение текущего состояния авторизации
exports.getlogin = function(req, res){
    if (!req.session.authorized){
        //Если пользователь не авторизирован, то проверяем нет ли данных о сохраненных сессиях
        global.controllers.stayonlinesessions.getsession(req.cookies.sessionid, function(error, user){
            if (error){
                res.send({
                    error: true,
                    msg: 'You are not loggined'
                });
                return;
            }
            req.session.authorized = true;
            req.session.user_id = user._id;
            req.session.username = user.email;
            res.send({
                error: false,
                displayname: req.session.username
            });
        });            
    }else{
        res.send({
            error: false,
            displayname: req.session.username
        });
    }
};

Несколько моментов о куках. Если при инициализации модуля сессий мы не указываем время хранения, то они не хранятся в браузере вовсе. Это лечится одной простой настройкой
app.use(express.session({ 
      secret: 'fegwegwe',
      store: new MongoStore(dbsettings),
      cookie: { path: '/', httpOnly: true, maxAge: 1000*60*60*24 }  //Где maxAge - время хранения в миллисекундах
    }));

На стороне клиента работа с куками довольно проста. Я использовал плагин jquery.cookie.js
jQuery.cookie('sessionid',msg.sessionid);

В итоге мы получили авторизацию, регистрацию, функцию «запомнить меня», далее предстоит отправка письма с подтверждением email-адреса, восстановление пароля, но это в следующий раз.
Share post

Comments 33

    +2
    Тут есть маленькое отступление и сложность с которой я столкнулся. Если инициализацию сессий указывать после определения маршрутов, то сессии по какой-то причине на работают, по этому сессии инициализируем раньше.

    Это не по какой то причине, а потому что express выстраивает цепочку вызовов из функций function(req, res, next) {… }. Отсюда и получается, что запрос попадает в ваш обработчик раньше чем в обработчик сессий (до него он даже не доходит).

    И еще:
    1) Чего подключение к базе делает в контроллерах?
    2) использование глобал, путь на темную сторону.
      –5
      1. Я придерживаюсь своего правила, что все что относится к базе должно быть выделено в отдельный модуль (класс/библиотеку), это лично мое правило, я никому его не навязываю и не вижу ничего критичного в выделении таких функций.
      2. Если есть набор механизмов используемых во всех модулях, почему бы и нет.
      Если такой подход не верный, расскажите как делать более правильно, это будет являться очень полезным знанием.
        +2
        1. Да, аргумент про выделение в отдельный модуль/класс/библиотеку верный, но контроллеры то причем?
          –1
          Вы правы, правы и те кто осудил само название модулей и подход. Спасибо за критику, она натолкнула на идею реализации MVC в более менее полном объеме и на основе классов
      –2
      Почему решили использовать именно Node и MongoDB?
        +1
        Они оба любят json :-).
        +5
        Не нашел ссылку на github ;(
          0
          Вот скажите мне, только честно — от чего вы, юзая ООП-заточеный язык, не используете объекты?

          Объекты — это хорошо.
          Объекты придают функции контекст.
          Объекты придают тестированию смысл.
          Объекты наше все :)

          PS. И вааще, CoffeeScript, jade и stylus придадут вам сил и бодрости, растраченные на расставление скобок и точек с запятыми :)
            0
            К слову, jade — один из самых медленных шаблонизаторов. Для обеспечения доступа к локальным переменным Jade использует конструкцию with, которая дико тормозит выполнение. В jade можно отключить использование with, передав в дополнительных параметрах self: true, насколько я помню. Это ускоряет его работу, но он всё равно остаётся одним из самых медленных. При этом, в таком режиме перед каждой переменной шаблона нужно будет писать префикс self., что сделает шаблоны уже не такими лаконичными.
              +1
              Не такой уж и медленный jade без with — тык.
                +2
                По моим подсчётам он остаёт от всех остальных. Я тестирую не в браузере, а в node.js.

                Вот код бенчмарка: https://github.com/baryshev/template-benchmark.

                А вот результаты на моей виртуалке с Ubuntu 12.04 и Node.js 0.8.9:

                Rendering 100000 templates:
                
                ECT
                  Escaped   : 559ms
                  Unescaped : 134ms
                  Total     : 693ms
                
                Eco
                  Escaped   : 2625ms
                  Unescaped : 491ms
                  Total     : 3116ms
                
                EJS
                  Escaped   : 4621ms
                  Unescaped : 2786ms
                  Total     : 7407ms
                
                Handlebars.js
                  Escaped   : 521ms
                  Unescaped : 275ms
                  Total     : 796ms
                
                Hogan.js
                  Escaped   : 889ms
                  Unescaped : 720ms
                  Total     : 1609ms
                
                Swig
                  Escaped   : 2052ms
                  Unescaped : 369ms
                  Total     : 2421ms
                
                Dust
                  Escaped   : 553ms
                  Unescaped : 363ms
                  Total     : 916ms
                
                doT
                  Escaped   : 1699ms
                  Unescaped : 100ms
                  Total     : 1799ms
                
                Fest
                  Escaped   : 418ms
                  Unescaped : 202ms
                  Total     : 620ms
                
                Jade without `with`
                  Escaped   : 4134ms
                  Unescaped : 3255ms
                  Total     : 7389ms
                
                Jade
                  Escaped   : 13495ms
                  Unescaped : 12210ms
                  Total     : 25705ms
                


                Такие вещи предпочитаю перепроверять. Кстати, шаблон doT по Вашей ссылке вообще не валиден. Это можно проверить, вставив его и данные в демо-редактор на сайте разработчика. Даже если бы он был валиден, такое сравнение не приемлемо, т.к. в doT по умолчанию отключено экранирование данных, а во всех остальных оно по умолчанию включено. Отсюда его «хвалёная» скорость. Экранирование — самая тяжёлая операция для js-шаблонизаторов и использовать её стоит с умом. Не стоит экранировать все данные подряд.
                В своём тесте я сравниваю для всех шаблонизаторов скорость с экранированием и без неё.
                  –1
                  Ссылка на тесты отсюда, так что мопед не мой. doT без экранирования, как понимаю, приводился для примера, а не что бы погоняться. Да и даже у вас jade без with вполне на уровне используемого в статье ejs.
                –2
                О, не знал.
                С другой стороны в моем случае оно пофик — jade используется как сырец для парсинга в html на сервере, а на клиенте шаблоны работают eco (ну, точнее сделанные из eco шаблонов stitch-ем функции, вроде там все прилично внутрях)

                PS. А вроде бы with предан анафиме и за его использование на кострах сжигают, не?
                  –2
                  Да там за многое руки отрубать нужно, но от этого jade менее удобным шаблонизатором не становится.
                    –1
                    Да, многое в нем приятно.
                    Особенно доставляет :coffeescript — маркер :)
              +1
              — controllers (все объекты для работы с БД)

              Эта фраза, мне кажется, требует от автора разворачивания понятия «контроллер» и вообще описания архитектурной схемы. Если примерять на MVC то, что я увидел то:
              контроллеры по вашей схеме – это модели по MVC,
              маршруты по вашей схеме – это контроллеры по MVC,
              '/auth/register' – это маршруты по MVC, а то место, где они у вас собраны (под заголовком «настраиваем маршруты») – диспетчер;
              Вроде бы так.
                0
                Тут есть маленькое отступление и сложность с которой я столкнулся. Если инициализацию сессий указывать после определения маршрутов, то сессии по какой-то причине на работают, по этому сессии инициализируем раньше.

                Само собой, при поступлении запроса express прогоняет request и response по цепочке обработчиков, если обработчик, ответственный за сессию, стоит после роутера — маршруты получат эти объекты без подцепленной сессии.
                  +5
                  После такого названия ожидал чего-то вроде если не дневника изучения node от нуля (или от уровня «я знаю, что такое apache») до «я понял, что на node писать лучше, чем на php/python/...». Открыл, а тут… :)

                  Знаете, могу сравнить разве со статьей под заголовокм «Изучая езду на автомобиле от начала до конца (часть 1)» и содержимым «Для начала обучения я прикупил гоночный болид, настроил его следующим образом, и собрался на гонки (о гонках в части 2)». Суровое у Вас такое «начало» получилось :)
                    –1
                    Я подходил к написанию с какими-то базовыми знаниями по сайтостроительству и node.js, увы удовлетворяющих мои потребности туториалов не нашел и решил, что мой пройденный путь будет кому-то интересен и полезен
                      0
                      Так изложение пути классное, просто оно хотя бы «часть 3», но не 1 и не 2.

                      Может, напишете приквел? Уверен, многие были бы только рады, потому что этот пост написал по делу — наверняка и остальное у Вас получится изложить толково.

                      В любом случае спасибо за статью!
                    +1
                    Попробуйте использовать библиотеку для flow-control, чтобы не плодить лапшевидный код. Например, async.
                      0
                      Или node-nextflow для CS
                        –1
                        спасибо, не знал, буду изучать
                      0
                      Расскажите, пожалуйста, как прикрутить свою 404 ошибку, чтобы она была действительно 404 (возвращала этот код) и не убивала статику. Когда делал сайтик на nodejs, пришлось статику вынести в /static и через nginx прописать для нее отдельный location, а в express выставить последним маршрутом общий /*. Работало, но проблема с кодом ответа так и не решилась.
                      Как сделать это по уму?
                        0
                        Добавляем обработчик в конец цепочки express, т.е. в.т.ч. после статики. До него дело дойдет только если остальные не возвратят ответ. res.send принимает в качестве параметра и код ответа. res.status, res.statusCode
                          –1
                          Я делал так:
                          В самом конце файла routes.js (у меня все маршруты в одном файле) вставляю примерно такой код:

                          app.use(errorNotFound);
                          
                          function errorNotFound (req, res) {
                              var html = utils.acceptHtml(req); 
                          // ф-ия acceptHtml смотрит заголовок запроса и -
                          // если true, то генерим ответ в виде html, иначе в json
                          
                              if (html) {
                                  res.writeHead(404);
                                  res.end('error 404\n' + 'url ' + req.url + ' (' + req.method +') was not found on the server.');
                          // res.end по желанию можно заменить например на res.render('404', { params: params })
                              }
                              else {
                                  res.json({ error: const.ERR_ROUTES_404 });
                              }
                          }
                          
                        • UFO just landed and posted this here
                            –1
                            это одна сторона монеты, еще есть мамы с детьми за которыми нужно приглядывать. Если и кого есть желание, можно объединить усилия и сделать что-то полезное и бесплатное
                            • UFO just landed and posted this here
                                –1
                                Предлагаю всех заинтересовавшихся собраться в одном месте и все обсудить, пишите в лс
                            –1
                            По поводу оформления модулей: структура, которую я привожу ниже, кажется мне более изящной:
                            someunit.js
                            var someunit = function () {
                                "use strict";
                                                                        // private переменные и методы
                            // ..
                                    
                                return {
                                                                        // public переменные и методы
                            // ..
                                
                            }();
                            
                            module.exports = someunit;  
                            
                              –1
                              И ещё. Думаю, будет нелишним собрать в одном месте всякие полезные плюшки, типа такой:

                                  var mem,
                                      oldmem;
                              
                                  setInterval(function () {                               
                              // выводим кол-во занимаемой памяти только если оно изменилось с последнего замера
                                      mem = process.memoryUsage().rss;
                                      if (mem != oldmem) {
                                          console.log('Memory usage, MB: ' + (mem / (1024 * 1024)).toFixed(3));
                                      }
                                      oldmem = mem;
                                  }, const.MEM_OUTPUT_INTERVAL);
                              


                              Так, тестируя проект, можно интерактивно наблюдать за кол-вом потребляемой памяти. Впрочем, сейчас есть куча способов следить за этим с помощью стороннего софта, но вышеприведённый код я написал ещё давно, хех.
                                0
                                exports.getlogin = function(req, res){
                                   // login
                                   return;
                                }
                                

                                у вас все функции созданы с 2 параметрами и везде обязательно понатыканы return; которые на самом деле не нужны будут если придерживаться рекомендациям разработки в express (connect), а именно использования параметров с возвратом ошибок и единой обработкой их, тогда ваши функции станут выглядеть вот так:
                                exports.getlogin = function(req, res, next) {
                                   // logic
                                   if(err) next(new Error('My custom message error'));
                                }
                                

                                и по ссылке посмотрите примеры обработчиков, который добавите в конец после всех роутов, тогда они будут перехватываться в случаи ошибки и будет единый обработчик, который можно стилизировать под json или html или и то и то прописав логику. А если пойти дальше, то можно унаследовать свои функции от Error и сделать разновидности ошибок, я использую например так:
                                // Error handling
                                app.use(function(err, req, res, next){
                                    console.log(err);
                                    if (err instanceof NotFound) {
                                        res.render('errors/404', {
                                            title : 'Not Found',
                                            layout: 'login/layout',
                                            status: 404
                                        });
                                    } else if (err instanceof AccessDenied) {
                                        res.render('errors/403', {
                                            title : 'Not Found',
                                            layout: 'login/layout',
                                            status: 403
                                        });
                                    } else if (err instanceof Unavailable) {
                                        res.render('errors/503', {
                                            title : 'Not Found',
                                            layout: 'login/layout',
                                            status: 503
                                        });
                                    } else if (err instanceof Offline) {
                                        res.render('errors/offline', {
                                            title : 'Offline',
                                            layout: 'login/layout'
                                        });
                                    } else {
                                        res.render('errors/500', {
                                            title : 'The Server Encountered an Error',
                                            layout: 'login/layout',
                                            error: err,
                                            status: 500
                                        });
                                    }
                                });
                                
                                function AccessDenied(msg) {
                                    this.name = 'AccessDenied';
                                    Error.call(this, msg);
                                    Error.captureStackTrace(this, arguments.callee);
                                }
                                function NotFound(msg) {
                                    this.name = 'NotFound';
                                    Error.call(this, msg);
                                    Error.captureStackTrace(this, arguments.callee);
                                }
                                function Unavailable(msg) {
                                    this.name = 'Unavailable';
                                    Error.call(this, msg);
                                    Error.captureStackTrace(this, arguments.callee);
                                }
                                function Offline(msg) {
                                    this.name = 'Offline';
                                    Error.call(this, msg);
                                    Error.captureStackTrace(this, arguments.callee);
                                }
                                

                                Only users with full accounts can post comments. Log in, please.