Тонкощі nodejs. Частина I: горезвісний app.js

Я працюю з 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();
});
}
}
Звичайно, додаток тепер виглядає не таким мінімалістичним, як у прикладах, зате є більш гнучким. У наступній частині я розповім про вилов помилок.

Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.