- PVSM.RU - https://www.pvsm.ru -
Различной документации по Node.js его модулях огромное количество, всякого рода готовых решений тоже хватает, но начав писать сайт сталкиваешься с проблемой: «А с чего начать?». Хочу вам рассказать свой опыт изучения Node.js на практике. Задача стоит довольно простая и понятная — GPS Трекер с интернет сервисом, отображающим наши передатчики на карте, рисующим маршрут перемещения и т.д., на сколько разгуляется фантазия. Проект не коммерческий и пишется во благо человечества для себя.
Работать мне приходится и на работе и дома, работа ни как не связана с сайтостроительством и это ни как не должно мешать рабочему процессу. По этому, выбирая IDE, выбор пал на Cloud9 IDE [1]. Дома для удобства использовался WebStorm [2]. Все данные сайта нужно где-то хранить, после изучения теоретической части [3] было решено на практике познакомиться с этим видом СУБД. Что бы не привязываться к рабочему месту, в роли СУБД была выбрана MongoDB [4] и бесплатный для базы на 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] и задача стояла типичная, дать возможность пользователям регистрироваться, восстанавливать пароль, запоминать вход на сайте, осуществить контроль почты и само собой вход.
Идея: в углу экрана кнопка, нажимаем на нее, появляется модальное окно с возможностью регистрироваться/войти на сайт с частичной проверкой вводимых данных(в дальнейшем можно усложнить проверку).
@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;
}
<!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>
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 */
};
Дня начала создадим контроллер БД. Его задача — подключаться к базе, плюс еще некоторые часто используемые процедуры будут храниться там, /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
Нажмите здесь для печати.