πŸ“œ ⬆️ ⬇️

Will Angular.js and Facebook Login be able to make friends?

Greetings, dear readers of Habr!

I would like to dedicate my first post to what is, for the time being, the most interesting thing to work with - Angular and Node.

For some time, (about 7 months) of working with Angular, a couple of my own experiences have appeared, which I am eager to share. Of course, this is not Facebook Login itself, which is described in the Facebook JS SDK section, and not β€œHello World with Angular.js”, but it's pretty simple.
')
The motivation in writing this article is the desire to share some experience in interesting directions.

[small disclaimer]

Perhaps everything that happens below this text is an explosive mixture of delirium with pieces of code, but I sincerely hope for an objective assessment and the help of experienced developers in choosing more correct / interesting solutions.

Backend


My choice fell on Node not by chance. Well, I love JavaScript. It is also very convenient, I think. The main thing is the ability to quickly and free deploy a web application on the Internet. To list "why" does not make sense. As a server-side framework, I chose Express, because there are lots of howto articles on it, very easy to understand, simple routing. That's all you need for now.

Next, we will try to analyze the important points in more detail.

Let's see how the server responds to requests from the browser:

app.js
var express = require('express') , expressLayouts = require('express-ejs-layouts') , less = require('less-middleware') , routes = require('./routes') , config = require('./settings') , http = require('http') , path = require('path') , app = express() ; // all environments app.set('view engine', 'ejs'); app.set('views', __dirname + '/views'); app.set('layout', 'layout'); app.locals({ appName: config.APP_NAME, appId: config.APP_ID, appUrl: config.APP_URL, scope: config.SCOPE, random: Math.random() }); app.use(expressLayouts); app.use(express.favicon()); app.use(express.logger('dev')); app.use(express.bodyParser()); app.use(express.methodOverride()); app.use(express.cookieParser()); app.use(express.session({ secret: config.SECRET, maxAge: new Date(Date.now() + 3600000) })); app.use(require('less-middleware')({ force: true, dest: path.join(__dirname, 'public', 'css'), src: path.join(__dirname, 'less'), prefix: '/static/css/' })); app.use('/static', express.static(path.join(__dirname, 'public'))); app.use('/static', express.static(path.join(__dirname, 'bower_components'))); app.use(app.router); // landing page app.get('/' , routes.index); app.get('/partials/:name' , routes.partials); app.get('/partials/:folder/:name' , routes.partials); app.all(/^(\/((?!static)\w.+))+$/ , routes.index); http.createServer(app).listen(config.PORT, function(){ console.log('Express server listening on port ' + config.PORT); }); 



Let's stop only on routing, since the rest is purely technical settings, most of which are often described in tutorials like "Get started with Express"

As we are preparing to create a single-page application, we need to configure the server so that it always sends the same page and tag with the <ng-view> </ ng-view> tag to the request, in order for Angular to process the current URL according to client side routing and load the corresponding partial (more on this later ..)

So - How do we explain to Express that there is a difference between requests for static files (js, css, images), and regular pages (if we don’t describe them on the server) so that:
- he did not swear to us about the nonexistent paths of the pages and did not interfere with Angular's work;
- he nevertheless told you everything he thinks about you if you requested a non-existent static file;

Something like that:
 //       app.use('/static', express.static(path.join(__dirname, 'public'))); app.use('/static', express.static(path.join(__dirname, 'bower_components'))); app.use(app.router); //  -        . // landing page app.get('/' , routes.index); // GET      index, .. homepage   <ng-view> app.get('/partials/:name' , routes.partials); // ,      Partials'  Angular'a app.get('/partials/:folder/:name' , routes.partials); //  ,      app.all(/^(\/((?!static)\w.+))+$/ , routes.index); //    ,  ",       /static/      Angular'a". 


A logical question appears - How can we tell the user about a 404 error, if the requested page (for example / ololo) is not actually provided for us?

Frontend


Before I explain how Angular can understand where you made a mistake when going to the page, let's look at how the file structure of the application looks (and all the statics on the server, not including third-party libraries from bower)

Hidden text
Public──public
β”‚ β”œβ”€β”€ css
β”‚ β”‚ └── style.css
β”‚ β”œβ”€β”€ images
β”‚ β”‚ └── fb_button.png
β”‚ β”œβ”€β”€ js
β”‚ β”‚ β”œβ”€ application.js - application initialization. More later ...
β”‚ β”‚ β”œβ”€β”€ controllers.js - global module for controllers
Dire β”‚ β”œβ”€ directives.js - global module for directives
β”‚ β”‚ β”œβ”€ filters.js - for filters
β”‚ β”‚ β”œβ”€β”€ services.js - for services
β”‚ β”‚ └── modules - application entities are stored in this folder
β”‚ β”‚ └── friends.js - this is one of them - the page / friends and controllers, directives, and filters relating only to this entity are described here.

