Your SlideShare is downloading. ×
  • Like
MEAN - Notes from the field (Full-Stack Development with Javascript)
Upcoming SlideShare
Loading in...5
×

Thanks for flagging this SlideShare!

Oops! An error has occurred.

×

Now you can save presentations on your phone or tablet

Available for both IPhone and Android

Text the download link to your phone

Standard text messaging rates apply

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

  • 651 views
Published

Full-Stack Development with Javascript Angular/Node (AngularJS/NodeJS)

Full-Stack Development with Javascript Angular/Node (AngularJS/NodeJS)

Published in Technology
  • Full Name Full Name Comment goes here.
    Are you sure you want to
    Your message goes here
    Be the first to comment
No Downloads

Views

Total Views
651
On SlideShare
0
From Embeds
0
Number of Embeds
2

Actions

Shares
Downloads
22
Comments
0
Likes
3

Embeds 0

No embeds

Report content

Flagged as inappropriate Flag as inappropriate
Flag as inappropriate

Select your reason for flagging this presentation as inappropriate.

Cancel
    No notes for slide
  • Anyone been playing this?
  • WTF?
  • Check out the score - weird!
    Back to work…
  • 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
  • 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
  • Node app here.
  • Angular app here
  • Note the node app serves index.html which includes app.js which bootstraps the angular app
  • Some things we are building with MEAN
    Timeline editing UI
  • Textbooks
    Copy and paste from OCRed images
    Rich annotations + notes
    In-book search
  • Bi directional is AWESOME!
    Whistle stop tour
  • Bi directional is AWESOME!
    Whistle stop tour
  • Pretty familiar
  • bind to params in URL
  • our HTML template
  • the controller
  • can add extra properties to the route
  • update() is a method in the CONTROLLER
  • Does this via event loop
  • dependancy injection
    $scope and userSvc are singletons
  • SERVICES - generally logic for persisting models or other interactions with the server side
  • wrapping behavior and logic to specific tags, independent from view-level controller
    independent scope, many on same view
    great for re-use
  • user and textbook passed as parameters from view scope
  • More common to use as attribute name
  • namespace
  • directive function
  • view scope -> directive scope (user/entity)
  • directive controller - $scope is injected by framework
  • 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
  • First some common gotchas
  • For IE compat use attribute
  • Dependancy injection
    Minification will shorten $scope, userSvc
    Array param will assume last param func, preceeding params strings mapping to vars
  • End result
  • Caught us out moving to 1.2
    Angular assumes all _ properties are private
    PITA in templates
    Backed out in later versions
  • You can find these on our blog
  • Remember this?
  • Talis App shape - SOA
    Proxy API
    CORS
    ripple of change protection - what version of angular app is it?
    api versioning
  • Angular has built in $exceptionHandler that will log to console :-/
  • Why are we using $.ajax?
  • Collect more feedback from user
    Propigate unexpected errors up to $rootScope
    Form in your master template
  • Logstash/ElasticSearch/Kibana
    Consider watermarking log statements through stack using an ID
  • https!!
  • transformRequest
    in your app’s run method
    adds the token (where available) to every single request made with $http
  • Dealing with token expiry and re-request
    responseInterceptor
  • Dealing with token expiry and re-request
    responseInterceptor
  • Dealing with token expiry and re-request
    responseInterceptor
  • Dealing with token expiry and re-request
    responseInterceptor
  • Dealing with token expiry and re-request
    responseInterceptor
  • Dealing with token expiry and re-request
    responseInterceptor
  • Dealing with token expiry and re-request
    responseInterceptor
  • In your main page
  • Dynamically generated by a service in node
    constants available anywhere by dependancy injection
  • in production
  • You can find these on our blog

