...

вторник, 31 декабря 2013 г.

[Из песочницы] Базовое Node.JS приложение с использованием express

Всем привет.

Искал статью, как сделать базовое Node.JS приложение с использованием express, точнее какая базовая структура должна быть у проекта, но так ничего похожего для меня не нашел.

Потому решил написать собственную, дабы объяснить таким же как и я как это сделать и как это должно выглядеть.

Подробности под катом. Осторожно. Много текста и кода.



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


Задача была следующей: сделать базовое приложение, которое смогло бы обрабатывать запросы, и выводить правильные страницы, либо же правильные ответы на запросы.


Итак. Начнем, пожалуй, с используемых модулей внутри приложения:



express - базовый пакет, для создания http-сервера
mongoose - фреймверк, для удобной работы с MongoDB
mongodb - native-driver для работы с MongoDB напрямую
connect-mongo - нужно для работы express с session
node-uuid - для генерирования токенов для авторизации (в случае использования веб-сервисов)
async - для работы с цепочкой асинхронных вызовов, ака Promise
ejs-locals - движок рендеринга, который поддерживает наследование шаблонов
nconf - для удобной работы с настройками приложения (собственный config.json)
string - для более удобной работы со строками, также очистка строк от ненужных вещей, типа html тегов и тд
validator - валидация данных
winston - для продвинутого логирования ошибок


Каждый из модулей можно установив используя команду:

npm install <<module_name>> --save


--save нужен для сохранения модуля в dependency (package.json), для дальнейшего развертывания приложения на других машинах.


Структура приложения получается следующей:



/config
config.json
index.js
/middleware
checkAuth.js
errorHandler.js
index.js
/models
user.js
/public
/*JS, CSS, HTML static files*/
/routes
authentication.js
error.js
index.js
main.js
register.js
/utils
index.js
log.js
mongoose.js
validate.js
/views
index.ejs
manage.js
package.json
server.js


Сейчас, собственно говоря, объясню в чем соль каждой из директорий и ее скриптов.

Начнем с самого главного скрипта, инициирующего все наше приложение.


server.js



var express = require('express'),
middleware = require('./middleware'),
http = require('http'),
app = express(),
config = require('./config'),
log = require('./utils/log')(app, module);

middleware.registerMiddleware(app, express);

http.createServer(app).listen(config.get('port'), function(){
log.info('Express server listening on port ' + config.get('port'));
});


В server.js создаем приложение epxress app, подключаем модуль middleware, в методе registerMiddleware подключаются все нужные middleware приложения.

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


package.json



{
"name": "test_express_app",
"version": "0.0.1",
"private": true,
"scripts": {
"start": "node server.js"
},
"dependencies": {
"express": "~3.4.6",
"mongoose": "~3.8.1",
"node-uuid": "~1.4.1",
"nconf": "~0.6.9",
"winston": "~0.7.2",
"async": "~0.2.9",
"mongodb": "~1.3.22",
"ejs-locals": "~1.0.2",
"connect-mongo": "~0.4.0",
"validator": "~2.0.0",
"string": "~1.7.0"
}
}



Содержит в себе всю нужную информацию о проекте, а также все требуемые пакеты.


manage.js



var mongoose = require('./utils/mongoose'),
async = require('async'),
User = require('./models/user'),
log = require('./utils/log')(null, module),
config = require('./config');

function openConnection(cb) {
mongoose.connection.on('open', function () {
log.info('connected to database ' + config.get('db:name'));
cb();
});
}

function dropDatabase(cb) {
var db = mongoose.connection.db;
db.dropDatabase(function () {
log.info('dropped database ' + config.get('db:name'));
cb();
});
}

function createBaseUser(cb) {
var admin = new User({
username: 'admin',
password: config.get('project:admin:password'),
email: config.get('project:admin:email'),
role: 1
});
admin.save(function () {
log.info('created database ' + config.get('db:name'));
log.info('created base admin user');
cb();
});
}

function ensureIndexes(cb) {
async.each(Object.keys(mongoose.models), function (model, callback) {
mongoose.models[model].ensureIndexes(callback);
}, function () {
log.info('indexes ensured completely');
cb();
});
}

function closeConnection() {
mongoose.disconnect();
log.info('disconnected');
}

async.series(
[
openConnection,
dropDatabase,

createBaseUser,

ensureIndexes
],
closeConnection
);


Нужен для инициализации базы данных, заполнение default информацией, которой сервер будет оперировать.


config




config.json



{
"port": 3000,
"db": {
"connection": "mongodb://localhost",
"name": "db_name",
"options": {
"server": {
"socketOptions": {
"keepAlive": 1
}
}
}
},
"session": {
"secret": "secret_key",
"key": "cid",
"cookie": {
"path": "/",
"httpOnly": true,
"maxAge": null
}
}
}


index.js



var nconf = require('nconf');
var path = require('path');

nconf.argv()
.env()
.file({file: path.join(__dirname, 'config.json')});

module.exports = nconf;


В файле config.js содержится информация о настройках соединения с базой данных, а также настройки сессии.

Для работы с config используется пакет nconf, который позволяет через getter и setter манипулировать с объектом настроек. Также можно использовать вложенные объекты через символ ::



config.get('session:secret');
config.get('session:cookie:path');


middleware



