Сразу оговорюсь, что рекомендации больше применимы к большим проектам со сложной структурой.
Начать хочется с наиболее часто встречаемой и распространенной реализации приложения – главной точкой входа – 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);
Отлично, все просто и наглядно: соединяемся с базой, создаем сервер, назначаем обработчки для путей. Но давайте подумаем, какими недостатками обладает код:
- Его тяжело тестировать, так как нет возможности напрямую проверить результат возвращаемый методами.
- Его тяжело конфигурировать – невозможно изменить конфигурацию для двух экземпляров приложения.
- Компоненты приложения недоступны для внешнего кода, а значит и для расширения.
- Ошибки никуда не передаются и должны быть обработаны на месте или же выброшены на самый верхний уровень.
На практике это приводит к монолитному коду. И скорому рефакторингу. Что можно сделать? Необходимо разделить логику и интерфейс.
Все что касается работы приложения давайте оставим в 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);
Что мы сделали? Добавили событийную модель для отлова ошибок. Добавили возможность указывать путь к конфигурации для каждого экземпляра приложения.
Таким образом мы избавились от перечисленых выше проблем:
- Любой метод доступен напрямую через объект app.
- Управление конфигурацией стало гибким: можно указать путь в консоли или через export APP_CONFIG_PATH=…
- Появился централизованный доступ к компонентам.
- Ошибки приложения отлавливаются объектом 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(); }); } }
Конечно, приложение теперь выглядит не таким минималистичным, как в примерах, зато является боле гибким. В следующей части я расскажу про отлов ошибок.
ссылка на оригинал статьи http://habrahabr.ru/post/241788/
Добавить комментарий