Transcript

  • 1. MEAN - Notes from the field Chris Clarke Hydrahack Birmingham 18th March 2014 Full-Stack Development with Javascript
  • 2. • Mongo • Express • AngularJS • NodeJS http://github.com/linnovate/mean What’s MEAN?
  • 3. Who are Talis?
  • 4. • MongoDB ~2.5yrs • Using express/node ~2yrs • Angular ~9 months
  • 5. Angular AppAngular App Typical MEAN Shape DBDB APIAPI Server side pages Server side pages StaticsStatics JSON JSON HTMLHTML JSON Client side Server side
  • 6. Typical structure
  • 7. Typical structure
  • 8. Typical structure
  • 9. Typical structure
  • 10. Video Timeline Editor
  • 11. Textbook Player
  • 12. 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
  • 13. Angular 101 • Routing • Templates • Controllers • Directives
  • 14. Routing $routeProvider.when('/modules/:module_id', { templateUrl: 'partials/module.html', controller: 'TeachCtrl', loginRequired: true, activeTab:"teach" });
  • 15. Routing $routeProvider.when('/modules/:module_id', { templateUrl: 'partials/module.html', controller: 'TeachCtrl', loginRequired: true, activeTab:"teach" });
  • 16. Routing $routeProvider.when('/modules/:module_id', { templateUrl: 'partials/module.html', controller: 'TeachCtrl', loginRequired: true, activeTab:"teach" });
  • 17. Routing $routeProvider.when('/modules/:module_id', { templateUrl: 'partials/module.html', controller: 'TeachCtrl', loginRequired: true, activeTab:"teach" });
  • 18. Routing $routeProvider.when('/modules/:module_id', { templateUrl: 'partials/module.html', controller: 'TeachCtrl', loginRequired: true, activeTab:"teach" });
  • 19. <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>
  • 20. <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>
  • 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. <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>
  • 27. <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>
  • 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. QuickTime™ and a 'avc1' decompressor are needed to see this picture.
  • 31. 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',....);
  • 32. 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',....);
  • 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. Directives <textbook-player user="user" textbook="textbook"> ... </textbook-player>
  • 38. Directives <textbook-player user="user" textbook="textbook"> ... </textbook-player>
  • 39. Directives <div user="user" textbook="textbook" textbook-player> ... </div>
  • 40. 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 } } });
  • 41. 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 } } });
  • 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. –Jonny Clientside “Waat?”
  • 45. Notes From the Field Act I: The Basics
  • 46. Elem vs. Attr directives <textbook-player user="user" textbook="textbook"> ... </textbook-player> <div user="user" textbook="textbook" textbook-player> ... </div>
  • 47. Minification angular.module('talis.controllers.user', []) .controller('AccountCtrl',function($scope, userSvc) { .. }); angular.module('talis.controllers.user', []) .controller('AccountCtrl',['$scope','userSvc’, function($scope, userSvc) { ... } ]);
  • 48. 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) { ... } ]);
  • 49. Mongo _id { _id: ObjectId(1234), name: “Jonny Clientside”, age: 24, interests: [‘JQuery’,‘HTML5’ } <a ng-href="#/people/{{ p._id }}">{{p.name}}</a>
  • 50. Notes From the Field Act II: Advanced
  • 51. Angular AppAngular App Typical MEAN Shape DBDB APIAPI Server side pages Server side pages StaticsStatics JSON JSON HTMLHTML JSON Client side Server side
  • 52. 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
  • 53. Logging • A lot of activity in the client side • Some within Express/Node server side • More behind your API proxy
  • 54. var loggingModule = angular.module('talis.services.logging', []); loggingModule.factory( "traceService", function(){ return({ print: printStackTrace }); } ); loggingModule.provider( "$exceptionHandler",{ $get: function(exceptionLoggingService){ return(exceptionLoggingService); } } );
  • 55. var loggingModule = angular.module('talis.services.logging', []); loggingModule.factory( "traceService", function(){ return({ print: printStackTrace }); } ); loggingModule.provider( "$exceptionHandler",{ $get: function(exceptionLoggingService){ return(exceptionLoggingService); } } );
  • 56. 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); }] );
  • 57. Logging
  • 58. 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?
  • 59. 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
  • 60. 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
  • 61. .run(function($rootScope,$injector) { $injector.get("$http").defaults.transformRequest = function(data, headersGetter) { headersGetter()['Authorization']="Bearer "+$rootScope.token if (data) { return angular.toJson(data); } }; });
  • 62. $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 }); }; });
  • 63. $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 }); }; });
  • 64. $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 }); }; });
  • 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. 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
  • 70. Environments <script type="text/javascript" src="env/config.js"></script>
  • 71. Environments angular.module('talis.environment', [], function($provide) constant('API_ENDPOINT', 'http://localhost:3000'). constant('ACTIVATE_FEATURE_FLIPS',true);
  • 72. Environments angular.module('talis.environment', [], function($provide) constant('API_ENDPOINT', 'https://talis.com'). constant('ACTIVATE_FEATURE_FLIPS',false);
  • 73. That’s it.
  • 74. http://engineering.talis.com
  • 75. We are hiring! http://www.talis.com/jobs
  • 76. @talis facebook.com/talisgroup +44 (0) 121 374 2740 talis.com info@talis.com 48 Frederick Street Birmingham B1 3HN