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

в 17:04, , рубрики: express, mongodb, node.js, метки: , ,

Предыстория

Различной документации по 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(https://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-адреса, восстановление пароля, но это в следующий раз.

Автор: a696385

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js