Искал статью, как сделать базовое 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/
Добавить комментарий