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

Post on 10-May-2015

764 views 1 download

Tags:

description

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

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