Upload
laurentvb
View
3.921
Download
2
Embed Size (px)
DESCRIPTION
A case-study of using node.js to develop entreprise applications. Includes real-world examples for express.js, mongoose.js and async.js
Citation preview
1
Developing web-apps like it’s 2013
a case-study of using node.js to build entreprise applications
2
Who?
Laurent Van Basselaere@Laurent_VB
I do stuff with codeat Arhs Developments
3
ARHS Developments
10 years, 300 peopleConsulting & fixed price projectsSoftware (Java)Business Intelligence
4
testing
5
Testing
6
7
MS Access? SRSLY?
MS AccessMultiple copiesSingle userNo remote useLame
8
Fatman FTW
MS Access BrowserMultiple versions CentralizedSingle user Unlimited usersNo remote use EverywhereLame Awesome
9
We wrote a web-app
10
Fatman tech stack
BootstrapKnockoutExpressNodeMongooseMongoDB
asyncunderscoremoment
11
Elegant MongoDB object modeling for Node.js
ORM seemed desirableClean object model to use in app code
var mongoose = require(‘mongoose’);mongoose.connect(‘localhost’, ‘fatman’);
var ProjectSchema = new mongoose.Schema({ id : String , name : String , users : [String]});
var Project = mongoose.model('Project', ProjectSchema);
Schema
12
var project = new Project({ id: ‘AKCT’ , name: ‘GOCA Newsoft AKCT’ , users: [‘vanbasla’, ‘grosjech’]});
project.save(function (err){ //… callback after save});
CREATE/EDIT
13
// find project by idProject.where(‘id’, ‘AKCT’) .findOne(function(err, project) { // do something with search result });
// find my projectsProject.find({‘users’: username}) .exec(function(err, projects){ // do something with search results });
RETRIEVE
14
function trim(value){ return value ? value.trim() : value;}function lessThan80chars(value){ return value.length <= 80;}
var ProjectSchema = new mongoose.Schema({ id : {type: String, required: true, unique: true} , name : {type: String, set: trim, validate: [ lessThan80chars, 'Value too long. Max 80 characters.']}});
MORE Schema
15
// staticsProjectSchema.statics.findById = function(projectId, cb){ Project.where('id', projectId).findOne(cb);};
// methodsProjectSchema.methods.issueTrackerEnabled = function() { return this.issueTracker != null;};
// middlewareProjectCaseSchema.pre(‘remove’, function(next) { // do something when a Project is deleted next();});
Advanced
16
17
In fatman
We use setters+ pre-save middleware
to keep history of edits.
// creates a setter for fieldfunction setter(field) { return function setField(newValue) { this._oldValues = this._oldValues || {}; this._oldValues[field] = this[field]; return newValue; }}
var TestCaseSchema = new Schema({ id: {type:Number,index:true}, description: {type:String, set: setter('description')}, history: [Schema.Types.Mixed]});
In fatman
18
// Populate history before save.TestCaseSchema.pre('save', function (next) { var self = this , oldValues = this._oldValues || {};
delete this._oldValues;
this.modifiedPaths.forEach(function (field) { if (field in oldValues) { self.history.push({ ‘old': oldValues[field], ‘new’: self[field] }); } });
next();});
In fatman
19
Express is a minimal and flexible node.js web application framework.
Simple and modularNode de-facto standard
var express = require('express'), consolidate = require('consolidate');
// create an express appvar app = express();
// configure view engineapp.engine('html', consolidate.handlebars);app.set('views', __dirname + '/views');
// configure a route with an url parameterapp.get('/hello/:name', hello);
function hello(req, res, next){ res.render('hello.html', { 'name' : req.params.name });}
app.listen(1337);console.log('Listening on port 1337');
Hello Express
function list (req, res, next){ Project.findById(req.params.projectId, function(err, project){ if (err) return next(err); TestCase.find({‘project’: project}, function(err, testcases){ if (err) return next(err); res.render(‘testcases.html’, { ‘project’: project, ‘testcases’: testcases }); }); });}
controller
function show (req, res, next){ Project.findById(req.params.projectId, function(err, project){ if (err) return next(err); TestCase.findOne({‘project’:project, ‘id’:id}, function(err, tc){ if (err) return next(err); res.render(‘testcase.html’, { ‘project’: project, ‘testcase’: tc }); }); });}
controller
function save (req, res, next){ Project.findById(req.params.projectId, function(err, project){ if (err) return next(err); var tc = new TestCase(req.body); tc.project = project; tc.save(function(err, tc){ if (err) return next(err); // redirect after post res.redirect(req.url); }); });}
controller
function loadProject(req, res, next){ Project.findById(req.params.projectId, function(err, project){ if (err) return next(err);
res.locals.project = project; next(); });}
// before all routes requiring a projectapp.all('/:projectId/*', loadProject);
MIDDLEWARE
function list (req, res, next){ var project = res.locals.project; TestCase.find({‘project’:project}, function(err, testcases){ if (err) return next(err); res.render(‘testcases.html’, { ‘testcases’: testcases }); });}
BETTER
function show (req, res, next){ var project = res.locals.project; TestCase.findOne({‘project’:project, ‘id’:id}, function(err, tc){ if (err) return next(err);
res.render(‘testcase.html’, { ‘testcase’: tc }); });}
BETTer
function save (req, res, next){ var tc = new TestCase(req.body); tc.project = res.locals.project; tc.save(function(err){ if (err) return next(err); res.redirect(req.url); });}
BETTer
function search (req, res, next){ Project.findById(projectId, function(err, project){ if (err) return next(err);
TestPlan.findByIdentifier(project, testPlanId, function(err, testPlan) { if (err) return next(err);
var tags = getTagsFromRequest(req); TestCase.findByTag(testPlan, tags, function(err, tagQuery, testCases){ if (err) return next(err);
TestCase.countTags(tagQuery, function(err, tagsResult) { if (err) return next(err);
res.render(‘search’, { ‘testCases’ : testCases, ‘tagsResult’ : tagsResult, ‘project’ : project, ‘testPlan’ : testPlan, ‘tags’ : tags }); }); }); }); });}
pyramid of doom
Async is a utility module which provides straight-forward, powerful functions for working with asynchronous JavaScript
Async.js
function search (req, res, next){ async.waterfall([ function(cb){ Project.findById(projectId, cb); }, function(cb, project){ res.locals.project = project; TestPlan.findByIdentifier(project, testPlanId, cb); }, function(cb, testPlan){ res.locals.testPlan = testPlan; var tags = res.locals.tags = getTagsFromRequest(req); TestCase.findByTag(testPlan, tags, cb); }, function(cb, tagQuery, testCases){ res.locals.testCases = testCases; TestCase.countTags(tagQuery, cb); } ], function(err, tagsResult){ if (err) return next(err); res.render(‘search’, tagsResult); });}
pyramid NO MOREAsync.js
var ids = [‘AKCT’, ‘FATMAN’];var projects = [];
ids.forEach(function(id){ Project.findById(id, function(err, project){ projects.push(project); });});res.render(‘projects’, {‘project’ : projects});
MORE async Async.js
WRONG
var ids = [‘AKCT’, ‘FATMAN’];var projects = [];
async.each(ids, function(id, next){ Project.findById(id, function(err, project){ projects.push(project); next(); })}, function(err){ res.render(‘projects’, {‘projects’: projects});});
MORE async Async.js
Collections
eachmapfilterrejectreducedetectsortBy…
MORE async Async.js
Control flow
seriesparallelwhilstdoWhilstuntildoUntilwaterfall…
FATMAN
60 days dev6 months prod12 projects (2 to 10 users)
FATMAN
FATMAN
FATMAN