In general, the modules folder could have been called differently - for example, pages ...
those. in a nutshell - there are 2 areas of modules:
- global - describes directives, filters, services that can be used throughout the project
- essential - describes the routing for the entity, and also - directives, filters, services, which uniquely relate only to this entity and cannot be used outside of this module.

Well, that’s all, of course, in theory ... Everyone does as he wishes, but this approach helped me not to drown in noodles from endless small directives and sudden controllers in the middle of patterns. For examples, probably, you will need a separate article. Let us return to our topic.

layout.ejs
 <html ng-app="angularfb"> <head> <title>Facebook app</title> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <link rel="stylesheet" href="/static/css/style.css"> <link rel="stylesheet" href="/static/bootstrap/dist/css/bootstrap.css"> <link rel="stylesheet" href="/static/bootstrap/dist/css/bootstrap-theme.css"> <link rel="stylesheet" href="/static/font-awesome/css/font-awesome.min.css"> <!--[if IE 7]> <link rel="stylesheet" href="/static/font-awesome/css/font-awesome-ie7.min.css"> <![endif]--> <link href='http://fonts.googleapis.com/css?family=Domine' rel='stylesheet' type='text/css'> <style type="text/css"> [ng\:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { display: none !important; } </style> <script type="text/javascript"> //           ,  Facebook API FB_APP_NAME = '<%= appName %>'; FB_APP_ID = '<%= appId %>' ; FB_APP_URL = '<%= appUrl %>'; FB_SCOPE = '<%= scope %>'; </script> </head> <body ng-class="{'page-loader': !$root.user}"> <!--     FB user'a,      --> <!-- body   <ng-view>  Angular     URL . --> <div ng-if="$root.user"> <%- include navigation %> <%- body %> <%- include footer %> </div> <!--       FB user'a --> <div ng-if="!$root.user" ng-cloak> <h2 class="text-center" ng-if="!auth.status"> <i class="icon-spin icon-spinner"></i> Loading... </h2> <!--   login with Facebook      auth.authResponseChange --> <a ng-if="auth.status && auth.status != 'connected'" class="fb-btn" ng-click="login()"></a> </div> </body> <script src="//connect.facebook.net/en_US/all.js"></script> <script src="/static/jquery/jquery.min.js"></script> <script src="/static/bootstrap/dist/js/bootstrap.min.js"></script> <script src="/static/underscore/underscore-min.js"></script> <script src="/static/angular/angular.js"></script> <script src="/static/angular-resource/angular-resource.min.js"></script> <script src="/static/angular-route/angular-route.min.js"></script> <script src="/static/js/services.js?<%= random %>"></script> <script src="/static/js/filters.js?<%= random %>"></script> <script src="/static/js/controllers.js?<%= random %>"></script> <script src="/static/js/directives.js?<%= random %>"></script> <script src="/static/js/modules/friends.js?<%= random %>"></script> <script src="/static/js/application.js?<%= random %>"></script> </html> 


application.js
 (function() { 'use strict'; angular.module('angularfb', [ 'ngRoute', 'ngResource', 'angularfb.filters', 'angularfb.controllers', 'angularfb.services', 'angularfb.directives', 'angularfb.friends' ]) .config([ '$routeProvider', '$locationProvider', function($routeProvider, $locationProvider){ $locationProvider.html5Mode(true); $routeProvider .when('/', { templateUrl: '/partials/homepage', controller: 'HomePageCtrl' }) .when('/logout', { resolve: [ '$rootScope', 'API', '$location', function($rootScope, API, $location){ API.logout(function(){ console.log('Logout... redirecting...'); $location.path('/') $rootScope.$apply(); }); }] }) .otherwise(); } ]) .run(['$rootScope', '$location', 'API', function($rootScope, $location, API){ FB.init({ appId : FB_APP_ID, channelUrl : FB_APP_URL, status : true, xfbml : true, oauth : true }); console.log('RUN!'); //    Login With Facebook $rootScope.login = function(){ API.login(function(){ console.log("Logged in. Redirecting...") $location.path('/'); }, function(){ console.log("not logged in... error...") }); } //     API.getLoginStatus( function( response ){ console.info("Authorized:", response) }, function( response ){ console.error("Not authorized:", response) } ) FB.Event.subscribe('auth.authResponseChange', function(response) { console.log('got AuthResponseChange:', response); if (response.status === 'connected') { API.me().then( function(resp){ console.log('got user Info', resp); }, function(error){ console.log("got user Info error", error); } ); } else { $rootScope.user = null; //      - ng-if  (. layout.ejs) $location.path('/'); } $rootScope.auth = response; $rootScope.$$phase || $rootScope.$apply(); }); $rootScope.$on('$routeChangeStart', function(event, next, current){ if ( !next.$$route ) next.templateUrl = '/partials/error'; }) }]) .controller('HomePageCtrl', [ '$scope', function($scope){ /* .... */ }]) }()); 


services.js
 (function(){ 'use strict'; angular.module('angularfb.services', []) .factory("API", [ '$rootScope', '$q', '$location', '$exceptionHandler', function($rootScope, $q, $location, $exceptionHandler){ return { me: function(){ var def = $q.defer(); FB.api('/me', function(response){ def.resolve($rootScope.user = response); //       $root,     layout.ejs "  " }) return def.promise; }, getLoginStatus: function(successCallback, errorCallback){ var self = this; FB.getLoginStatus(function(response) { self._processAuthResponse( response, successCallback, errorCallback ) }); }, login: function(successCallback, errorCallback){ var self = this; FB.login(function(response){ self._processAuthResponse( response, successCallback, errorCallback ); }, { scope: FB_SCOPE } ) }, logout: function( logoutCallback ){ return FB.logout(function( response ){ if (_.isFunction( logoutCallback )) logoutCallback.call(this, arguments) else { $location.path('/') $rootScope.user = null; } $rootScope.auth = response; }) }, _processAuthResponse: function( response, successCb, errorCb ) { var self = this; if (response.authResponse) { if (_.isFunction(successCb)) successCb.call(this, response) else if(_.isUndefined(successCb)){ $location.path('/') } else throw new Error("Success callback should be a function") } else { if (_.isFunction(errorCb)) errorCb.call(this, response) else if(_.isUndefined(errorCb)){} else throw new Error("Error callback should be a function") } $rootScope.auth = response; self._applyScope(); }, _applyScope: function( cb ) { if (!$rootScope.$$phase) { try { $rootScope.$eval( cb ); } catch (e) { $exceptionHandler(e); } finally { try { $rootScope.$apply(); } catch (e) { $exceptionHandler(e); throw e; } } } else { $rootScope.$eval( cb ); } } } } ]) })(); 



I assume that they will scold for such bikes in the API service, as with successCallback, errorCallback and _processAuthResponse, if only because it could have been a lot easier. I wanted to do some motoring, pile up a little ... For somehow, everything seems to work out well in the work of the authorization itself.

Facebook Login


What about Facebook? Mentioned in the title, several times in the text, and no explanation ... I'm sorry, to blame.

So how do we always keep our app authorization state up to date?
Let's sort the code in parts:

 //   , application.js,   .run(), //   Login Status API.getLoginStatus() //    event "auth.authResponseChange",  //         . // response    {authResponse: Object, status: String } // ,   ,  response.status === 'connected' //  API    $rootScope.user  '/me' , //      response  $rootScope.auth //           //  ,   . FB.Event.subscribe('auth.authResponseChange', function(response) { if (response.status === 'connected'){ API.me() } else { $rootScope.user = null; $location.path('/'); } $rootScope.auth = response; $rootScope.$$phase || $rootScope.$apply(); }); 


As long as we do not have $ root.user, the application hides the main content and instead shows 'Loading ..'. And if $ root.auth has already arrived, and the status! == 'connected', then we will show the β€œConnect with Facebook” button. If the user presses the button and logs in, we get the user, the template in layout.ejs will instantly react and show the main content with the header, footer and <ng-view>, respectively, hide the login button and loading.

Oh yeah .., about the processing Not Found:

Can anyone notice that in the application.js, when describing $ routeProvider, there are no parameters in the .otherwise () method for redirecting. All for a reason. We don’t need to redirect anywhere (on / 404 for example), because the error could have been caused by a typo or something similar, which the user can quickly correct and the page will appear as needed, so we don’t throw anywhere, but before we clicked on the link , or loaded the page, check if any route is described for such a path (.where ())? If not, then load it, and substitute the template for it.

 $rootScope.$on('$routeChangeStart', function(event, next, current){ if ( !next.$$route ) next.templateUrl = '/partials/error'; }) 


Thus, we still have the wrong link in the address bar; information that this path does not exist; the user saw, and the URL is already easily fixed and you can continue to work with the application.

Generally


- We looked at how Express gives Angular to breathe easily, giving everything to personal reasoning, except for static files.
- Touched the topic of ordering files for Anguar.js applications
- We made sure (I hope) that Angular can be trusted not only with 404 error handling, but even communicate with the user after authorization, again on the client, through Facebook.

And you can feel everything live here:
- [http://angular-fb.herokuapp.com/]

If someone was interested, I can leave a link to github with this project.
Thanks to everyone who read.

UPD: added about Facebook Login. Thank you Tulov_Alex

Source: https://habr.com/ru/post/204170/


All Articles