Location via proxy:   [ UP ]  
[Report a bug]   [Manage cookies]                
SlideShare a Scribd company logo
MEAN - Notes from the field
Chris Clarke
Hydrahack Birmingham
18th March 2014
Full-Stack Development with Javascript
MEAN - Notes from the field (Full-Stack Development with Javascript)
MEAN - Notes from the field (Full-Stack Development with Javascript)
• Mongo
• Express
• AngularJS
• NodeJS
http://github.com/linnovate/mean
What’s MEAN?
Who are Talis?
• MongoDB ~2.5yrs
• Using express/node ~2yrs
• Angular ~9 months
Angular AppAngular App
Typical MEAN Shape
DBDB
APIAPI
Server side
pages
Server side
pages
StaticsStatics
JSON
JSON
HTMLHTML
JSON
Client side
Server side
Typical structure
Typical structure
Typical structure
Typical structure
Video Timeline Editor
Textbook Player
Angular 101
• Single page web app framework, by Google
• Extends HTML vocabulary to provide dynamic
views
• Broadly MVC (more accurately MVVM)
• Bi-directional data binding to HTML
Angular 101
• Routing
• Templates
• Controllers
• Directives
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
Routing
$routeProvider.when('/modules/:module_id', {
templateUrl: 'partials/module.html',
controller: 'TeachCtrl',
loginRequired: true,
activeTab:"teach"
});
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<ul ng-show="modules!=null">
<li ng-repeat="m in modules | orderBy:'title'"
ng-class="{active:module._id==m._id}">
<a ng-href="#/modules/{{ m._id }}">{{m.title}}</a>
</li>
<li>
<a ng-click="add()">Add new</a>
</li>
</ul>
<input ng-model="profile.first_name" type="text" required>
<input ng-model="profile.surname" type="text" required>
<input ng-model="profile.email" type="email" required>
<button ng-disabled="!profile.email"
ng-click="update()">Update</button>
<input ng-model="profile.first_name" type="text" required>
<input ng-model="profile.surname" type="text" required>
<input ng-model="profile.email" type="email" required>
<button ng-disabled="!profile.email"
ng-click="update()">Update</button>
<input ng-model="profile.first_name" type="text" required>
<input ng-model="profile.surname" type="text" required>
<input ng-model="profile.email" type="email" required>
<button ng-disabled="!profile.email"
ng-click="update()">Update</button>
<input ng-model="profile.first_name" type="text" required>
<input ng-model="profile.surname" type="text" required>
<input ng-model="profile.email" type="email" required>
<button ng-disabled="!profile.email"
ng-click="update()">Update</button>
QuickTime™ and a
'avc1' decompressor
are needed to see this picture.
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Controllers Horizontal
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
// update the profile
$scope.update = function() {
userSvc.updateProfile($scope.profile,function(err,profile) {
if (!err) {
$scope.profile = profile;
}
});
})
.controller('SomeOtherCtrl',....);
Directives
<textbook-player user="user" textbook="textbook">
...
</textbook-player>
Directives
<textbook-player user="user" textbook="textbook">
...
</textbook-player>
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
angular.module('talis.directives.player.textbook', [])
.directive("textbookPlayer", function() {
return {
restrict: "A",
scope: {
user: '=',
entity: '='
},
controller: function($scope,textbookSvc) {
// textbook logic in here
}
}
});
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
angular.module('talis.directives.player.textbook', [])
.directive("textbookPlayer", function() {
return {
restrict: "A",
scope: {
user: '=',
entity: '='
},
controller: function($scope,textbookSvc) {
// textbook logic in here
}
}
});
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
angular.module('talis.directives.player.textbook', [])
.directive("textbookPlayer", function() {
return {
restrict: "A",
scope: {
user: '=',
entity: '='
},
controller: function($scope,textbookSvc) {
// textbook logic in here
}
}
});
Directives
<div user="user" textbook="textbook" textbook-player>
...
</div>
angular.module('talis.directives.player.textbook', [])
.directive("textbookPlayer", function() {
return {
restrict: "A",
scope: {
user: '=',
entity: '='
},
controller: function($scope,textbookSvc) {
// textbook logic in here
}
}
});
–Jonny Clientside
“Waat?”
Notes From the Field
Act I: The Basics
Elem vs. Attr directives
<textbook-player user="user" textbook="textbook">
...
</textbook-player>
<div user="user" textbook="textbook" textbook-player>
...
</div>
Minification
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
..
});
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',['$scope','userSvc’,
function($scope, userSvc) {
...
}
]);
Minification
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',function($scope, userSvc) {
..
});
angular.module('talis.controllers.user', [])
.controller('AccountCtrl',['$scope','userSvc’,
function($scope, userSvc) {
...
}
]);
a.m('talis.controllers.user', [])
.c('AccountCtrl',['$scope','userSvc’,
function(s, u) {
...
}
]);
Mongo _id
{
_id: ObjectId(1234),
name: “Jonny Clientside”,
age: 24,
interests: [‘JQuery’,‘HTML5’
}
<a ng-href="#/people/{{ p._id }}">{{p.name}}</a>
Notes From the Field
Act II: Advanced
Angular AppAngular App
Typical MEAN Shape
DBDB
APIAPI
Server side
pages
Server side
pages
StaticsStatics
JSON
JSON
HTMLHTML
JSON
Client side
Server side
Angular AppAngular App
JSON
9090
9090
9090
9090
Users
API
Users
API
APIAPI
Server side
pages
Server side
pages
StaticsStatics
JSON
JSON
JSON
Client side
Meta
API
Meta
API
Files
API
Files
API
Anno
API
Anno
API
JSON
DBDBDBDBDBDB DBDB
RedisRedisRedisRedis
HTMLHTML
JSON
Logging
• A lot of activity in the client side
• Some within Express/Node server side
• More behind your API proxy
var loggingModule = angular.module('talis.services.logging', []);
loggingModule.factory(
"traceService",
function(){
return({
print: printStackTrace
});
}
);
loggingModule.provider(
"$exceptionHandler",{
$get: function(exceptionLoggingService){
return(exceptionLoggingService);
}
}
);
var loggingModule = angular.module('talis.services.logging', []);
loggingModule.factory(
"traceService",
function(){
return({
print: printStackTrace
});
}
);
loggingModule.provider(
"$exceptionHandler",{
$get: function(exceptionLoggingService){
return(exceptionLoggingService);
}
}
);
loggingModule.factory(
"exceptionLoggingService",
["$log","$window", "traceService",
function($log, $window, traceService){
function error(exception, cause){
$log.error.apply($log, arguments);
try{
var errorMessage = exception.toString();
var stackTrace = traceService.print({e: exception});
$.ajax({
type: "POST",
url: "/logger",
contentType: "application/json",
data: angular.toJson({
url: $window.location.href,
message: errorMessage,
type: "exception",
stackTrace: stackTrace,
cause: ( cause || "")
})
});
} catch (loggingError){
$log.warn("Error server-side logging failed");
$log.log(loggingError);
}
}
return(error);
}]
);
MEAN - Notes from the field (Full-Stack Development with Javascript)
Logging
Security
• APIs secured with OAuth 2.0 Bearer tokens
• Tokens obtained with a key/secret
• If your app is downloaded and run on the client,
where do you put the secret?
Security
• Have node return the OAuth token as JSON behind
a login barrier
• Angular requests this JSON when a route that
requires login is first requsted
• If status != 200, Angular app redirects browser to
login page
• User logs in, repeat
Security
• Dealing with tokens on every service call is a PITA
• Tokens expiring is normal
• Deal with it globally using a couple of advanced
$http features
.run(function($rootScope,$injector) {
$injector.get("$http").defaults.transformRequest =
function(data, headersGetter) {
headersGetter()['Authorization']="Bearer "+$rootScope.token
if (data) {
return angular.toJson(data);
}
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
$httpProvider.responseInterceptors.push(
function ($rootScope, $q, $injector, $location) {
return function(promise) {
return promise.then(function(response) {
return response; // no action, was successful
}, function (response) {
// error - was it 401 or something else?
if (response.status===401 && response.data.error && response.data.error === "invalid_token") {
var deferred = $q.defer(); // defer until we can re-request a new token
// Get a new token... (cannot inject $http directly as will cause a circular ref)
$injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK')
.then(function(loginResponse) {
if (loginResponse.data) {
$rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope
// now let's retry the original request
$injector.get("$http")(response.config).then(function(response) {
// we have a successful response - resolve it using deferred
deferred.resolve(response);
},function(response) {
deferred.reject(); // something went wrong
});
} else {
deferred.reject(); // login.json didn't give us data
}
}, function(response) {
deferred.reject(); // token retry failed, redirect so user can login again
$location.path('/user/sign/in');
return;
});
return deferred.promise; // return the deferred promise
}
return $q.reject(response); // not a recoverable error
});
};
});
Environments
• Pretty usual to deal with prod, dev, testing
environment config on the server side
• Inject this into your client side app using a dynamic
JS include
Environments
<script type="text/javascript" src="env/config.js"></script>
Environments
angular.module('talis.environment', [], function($provide)
constant('API_ENDPOINT', 'http://localhost:3000').
constant('ACTIVATE_FEATURE_FLIPS',true);
Environments
angular.module('talis.environment', [], function($provide)
constant('API_ENDPOINT', 'https://talis.com').
constant('ACTIVATE_FEATURE_FLIPS',false);
That’s it.
http://engineering.talis.com
We are hiring!
http://www.talis.com/jobs
@talis
facebook.com/talisgroup
+44 (0) 121 374 2740
talis.com
info@talis.com
48 Frederick Street
Birmingham
B1 3HN

More Related Content

MEAN - Notes from the field (Full-Stack Development with Javascript)

  • 1. MEAN - Notes from the field Chris Clarke Hydrahack Birmingham 18th March 2014 Full-Stack Development with Javascript
  • 4. • Mongo • Express • AngularJS • NodeJS http://github.com/linnovate/mean What’s MEAN?
  • 6. • MongoDB ~2.5yrs • Using express/node ~2yrs • Angular ~9 months
  • 7. Angular AppAngular App Typical MEAN Shape DBDB APIAPI Server side pages Server side pages StaticsStatics JSON JSON HTMLHTML JSON Client side Server side
  • 14. Angular 101 • Single page web app framework, by Google • Extends HTML vocabulary to provide dynamic views • Broadly MVC (more accurately MVVM) • Bi-directional data binding to HTML
  • 15. Angular 101 • Routing • Templates • Controllers • Directives
  • 21. <ul ng-show="modules!=null"> <li ng-repeat="m in modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 22. <ul ng-show="modules!=null"> <li ng-repeat="m in modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 23. <ul ng-show="modules!=null"> <li ng-repeat="m in modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 24. <ul ng-show="modules!=null"> <li ng-repeat="m in modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 25. <ul ng-show="modules!=null"> <li ng-repeat="m in modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 26. <ul ng-show="modules!=null"> <li ng-repeat="m in modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 27. <ul ng-show="modules!=null"> <li ng-repeat="m in modules | orderBy:'title'" ng-class="{active:module._id==m._id}"> <a ng-href="#/modules/{{ m._id }}">{{m.title}}</a> </li> <li> <a ng-click="add()">Add new</a> </li> </ul>
  • 28. <input ng-model="profile.first_name" type="text" required> <input ng-model="profile.surname" type="text" required> <input ng-model="profile.email" type="email" required> <button ng-disabled="!profile.email" ng-click="update()">Update</button>
  • 29. <input ng-model="profile.first_name" type="text" required> <input ng-model="profile.surname" type="text" required> <input ng-model="profile.email" type="email" required> <button ng-disabled="!profile.email" ng-click="update()">Update</button>
  • 30. <input ng-model="profile.first_name" type="text" required> <input ng-model="profile.surname" type="text" required> <input ng-model="profile.email" type="email" required> <button ng-disabled="!profile.email" ng-click="update()">Update</button>
  • 31. <input ng-model="profile.first_name" type="text" required> <input ng-model="profile.surname" type="text" required> <input ng-model="profile.email" type="email" required> <button ng-disabled="!profile.email" ng-click="update()">Update</button>
  • 32. QuickTime™ and a 'avc1' decompressor are needed to see this picture.
  • 33. Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 34. Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 35. Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 36. Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 37. Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 38. Controllers Horizontal angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc) { // update the profile $scope.update = function() { userSvc.updateProfile($scope.profile,function(err,profile) { if (!err) { $scope.profile = profile; } }); }) .controller('SomeOtherCtrl',....);
  • 41. Directives <div user="user" textbook="textbook" textbook-player> ... </div>
  • 42. Directives <div user="user" textbook="textbook" textbook-player> ... </div> angular.module('talis.directives.player.textbook', []) .directive("textbookPlayer", function() { return { restrict: "A", scope: { user: '=', entity: '=' }, controller: function($scope,textbookSvc) { // textbook logic in here } } });
  • 43. Directives <div user="user" textbook="textbook" textbook-player> ... </div> angular.module('talis.directives.player.textbook', []) .directive("textbookPlayer", function() { return { restrict: "A", scope: { user: '=', entity: '=' }, controller: function($scope,textbookSvc) { // textbook logic in here } } });
  • 44. Directives <div user="user" textbook="textbook" textbook-player> ... </div> angular.module('talis.directives.player.textbook', []) .directive("textbookPlayer", function() { return { restrict: "A", scope: { user: '=', entity: '=' }, controller: function($scope,textbookSvc) { // textbook logic in here } } });
  • 45. Directives <div user="user" textbook="textbook" textbook-player> ... </div> angular.module('talis.directives.player.textbook', []) .directive("textbookPlayer", function() { return { restrict: "A", scope: { user: '=', entity: '=' }, controller: function($scope,textbookSvc) { // textbook logic in here } } });
  • 47. Notes From the Field Act I: The Basics
  • 48. Elem vs. Attr directives <textbook-player user="user" textbook="textbook"> ... </textbook-player> <div user="user" textbook="textbook" textbook-player> ... </div>
  • 49. Minification angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc) { .. }); angular.module('talis.controllers.user', []) .controller('AccountCtrl',['$scope','userSvc’, function($scope, userSvc) { ... } ]);
  • 50. Minification angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc) { .. }); angular.module('talis.controllers.user', []) .controller('AccountCtrl',['$scope','userSvc’, function($scope, userSvc) { ... } ]); a.m('talis.controllers.user', []) .c('AccountCtrl',['$scope','userSvc’, function(s, u) { ... } ]);
  • 51. Mongo _id { _id: ObjectId(1234), name: “Jonny Clientside”, age: 24, interests: [‘JQuery’,‘HTML5’ } <a ng-href="#/people/{{ p._id }}">{{p.name}}</a>
  • 52. Notes From the Field Act II: Advanced
  • 53. Angular AppAngular App Typical MEAN Shape DBDB APIAPI Server side pages Server side pages StaticsStatics JSON JSON HTMLHTML JSON Client side Server side
  • 54. Angular AppAngular App JSON 9090 9090 9090 9090 Users API Users API APIAPI Server side pages Server side pages StaticsStatics JSON JSON JSON Client side Meta API Meta API Files API Files API Anno API Anno API JSON DBDBDBDBDBDB DBDB RedisRedisRedisRedis HTMLHTML JSON
  • 55. Logging • A lot of activity in the client side • Some within Express/Node server side • More behind your API proxy
  • 56. var loggingModule = angular.module('talis.services.logging', []); loggingModule.factory( "traceService", function(){ return({ print: printStackTrace }); } ); loggingModule.provider( "$exceptionHandler",{ $get: function(exceptionLoggingService){ return(exceptionLoggingService); } } );
  • 57. var loggingModule = angular.module('talis.services.logging', []); loggingModule.factory( "traceService", function(){ return({ print: printStackTrace }); } ); loggingModule.provider( "$exceptionHandler",{ $get: function(exceptionLoggingService){ return(exceptionLoggingService); } } );
  • 58. loggingModule.factory( "exceptionLoggingService", ["$log","$window", "traceService", function($log, $window, traceService){ function error(exception, cause){ $log.error.apply($log, arguments); try{ var errorMessage = exception.toString(); var stackTrace = traceService.print({e: exception}); $.ajax({ type: "POST", url: "/logger", contentType: "application/json", data: angular.toJson({ url: $window.location.href, message: errorMessage, type: "exception", stackTrace: stackTrace, cause: ( cause || "") }) }); } catch (loggingError){ $log.warn("Error server-side logging failed"); $log.log(loggingError); } } return(error); }] );
  • 61. Security • APIs secured with OAuth 2.0 Bearer tokens • Tokens obtained with a key/secret • If your app is downloaded and run on the client, where do you put the secret?
  • 62. Security • Have node return the OAuth token as JSON behind a login barrier • Angular requests this JSON when a route that requires login is first requsted • If status != 200, Angular app redirects browser to login page • User logs in, repeat
  • 63. Security • Dealing with tokens on every service call is a PITA • Tokens expiring is normal • Deal with it globally using a couple of advanced $http features
  • 64. .run(function($rootScope,$injector) { $injector.get("$http").defaults.transformRequest = function(data, headersGetter) { headersGetter()['Authorization']="Bearer "+$rootScope.token if (data) { return angular.toJson(data); } }; });
  • 65. $httpProvider.responseInterceptors.push( function ($rootScope, $q, $injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 66. $httpProvider.responseInterceptors.push( function ($rootScope, $q, $injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 67. $httpProvider.responseInterceptors.push( function ($rootScope, $q, $injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 68. $httpProvider.responseInterceptors.push( function ($rootScope, $q, $injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 69. $httpProvider.responseInterceptors.push( function ($rootScope, $q, $injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 70. $httpProvider.responseInterceptors.push( function ($rootScope, $q, $injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 71. $httpProvider.responseInterceptors.push( function ($rootScope, $q, $injector, $location) { return function(promise) { return promise.then(function(response) { return response; // no action, was successful }, function (response) { // error - was it 401 or something else? if (response.status===401 && response.data.error && response.data.error === "invalid_token") { var deferred = $q.defer(); // defer until we can re-request a new token // Get a new token... (cannot inject $http directly as will cause a circular ref) $injector.get("$http").jsonp('/some/endpoint/that/reissues/tokens?cb=JSON_CALLBACK') .then(function(loginResponse) { if (loginResponse.data) { $rootScope.oauth = loginResponse.data.oauth; // we have a new oauth token - set at $rootScope // now let's retry the original request $injector.get("$http")(response.config).then(function(response) { // we have a successful response - resolve it using deferred deferred.resolve(response); },function(response) { deferred.reject(); // something went wrong }); } else { deferred.reject(); // login.json didn't give us data } }, function(response) { deferred.reject(); // token retry failed, redirect so user can login again $location.path('/user/sign/in'); return; }); return deferred.promise; // return the deferred promise } return $q.reject(response); // not a recoverable error }); }; });
  • 72. Environments • Pretty usual to deal with prod, dev, testing environment config on the server side • Inject this into your client side app using a dynamic JS include
  • 74. Environments angular.module('talis.environment', [], function($provide) constant('API_ENDPOINT', 'http://localhost:3000'). constant('ACTIVATE_FEATURE_FLIPS',true);
  • 75. Environments angular.module('talis.environment', [], function($provide) constant('API_ENDPOINT', 'https://talis.com'). constant('ACTIVATE_FEATURE_FLIPS',false);
  • 79. @talis facebook.com/talisgroup +44 (0) 121 374 2740 talis.com info@talis.com 48 Frederick Street Birmingham B1 3HN

Editor's Notes

  1. Anyone been playing this?
  2. WTF?
  3. Check out the score - weird! Back to work…
  4. Whole stack is JS, from the DB, the native data format (BSON), the server side and the front end. Even your DB queries are JSON and the DB client for debugging
  5. Birmingham EdTech learning platform 65 unis 1 million students 50% UK + other campuses on 3 continents ~15M hits per week page/api Legacy in PHP
  6. Node app here.
  7. Angular app here
  8. Note the node app serves index.html which includes app.js which bootstraps the angular app
  9. Some things we are building with MEAN Timeline editing UI
  10. Textbooks Copy and paste from OCRed images Rich annotations + notes In-book search
  11. Bi directional is AWESOME! Whistle stop tour
  12. Bi directional is AWESOME! Whistle stop tour
  13. Pretty familiar
  14. bind to params in URL
  15. our HTML template
  16. the controller
  17. can add extra properties to the route
  18. update() is a method in the CONTROLLER
  19. Does this via event loop
  20. dependancy injection $scope and userSvc are singletons
  21. SERVICES - generally logic for persisting models or other interactions with the server side
  22. wrapping behavior and logic to specific tags, independent from view-level controller independent scope, many on same view great for re-use
  23. user and textbook passed as parameters from view scope
  24. More common to use as attribute name
  25. namespace
  26. directive function
  27. view scope -&amp;gt; directive scope (user/entity)
  28. directive controller - $scope is injected by framework
  29. If you’re a client side developer then Angular comes as a shock Actually if you’re a server side guy you’ll be more at home Resist JQuery
  30. First some common gotchas
  31. For IE compat use attribute
  32. Dependancy injection Minification will shorten $scope, userSvc Array param will assume last param func, preceeding params strings mapping to vars
  33. End result
  34. Caught us out moving to 1.2 Angular assumes all _ properties are private PITA in templates Backed out in later versions
  35. You can find these on our blog
  36. Remember this?
  37. Talis App shape - SOA Proxy API CORS ripple of change protection - what version of angular app is it? api versioning
  38. Angular has built in $exceptionHandler that will log to console :-/
  39. Why are we using $.ajax?
  40. Collect more feedback from user Propigate unexpected errors up to $rootScope Form in your master template
  41. Logstash/ElasticSearch/Kibana Consider watermarking log statements through stack using an ID
  42. https!!
  43. transformRequest in your app’s run method adds the token (where available) to every single request made with $http
  44. Dealing with token expiry and re-request responseInterceptor
  45. Dealing with token expiry and re-request responseInterceptor
  46. Dealing with token expiry and re-request responseInterceptor
  47. Dealing with token expiry and re-request responseInterceptor
  48. Dealing with token expiry and re-request responseInterceptor
  49. Dealing with token expiry and re-request responseInterceptor
  50. Dealing with token expiry and re-request responseInterceptor
  51. In your main page
  52. Dynamically generated by a service in node constants available anywhere by dependancy injection
  53. in production
  54. You can find these on our blog