Pull to refresh

Тонкости nodejs. Часть I: пресловутый app.js

Reading time 5 min
Views 42K
Я работаю с node.js более трех лет и за это время успел хорошо познакомиться с платформой, ее сильными и слабыми сторонами. За это время платформа сильно изменилась, как, собственно, и сам javascript. Идея использовать одну среду и на сервере и на клиенте пришлась многим по душе. Еще бы! Это удобно и просто! Но, к сожалению, на практике все оказалось не так радужно, вместе с плюсами платформа впитала в себя и минусы используемого языка, а разный подход к реализации практически свел на нет плюсы от использования единой среды. Так все попытки реализовать серверный js до ноды не взлетели, взять тот же Rhino. И, скорее всего, node ждала та же участь, если бы не легендарный V8, неблокирующий код и потрясающая производительность. Именно за это его так любят разработчики. В этой серии статей, я постараюсь рассказать о неочевидных на первый взгляд проблемах и тонкостях работы, с которыми вы столкнетесь в разработке на nodejs.



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

Начать хочется с наиболее часто встречаемой и распространенной реализации приложения – главной точкой входа – app.js, на примере веб-приложения с использованием express. Обычно выглядит она так:

// config.js

exports.port = 8080;

// app.js

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

var app = express();

app.get('/hello', function(req, res) {
    res.end('Hello world');
});

app.listen(config.port);
На первый взгляд все отлично, код понятный, конфигурация вынесена в отдельный файл и может быть изменена для дева и продакшна. Подобная реализация встречается на всех ресурсах посвященных созданию веб-приложений на nodejs. Вот мы и заложили фундамент нашей ошибки в десяти строках чистейшего кода. Но обо всем по порядку.

И так, мы написали hello world. Но, это чересчур абстрактный пример. Давайте добавим конкретики и напишем приложение которое будет выводить список файлов из указаной директории и отображать содержимое отдельных файлов, запрашивая данные из mongo.

// config.js

exports.port = 8080;
exports.mongoUrl = 'mongodb://localhost:27017/test';

// app.js

var express = require('express');
var MongoClient = require('mongodb').MongoClient;
var config = require('./config.js'); 

// Создаем соединение с базой
var db;
MongoClient.connect(config.mongoUrl, function(err, client){
    if (err) {    
        console.error(err);
        process.exit(1);
    }
    db = client;
});

// Создаем веб-сервер
var app = express();

app.get('/', function(req, res, next) {
    db.collection('files').find({}).toArray(function(err, list){
        if (err) return next(err);

        res.type('text/plain').end(list.map(function(file){
            return file.path;
        }).join('\r'));
    });
});

app.get('/file', function(req, res, next){
    db.collection('files').findOne({path:req.query.file}).toArray(function(err, file){
        if (err) return next(err);

        res.type('text/plain').end(file.content);
    });
});

app.listen(config.port);
Отлично, все просто и наглядно: соединяемся с базой, создаем сервер, назначаем обработчки для путей. Но давайте подумаем, какими недостатками обладает код:

  1. Его тяжело тестировать, так как нет возможности напрямую проверить результат возвращаемый методами.
  2. Его тяжело конфигурировать – невозможно изменить конфигурацию для двух экземпляров приложения.
  3. Компоненты приложения недоступны для внешнего кода, а значит и для расширения.
  4. Ошибки никуда не передаются и должны быть обработаны на месте или же выброшены на самый верхний уровень.

На практике это приводит к монолитному коду. И скорому рефакторингу. Что можно сделать? Необходимо разделить логику и интерфейс.
Все что касается работы приложения давайте оставим в app.js, а все что касается веб-http-интерфейса в http.js.

// app.js

var MongoClient = require('mongodb').MongoClient;
var EventEmitter = require('event').EventEmitter;
var util = require('util');

module.exports = App;

function App(config) {
    var self = this;

    // Инициализируем event emitter
    EventEmitter.call(this);

    MongoClient.connect(config.mongoUrl, function(err, db){
        if (err) return self.emit("error", err);

        self.db = db;
    });


    this.list = function(callback) {
        self.db
        .collection('files')
        .find({})
        .toArray(function(err, files){
            if (err) return callback(err);

            files = files.map(function(file){
                return file.path
            });
            callback(null, files);
        });
    };

    this.file = function(file, callback) {
        self.db
        .collection('files')
        .findOne({path:file})
        .toArray(callback);
    };
}

util.inherits(App, EventEmitter);

// config.js
exports.mongoUrl = "mongo://localhost:27017/test";

exports.http = {
    port : 8080
};

// http.js

var App = require('./app.js');
var express = require('express');

var configPath = process.argv[2] || process.env.APP_CONFIG_PATH || './config.js';
var config = require(configPath);

var app = new App(config);

app.on("error", function(err){
    console.error(err);
    process.exit(1);
});

var server = express();

server.get('/', function(req, res, next){
    app.list(function(err, files){
        if (err) return next(err);

        res.type('text/plain').end(files.join('\n'));
    });
});

server.get('/file', function(req, res, next){
    app.file(req.query.file, function(err, file){
        if (err) return next(err);

        res.type('text/plain').end(file);
    });
});

server.listen(config.http.port);
Что мы сделали? Добавили событийную модель для отлова ошибок. Добавили возможность указывать путь к конфигурации для каждого экземпляра приложения.
Таким образом мы избавились от перечисленых выше проблем:
  1. Любой метод доступен напрямую через объект app.
  2. Управление конфигурацией стало гибким: можно указать путь в консоли или через export APP_CONFIG_PATH=…
  3. Появился централизованный доступ к компонентам.
  4. Ошибки приложения отлавливаются объектом app и могут быть обработаны с учетом контекста.

Теперь мы можем легко добавить новый интерфейс для командной строки:
// cli.js

var App = require('./app.js');
var program = require('commander');

var app;

program
    .version('0.1.0')
    .option('-c, --config <file>', 'Config path', 'config.js', function(configPath){
        var config = require(configPath);

        app = new App(config);

        app.on("error", function(err){
            console.error(err);
            process.exit(1);
        });
    });

program.command('list')
    .description('List files')
    .action(function(){
        app.list(function(err, files){
            if (err) return app.emit("error", err);

            console.log(files.join('\n'));
        });
    });

program.command('print <file>')
    .description('Print file content')
    .action(function(cmd){
        app.file(cmd.file, function(err, file){
            if (err) return app.emit("error", err);

            console.log(file);
        });
    });
или тест
// test.js

var App = require('App');
var app = new App({
    mongoUrl : "mongo://testhost:27017/test"
});

// Пусть тестовая база содержит только один документ:
// {path:'README.md', content:'This is README.md'}

app.on("error", function(err){
    console.error('Test failed', err);
    process.exit(1);
});

// Тест написан для библиотеки nodeunit
module.exports = {
    "Test file list":function(test) {
        app.list(function(err, files){
            test.ok(Array.isArray(files), "Files is an Array.");
            test.equals(files.length, 1, "Files length is 1.");
            test.equals(file[0], "README.md", "Filename is 'README.md'.");
            test.done();
        });
    }
}
Конечно, приложение теперь выглядит не таким минималистичным, как в примерах, зато является боле гибким. В следующей части я расскажу про отлов ошибок.
Tags:
Hubs:
+41
Comments 20
Comments Comments 20

Articles