- PVSM.RU - https://www.pvsm.ru -

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

Предыстория

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

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

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

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

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

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

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

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 [12].
И собственно наш 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 [13]

jQuery.cookie('sessionid',msg.sessionid);

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

Автор: a696385


Сайт-источник PVSM.RU: https://www.pvsm.ru

Путь до страницы источника: https://www.pvsm.ru/node-js/17215

Ссылки в тексте:

[1] Cloud9 IDE: https://c9.io/

[2] WebStorm: http://www.jetbrains.com/webstorm

[3] теоретической части: http://habrahabr.ru/post/152477/

[4] MongoDB: http://www.mongodb.org/

[5] хостинг: https://www.reg.ru/?rlink=reflink-717

[6] mongohq.com: http://mongohq.com

[7] express: http://expressjs.com/

[8] EJS: http://embeddedjs.com/

[9] mongodb: https://github.com/christkv/node-mongodb-native

[10] node-validator: https://github.com/chriso/node-validator

[11] топик: http://habrahabr.ru/post/145970/

[12] connect-mongo: http://github.com/kcbanner/connect-mongo

[13] jquery.cookie.js : https://github.com/carhartl/jquery-cookie