Post on 10-May-2015
description
transcript
MEAN - Notes from the field
Chris ClarkeHydrahack Birmingham18th March 2014
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 ShapeDBDB
APIAPI Server side pages
Server side pagesStaticsStatics
JSON
JSONHTMLHTML
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 ShapeDBDB
APIAPI Server side pages
Server side pagesStaticsStatics
JSON
JSONHTMLHTML
JSON
Client side
Server side
Angular AppAngular App
JSON
9090
9090
9090
9090
Users API
Users API
APIAPI Server side pages
Server side pagesStaticsStatics
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); }]);
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
@talisfacebook.com/talisgroup
+44 (0) 121 374 2740
talis.cominfo@talis.com
48 Frederick StreetBirminghamB1 3HN