Базовое 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

ссылка на оригинал статьи http://habrahabr.ru/post/207930/

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *