Базовое Node.JS приложение с использованием express

в 12:26, , рубрики: express, javascript, mongodb, node.js, метки: , , ,

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

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js