Всем привет.
Искал статью, как сделать базовое 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
Автор: SSolonko