📜 ⬆️ ⬇️

Subtleties nodejs. Part I: the notorious app.js

I have been working with node.js for more than three years and during this time I managed to become familiar with the platform, its strengths and weaknesses. During this time, the platform has changed a lot, just like javascript itself. The idea of ​​using one medium on both the server and the client was much to the liking. Still would! It is convenient and easy! But, unfortunately, in practice, everything turned out to be not so rosy, along with the pluses, the platform absorbed the minuses of the language used, and the different approach to implementation almost nullified the advantages of using a single environment. So all attempts to implement server js before the node did not take off, take the same Rhino. And, most likely, the node was waiting for the same fate, if not for the legendary V8, non-blocking code and amazing performance. That is why developers love it so much. In this series of articles, I will try to talk about the problems that are unobvious at first glance and the intricacies of work that you will encounter in the development on nodejs.



At once I will make a reservation that the recommendations are more applicable to large projects with a complex structure.
')
I want to start with the most common and common implementation of the application - the main entry point - app.js, using the example of a web application using express. It usually looks like this:

// 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); 
At first glance, everything is fine, the code is clear, the configuration is moved to a separate file and can be changed for virgin and production. Such an implementation is found on all resources devoted to the creation of web applications on nodejs. So we laid the foundation of our error in ten lines of the purest code. But first things first.

And so, we wrote hello world. But, this is an overly abstract example. Let's add specifics and write an application that will display a list of files from the specified directory and display the contents of individual files, requesting data from 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); 
Ok, everything is simple and clear: connect to the base, create a server, assign handles for the paths. But let's think about what disadvantages the code has:

  1. It is difficult to test it, since there is no possibility to directly check the result returned by the methods.
  2. It is difficult to configure - it is impossible to change the configuration for two instances of the application.
  3. Application components are not available for external code, and therefore for the extension.
  4. Errors are not transmitted anywhere and must be processed on the spot or thrown to the highest level.

In practice, this leads to a monolithic code. And fast refactoring. What can be done? It is necessary to separate the logic and interface.
As for the application, let's leave everything in app.js, and everything as regards the web http interface in 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); 
What have we done? Added event model for catching errors. Added the ability to specify the configuration path for each application instance.
Thus, we got rid of the problems listed above:
  1. Any method is directly accessible via the app object.
  2. Configuration management has become flexible: you can specify the path in the console or via export APP_CONFIG_PATH = ...
  3. Appeared centralized access to components.
  4. Application errors are caught by the app object and can be handled with context.

Now we can easily add a new command line interface:
 // 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); }); }); 
or test
 // 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(); }); } } 
Of course, the application now does not look as minimalistic as in the examples, but it is more flexible. In the next part I will tell about catching errors.

Source: https://habr.com/ru/post/241788/


All Articles