exports.registerMiddleware = function (app, express) {
var ejs = require('ejs-locals'),
path = require('path'),
config = require('../config'),

mongoose = require('../utils/mongoose'),
MongoStore = require('connect-mongo')(express),

router = require('../routes'),
errorHandler = require('./errorHandler')(app, express),

checkAuth = require('./checkAuth');

/**
* Page Rendering
* */
app.engine('html', ejs);
app.engine('ejs', ejs);
app.set('views', path.join(__dirname, '../views'));
app.set('view engine', 'ejs');


/**
* Public directory
* */
app.use(express.static(path.join(__dirname, '../public')));
app.use("/public", express.static(path.join(__dirname, '../public')));


/**
* Favicon
* */
app.use(express.favicon('public/images/favicon.ico'));


/**
* Logger
* */
if (app.get('env') == 'development') {
app.use(express.logger('dev'));
}


/**
* Session
* */
app.use(express.bodyParser());
app.use(express.cookieParser());
app.use(express.session({
secret: config.get('session:secret'),
key: config.get('session:key'),
cookie: config.get('session:cookie'),
store: new MongoStore({mongoose_connection: mongoose.connection})
}));

/**
* Authorization Access
* */
app.use(checkAuth);


/**
* Routing
* */
app.use(app.router);
router(app);


/**
* Error handing
* */
app.use(errorHandler);
};


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


Хочу также отметить — errorHandler middleware предназначен для собственного handling ошибок сервера, и вывода страницы ошибки


errorHandler



var config = require('../config');

var sendHttpError = function (error, res) {
res.status(error.status);

if (res.req.headers['x-requested-width'] === 'XMLHttpRequest') {
res.json(error);
} else {
res.render('error', {
error: {
status: error.status,
message: error.message,
stack: config.get('debug') ? error.stack : ''
},
project: config.get('project')
});
}
};

module.exports = function (app, express) {
var log = require('../utils/log')(app, module),
HttpError = require('../error').HttpError;

return function(err, req, res, next) {
if (typeof err === 'number') {
err = new HttpError(err);
}
if (err instanceof HttpError) {
sendHttpError(err, res);
} else {
if (app.get('env') === 'development') {
express.errorHandler()(err, req, res, next);
} else {
log.error(err);
err = new HttpError(500);
sendHttpError(err, res);
}
}
};
};


Также хочется отметить middleware checkAuth



var HttpError = require('../error').HttpError;

module.exports = function (req, res, next) {
if (!req.session.user) {
return next(new HttpError(401, "You are not authorized!"));
}
next();
};




Который будет проверять запросы на наличие сессии и, в случае ее отсутствия, будет бросать ошибку. Его можно использовать как глобальный middleware или же указать конкретно метод, где он будет использоваться:

app.get('/user-info', checkAuth, function (req, res, next) {
//do your staff
});


models




C помощью Mongoose мы будем создавать собственные модели для работы с данными. Пример модели может выглядеть следующим образом:

var crypto = require('crypto'),
mongoose = require('../utils/mongoose'),
Schema = mongoose.Schema,
async = require('async');

var User = new Schema({
username: {
type: String,
unique: true,
required: true
},
hashedPassword: {
type: String,
required: true
},
salt: {
type: String,
required: true
}
});

User.methods.encryptPassword = function (password) {
return crypto.createHmac('sha1', this.salt).update(password).digest('hex');
};

User.virtual('password')
.set(function (password) {
this._plainPassword = password;
this.salt = Math.random() + '';
this.hashedPassword = this.encryptPassword(password);
})
.get(function () {
return this._plainPassword;
});

User.methods.checkPassword = function (password) {
return this.encryptPassword(password) === this.hashedPassword;
};

module.exports = mongoose.model('User', User);



public




В данной директории будут содержаться все скрипты и css файлы, доступные извне. Осуществляется данная опция с помощью следующей настройки:

/**
* Public directory
* */
app.use(express.static(path.join(__dirname, '../public')));
app.use("/public", express.static(path.join(__dirname, '../public')));


routes




Cамое, пожалуй, интересное. В данной директории, мы объявляем модуль, который будет отвечает за роутинг. файл index.js

var main = require('./main'),
register = require('./register'),
authentication = require('./authentication'),
error = require('./error');

module.exports = function (app) {
app.get('/', main.home);

app.post('/register', register.requestRegistration);

app.get('/users', authentication.users);
app.get('/users/:id', authentication.user);

app.get('*', error['404']);
};


Здесь мы просто объявляем наши роуты, и просто делегируем выполенение другим модулям. Например, route "/":



/**
* Method: GET
* URI: /
* */
exports.home = function(req, res, next) {
res.render('index');
};


Cобственно говоря и все. В данном случае, как база приложение будет работать. Для поддержки сессии включаем соответствующий middleware. Всю бизнес логику, связанную с пользователем, переносим в models/user.js, в частности валидацию и регистрацию, к примеру.


PS:

В написании данной статьи была использована информация из скринкастов И.Кантора. Ссылка на скринкаст.

Также использовалась информация из курсов по MongoDB


This entry passed through the Full-Text RSS service — if this is your content and you're reading it on someone else's site, please read the FAQ at fivefilters.org/content-only/faq.php#publishers.


Комментариев нет:

Отправить комментарий