Creazione di applicazioni Web moderne con AngularJS e Play Framework
Pubblicato: 2022-03-11Scegliere lo strumento giusto per lo scopo giusto fa molta strada, soprattutto quando si tratta di creare moderne applicazioni web. Molti di noi hanno familiarità con AngularJS e quanto sia facile sviluppare front-end di applicazioni Web robusti. Sebbene molti si oppongano all'uso di questo popolare framework web, ha sicuramente molto da offrire e può essere una scelta appropriata per un'ampia gamma di esigenze. D'altra parte, i componenti che utilizzi nel back-end determineranno molto sulle prestazioni dell'applicazione Web, poiché influiscono sull'esperienza utente complessiva. Play è un framework web ad alta velocità per Java e Scala. Si basa su un'architettura leggera, stateless e web-friendly e segue modelli e principi MVC simili a Rails e Django.
In questo articolo, daremo un'occhiata a come possiamo utilizzare AngularJS e Play per creare una semplice applicazione blog con un meccanismo di autenticazione di base e la possibilità di inserire post e commenti. Lo sviluppo di AngularJS, con alcune chicche di Twitter Bootstrap, ci consentirà di potenziare un'esperienza applicativa a pagina singola su un back-end API REST basato su Play.
Applicazioni Web - Per iniziare
Scheletro dell'applicazione AngularJS
Le app AngularJS e Play risiederanno di conseguenza nelle directory client e server. Per ora creeremo la directory "client".
mkdir -p blogapp/client
Per creare uno scheletro di applicazione AngularJS, utilizzeremo Yeoman, uno straordinario strumento di scaffolding. Installare Yeoman è facile. Usarlo per impalcare una semplice applicazione scheletrica AngularJS è probabilmente ancora più semplice:
cd blogapp/client yo angular
L'esecuzione del secondo comando sarà seguita da alcune opzioni tra cui è necessario scegliere. Per questo progetto non abbiamo bisogno di “Sass (con Compass)”. Avremo bisogno di Boostrap insieme ai seguenti plugin AngularJS:
- angular-animate.js
- angular-cookies.js
- risorsa-angolare.js
- angular-route.js
- angular-sanitize.js
- angular-touch.js
A questo punto, una volta finalizzate le selezioni, inizierai a vedere l'output di NPM e Bower sul tuo terminale. Quando i download sono completi e i pacchetti sono stati installati, avrai uno scheletro dell'applicazione AngularJS pronto per essere utilizzato.
Scheletro dell'applicazione Play Framework
Il modo ufficiale per creare una nuova applicazione Play prevede l'uso dello strumento Typesafe Activator. Prima di poterlo utilizzare, devi scaricarlo e installarlo sul tuo computer. Se sei su Mac OS e usi Homebrew, puoi installare questo strumento con una singola riga di comando:
brew install typesafe-activator
Creare un'applicazione Play dalla riga di comando è semplicissimo:
cd blogapp/ activator new server play-java cd server/
Importazione in un IDE
Per importare l'applicazione in un IDE come Eclipse o IntelliJ, è necessario "eclissare" o "idealizzare" l'applicazione. Per farlo, esegui il seguente comando:
activator
Una volta visualizzato un nuovo prompt, digita "eclipse" o "idea" e premi invio per preparare il codice dell'applicazione rispettivamente per Eclipse o IntelliJ.
Per brevità, in questo articolo tratteremo solo il processo di importazione del progetto in IntelliJ. Il processo di importazione in Eclipse dovrebbe essere altrettanto semplice. Per importare il progetto in IntelliJ, inizia attivando l'opzione "Progetto da fonti esistenti..." che si trova in "File -> Nuovo". Quindi, seleziona il tuo file build.sbt e fai clic su "OK". Facendo nuovamente clic su "OK" nella finestra di dialogo successiva, IntelliJ dovrebbe iniziare a importare l'applicazione Play come progetto SBT.
Typesafe Activator viene fornito con un'interfaccia utente grafica, che puoi utilizzare per creare questo codice applicativo scheletrico.
Ora che abbiamo importato la nostra applicazione Play in IntelliJ, dovremmo anche importare la nostra applicazione AngularJS nell'area di lavoro. Possiamo importarlo come progetto separato o come modulo nel progetto esistente in cui risiede l'applicazione Play.
Qui importeremo l'applicazione Angular come modulo. Nel menu "File", selezioneremo l'opzione "Nuovo -> Modulo da fonti esistenti...". Dalla finestra di dialogo, sceglieremo la directory "client" e faremo clic su "OK". Nelle due schermate successive, fare clic rispettivamente su "Avanti" e "Fine".
Generazione di server locali
A questo punto, dovrebbe essere possibile avviare l'applicazione AngularJS come attività Grunt dall'IDE. Espandi la cartella del client e fai clic con il pulsante destro del mouse su Gruntfile.js. Nel menu a comparsa, seleziona "Mostra attività Grunt". Apparirà un pannello con l'etichetta "Grunt" con un elenco di attività:
Per iniziare a servire l'applicazione, fai doppio clic su "servisci". Questo dovrebbe aprire immediatamente il tuo browser web predefinito e puntarlo a un indirizzo localhost. Dovresti vedere una pagina stub di AngularJS con il logo di Yeoman su di essa.
Successivamente, dobbiamo avviare il nostro server di applicazioni back-end. Prima di poter procedere, dobbiamo affrontare un paio di questioni:
- Per impostazione predefinita, sia l'applicazione AngularJS (avviata da Yeoman) che l'applicazione Play tentano di essere eseguite sulla porta 9000.
- In produzione, entrambe le applicazioni verranno probabilmente eseguite in un dominio e probabilmente utilizzeremo Nginx per instradare le richieste di conseguenza. Ma in modalità di sviluppo, quando cambiamo il numero di porta di una di queste applicazioni, i browser web le tratteranno come se fossero in esecuzione su domini diversi.
Per aggirare entrambi questi problemi, tutto ciò che dobbiamo fare è utilizzare un proxy Grunt in modo che tutte le richieste AJAX all'applicazione Play siano inviate tramite proxy. In sostanza, entrambi questi server delle applicazioni saranno disponibili con lo stesso numero di porta apparente.
Per prima cosa cambiamo il numero di porta del server delle applicazioni Play in 9090. Per fare ciò, apri la finestra "Esegui/Debug Configurazioni" facendo clic su "Esegui -> Modifica configurazioni". Quindi, cambia il numero di porta nel campo "Url To Open". Fare clic su "OK" per approvare questa modifica e chiudere la finestra. Facendo clic sul pulsante "Esegui" dovrebbe avviare il processo di risoluzione delle dipendenze: i registri di questo processo inizieranno ad apparire.
Una volta terminato, puoi accedere a http://localhost:9090 sul tuo browser web e in pochi secondi dovresti essere in grado di vedere la tua applicazione Play. Per configurare un proxy Grunt, dobbiamo prima installare un piccolo pacchetto Node.js usando NPM:
cd blogapp/client npm install grunt-connect-proxy --save-dev
Quindi dobbiamo modificare il nostro Gruntfile.js. In quel file, individua l'attività "connetti" e inserisci la chiave/valore "proxy" dopo di essa:
proxies: [ { context: '/app', // the context of the data service host: 'localhost', // wherever the data service is running port: 9090, // the port that the data service is running on changeOrigin: true } ],
Grunt ora trasmetterà tutte le richieste a "/app/*" all'applicazione Play back-end. Questo ci eviterà di dover inserire nella whitelist ogni chiamata al back-end. Inoltre, dobbiamo anche modificare il nostro comportamento di caricamento in tempo reale:
livereload: { options: { open: true, middleware: function (connect) { var middlewares = []; // Setup the proxy middlewares.push(require('grunt-connect-proxy/lib/utils').proxyRequest); // Serve static files middlewares.push(connect.static('.tmp')); middlewares.push(connect().use( '/bower_components', connect.static('./bower_components') )); middlewares.push(connect().use( '/app/styles', connect.static('./app/styles') )); middlewares.push(connect.static(appConfig.app)); return middlewares; } } },
Infine, dobbiamo aggiungere una nuova dipendenza "'configureProxies:server" all'attività "serve":
grunt.registerTask('serve', 'Compile then start a connect web server', function (target) { if (target === 'dist') { return grunt.task.run(['build', 'connect:dist:keepalive']); } grunt.task.run([ 'clean:server', 'wiredep', 'concurrent:server', 'autoprefixer:server', 'configureProxies:server', 'connect:livereload', 'watch' ]); });
Al riavvio di Grunt, dovresti notare le seguenti righe nei registri che indicano che il proxy è in esecuzione:
Running "autoprefixer:server" (autoprefixer) task File .tmp/styles/main.css created. Running "configureProxies:server" (configureProxies) task Running "connect:livereload" (connect) task Started connect web server on http://localhost:9000
Creazione di un modulo di iscrizione
Inizieremo creando un modulo di iscrizione per la nostra applicazione blog. Questo ci permetterà anche di verificare che tutto funzioni come dovrebbe. Possiamo usare Yeoman per creare un controller di registrazione e visualizzare in AngularJS:
yo angular:controller signup yo angular:view signup
Successivamente dovremmo aggiornare il routing della nostra applicazione per fare riferimento a questa vista appena creata e rimuovere il controller e la vista "informazioni" generati automaticamente ridondanti. Dall'interno del file "app/scripts/app.js", rimuovi i riferimenti a "app/scripts/controllers/about.js" e "app/views/about.html", lasciandolo con:
.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });
Allo stesso modo, aggiorna il file "app/index.html" per rimuovere i collegamenti ridondanti e aggiungi un collegamento alla pagina di registrazione:
<div class="collapse navbar-collapse"> <ul class="nav navbar-nav"> <li class="active"><a href="#/">Home</a></li> <li><a ng-href="#/signup">Signup</a></li> </ul> </div> </div>
Inoltre, rimuovi il tag script per "about.js":
<!-- build:js({.tmp,app}) scripts/scripts.js --> <script src="scripts/app.js"></script> <script src="scripts/controllers/main.js"></script> <script src="scripts/controllers/signup.js"></script> <!-- endbuild --> </body> </html>
Quindi, aggiungi un modulo al nostro file "signup.html":
<form name="signupForm" ng-submit="signup()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Sign up!</button> </form>
Dobbiamo fare in modo che il modulo venga elaborato dal controller Angular. Vale la pena notare che non è necessario aggiungere specificamente l'attributo "ng-controller" nelle nostre viste, poiché la nostra logica di routing in "app.js" attiva automaticamente un controller prima che la nostra vista venga caricata. Tutto quello che dobbiamo fare per collegare questo modulo è avere una corretta funzione di "iscrizione" definita in $ scope. Questo dovrebbe essere fatto nel file "signup.js":
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .success(function(data) { $log.debug(data); }); }; });
Ora apriamo la Console per gli sviluppatori di Chrome, passiamo alla scheda "Rete" e proviamo a inviare il modulo di registrazione.
Vedremo che il back-end di Play risponde naturalmente con una pagina di errore "Azione non trovata". Questo è previsto in quanto non è stato ancora implementato. Ma ciò significa anche che la nostra configurazione del proxy Grunt funziona correttamente!
Successivamente, aggiungeremo una "Azione" che è essenzialmente un metodo nel controller dell'applicazione Play. Nella classe "Applicazione" nel pacchetto "app/controller", aggiungi un nuovo metodo "registrazione":
public static Result signup() { return ok("Success!"); }
Ora apri il file "conf/routes" e aggiungi la seguente riga:
POST /app/signup controllers.Application.signup
Infine, torniamo al nostro browser web, http://localhost:9000/#/signup. Facendo clic sul pulsante "Invia" questa volta dovrebbe produrre qualcosa di diverso:
Dovresti vedere il valore hardcoded restituito, quello che abbiamo scritto nel metodo di registrazione. In tal caso, siamo pronti per andare avanti poiché il nostro ambiente di sviluppo è pronto e funziona sia per le applicazioni Angular che Play.
Definire i modelli Ebean in Play
Prima di definire i modelli, scegliamo prima un datastore. In questo articolo utilizzeremo il database in memoria H2. Per abilitarlo, trova e decommenta le seguenti righe nel file "application.conf":
db.default.driver=org.h2.Driver db.default.url="jdbc:h2:mem:play" db.default.user=sa db.default.password="" ... ebean.default="models.*"
E aggiungi la seguente riga:
applyEvolutions.default=true
Il nostro modello di dominio del blog è piuttosto semplice. Prima di tutto, abbiamo utenti che possono creare post e quindi ogni post può essere commentato da qualsiasi utente registrato. Creiamo i nostri modelli Ebean.
Utente
// User.java @Entity public class User extends Model { @Id public Long id; @Column(length = 255, unique = true, nullable = false) @Constraints.MaxLength(255) @Constraints.Required @Constraints.Email public String email; @Column(length = 64, nullable = false) private byte[] shaPassword; @OneToMany(cascade = CascadeType.ALL) @JsonIgnore public List<BlogPost> posts; public void setPassword(String password) { this.shaPassword = getSha512(password); } public void setEmail(String email) { this.email = email.toLowerCase(); } public static final Finder<Long, User> find = new Finder<Long, User>( Long.class, User.class); public static User findByEmailAndPassword(String email, String password) { return find .where() .eq("email", email.toLowerCase()) .eq("shaPassword", getSha512(password)) .findUnique(); } public static User findByEmail(String email) { return find .where() .eq("email", email.toLowerCase()) .findUnique(); } public static byte[] getSha512(String value) { try { return MessageDigest.getInstance("SHA-512").digest(value.getBytes("UTF-8")); } catch (NoSuchAlgorithmException e) { throw new RuntimeException(e); } catch (UnsupportedEncodingException e) { throw new RuntimeException(e); } } }
Post sul blog
// BlogPost.java @Entity public class BlogPost extends Model { @Id public Long id; @Column(length = 255, nullable = false) @Constraints.MaxLength(255) @Constraints.Required public String subject; @Column(columnDefinition = "TEXT") @Constraints.Required public String content; @ManyToOne public User user; public Long commentCount; @OneToMany(cascade = CascadeType.ALL) public List<PostComment> comments; public static final Finder<Long, BlogPost> find = new Finder<Long, BlogPost>( Long.class, BlogPost.class); public static List<BlogPost> findBlogPostsByUser(final User user) { return find .where() .eq("user", user) .findList(); } public static BlogPost findBlogPostById(final Long id) { return find .where() .eq("id", id) .findUnique(); } }
Posta un commento
// PostComment.java @Entity public class PostComment extends Model { @Id public Long id; @ManyToOne @JsonIgnore public BlogPost blogPost; @ManyToOne public User user; @Column(columnDefinition = "TEXT") public String content; public static final Finder<Long, PostComment> find = new Finder<Long, PostComment>( Long.class, PostComment.class); public static List<PostComment> findAllCommentsByPost(final BlogPost blogPost) { return find .where() .eq("post", blogPost) .findList(); } public static List<PostComment> findAllCommentsByUser(final User user) { return find .where() .eq("user", user) .findList(); } }
Azione di registrazione reale
Ora creiamo la nostra prima vera azione, consentendo agli utenti di registrarsi:
// Application.java public static Result signup() { Form<SignUp> signUpForm = Form.form(SignUp.class).bindFromRequest(); if ( signUpForm.hasErrors()) { return badRequest(signUpForm.errorsAsJson()); } SignUp newUser = signUpForm.get(); User existingUser = User.findByEmail(newUser.email); if(existingUser != null) { return badRequest(buildJsonResponse("error", "User exists")); } else { User user = new User(); user.setEmail(newUser.email); user.setPassword(newUser.password); user.save(); session().clear(); session("username", newUser.email); return ok(buildJsonResponse("success", "User created successfully")); } } public static class UserForm { @Constraints.Required @Constraints.Email public String email; } public static class SignUp extends UserForm { @Constraints.Required @Constraints.MinLength(6) public String password; } private static ObjectNode buildJsonResponse(String type, String message) { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", message); wrapper.put(type, msg); return wrapper; }
Tieni presente che l'autenticazione utilizzata in questa app è molto semplice e non è consigliata per l'uso in produzione.
La parte interessante è che stiamo usando i moduli di Play per gestire i moduli di iscrizione. Abbiamo impostato un paio di vincoli sulla nostra classe di moduli di iscrizione. La convalida verrà eseguita automaticamente per noi senza necessità di una logica di convalida esplicita.
Se torniamo alla nostra applicazione AngularJS nel browser web e facciamo nuovamente clic su "Invia", vedremo che il server ora risponde con un errore appropriato: questi campi sono obbligatori.
Gestione degli errori del server in AngularJS
Quindi stiamo ricevendo un errore dal server, ma l'utente dell'applicazione non ha idea di cosa stia succedendo. Il minimo che possiamo fare è mostrare l'errore al nostro utente. Idealmente, dovremmo capire che tipo di errore stiamo ricevendo e visualizzare un messaggio intuitivo. Creiamo un semplice servizio di avviso che ci aiuterà a visualizzare l'errore.
Innanzitutto, dobbiamo generare un modello di servizio con Yeoman:
yo angular:service alerts
Quindi, aggiungi questo codice a "alerts.js":
angular.module('clientApp') .factory('alertService', function($timeout) { var ALERT_TIMEOUT = 5000; function add(type, msg, timeout) { if (timeout) { $timeout(function(){ closeAlert(this); }, timeout); } else { $timeout(function(){ closeAlert(this); }, ALERT_TIMEOUT); } return alerts.push({ type: type, msg: msg, close: function() { return closeAlert(this); } }); } function closeAlert(alert) { return closeAlertIdx(alerts.indexOf(alert)); } function closeAlertIdx(index) { return alerts.splice(index, 1); } function clear(){ alerts = []; } function get() { return alerts; } var service = { add: add, closeAlert: closeAlert, closeAlertIdx: closeAlertIdx, clear: clear, get: get }, alerts = []; return service; } );
Ora creiamo un controller separato responsabile per gli avvisi:
yo angular:controller alerts
angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });
Ora dobbiamo mostrare effettivamente dei bei messaggi di errore Bootstrap. Il modo più semplice è utilizzare l'interfaccia utente angolare. Possiamo usare Bower per installarlo:
bower install angular-bootstrap --save
Nel tuo "app.js" aggiungi il modulo dell'interfaccia utente angolare:
angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])
Aggiungiamo la direttiva alert al nostro file “index.html”:
<div class="container"> <div ng-controller="AlertsCtrl"> <alert ng-repeat="alert in alerts" type="{{alert.type}}" close="alert.close()">{{ alert.msg }}</alert> </div> <div ng-view=""></div> </div>
Infine, dobbiamo aggiornare il controller di iscrizione:

angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location, userService) { $scope.signup = function() { var payload = { email : $scope.email, password : $scope.password }; $http.post('app/signup', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } if(status === 500) { alertService.add('danger', 'Internal server error!'); } }) }; });
Ora, se inviamo di nuovo il modulo vuoto, vedremo gli errori visualizzati sopra il modulo:
Ora che gli errori sono stati gestiti, dobbiamo fare qualcosa quando la registrazione dell'utente ha esito positivo. Possiamo reindirizzare l'utente a una pagina dashboard in cui può aggiungere post. Ma prima dobbiamo crearlo:
yo angular:view dashboard yo angular:controller dashboard
Modifica il metodo di registrazione del controller "signup.js" in modo che in caso di successo reindirizzi l'utente:
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });
Aggiungi un nuovo percorso in "apps.js":
.when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })
Dobbiamo anche tenere traccia se l'utente ha effettuato l'accesso. Creiamo un servizio separato per questo:
yo angular:service user
// user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });
E modifica anche il controller di registrazione per impostare l'utente su uno che si è appena registrato:
.success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });
Prima di aggiungere la funzionalità principale dell'aggiunta di post, prendiamoci cura di alcune altre importanti funzionalità come la possibilità di accedere e disconnettersi, visualizzare le informazioni sull'utente sulla dashboard e anche aggiungere il supporto per l'autenticazione nel back-end.
Autenticazione di base
Passiamo alla nostra applicazione Play e implementiamo le azioni di accesso e disconnessione. Aggiungi queste righe a "Application.java":
public static Result login() { Form<Login> loginForm = Form.form(Login.class).bindFromRequest(); if (loginForm.hasErrors()) { return badRequest(loginForm.errorsAsJson()); } Login loggingInUser = loginForm.get(); User user = User.findByEmailAndPassword(loggingInUser.email, loggingInUser.password); if(user == null) { return badRequest(buildJsonResponse("error", "Incorrect email or password")); } else { session().clear(); session("username", loggingInUser.email); ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "Logged in successfully"); msg.put("user", loggingInUser.email); wrapper.put("success", msg); return ok(wrapper); } } public static Result logout() { session().clear(); return ok(buildJsonResponse("success", "Logged out successfully")); } public static Result isAuthenticated() { if(session().get("username") == null) { return unauthorized(); } else { ObjectNode wrapper = Json.newObject(); ObjectNode msg = Json.newObject(); msg.put("message", "User is logged in already"); msg.put("user", session().get("username")); wrapper.put("success", msg); return ok(wrapper); } } public static class Login extends UserForm { @Constraints.Required public String password; }
Quindi aggiungiamo la possibilità di consentire particolari chiamate di back-end solo agli utenti autenticati. Crea "Secured.java" con il seguente codice:
public class Secured extends Security.Authenticator { @Override public String getUsername(Context ctx) { return ctx.session().get("username"); } @Override public Result onUnauthorized(Context ctx) { return unauthorized(); } }
Useremo questa classe in seguito per proteggere le nuove azioni. Successivamente, dovremmo modificare il menu principale dell'applicazione AngularJS in modo che visualizzi il nome utente e i collegamenti di logout. Per questo, dobbiamo creare un controller:
yo angular:controller menu
// menu.js angular.module('clientApp') .controller('MenuCtrl', function ($scope, $http, userService, $location) { $scope.user = userService; $scope.logout = function() { $http.get('/app/logout') .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = ''; $location.path('/login'); } }); }; $scope.$watch('user.username', function (newVal) { if(newVal === '') { $scope.isLoggedIn = false; } else { $scope.username = newVal; $scope.isLoggedIn = true; } }); });
Abbiamo anche bisogno di una vista e di un controller per la pagina di accesso:
yo angular:controller login yo angular:view login
<!-- login.html --> <form name="loginForm" ng-submit="login()" novalidate> <div> <label for="email">Email</label> <input name="email" class="form-control" type="email" placeholder="Email" ng-model="email"> </div> <div> <label for="password">Password</label> <input name="password" class="form-control" type="password" placeholder="Password" ng-model="password"> </div> <button type="submit" class="btn btn-primary">Log in</button> </form>
// login.js angular.module('clientApp') .controller('LoginCtrl', function ($scope, userService, $location, $log, $http, alertService) { $scope.isAuthenticated = function() { if(userService.username) { $log.debug(userService.username); $location.path('/dashboard'); } else { $http.get('/app/isauthenticated') .error(function() { $location.path('/login'); }) .success(function(data) { if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); } }; $scope.isAuthenticated(); $scope.login = function() { var payload = { email : this.email, password : this.password }; $http.post('/app/login', payload) .error(function(data, status){ if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'email' || key === 'password') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { alertService.add('danger', 'Invalid login or password!'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data){ $log.debug(data); if(data.hasOwnProperty('success')) { userService.username = data.success.user; $location.path('/dashboard'); } }); }; });
Quindi modifichiamo il menu in modo che possa visualizzare i dati dell'utente:
<!-- index.html --> <div class="collapse navbar-collapse" ng-controller="MenuCtrl"> <ul class="nav navbar-nav pull-right" ng-hide="isLoggedIn"> <li><a ng-href="/#/signup">Sign up!</a></li> <li><a ng-href="/#/login">Login</a></li> </ul> <div class="btn-group pull-right acc-button" ng-show="isLoggedIn"> <button type="button" class="btn btn-default">{{ username }}</button> <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown" aria-expanded="false"> <span class="caret"></span> <span class="sr-only">Toggle Dropdown</span> </button> <ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul> </div> </div>
Ora se accedi all'applicazione, dovresti essere in grado di vedere la seguente schermata:
Aggiunta di post
Ora che disponiamo di meccanismi di registrazione e autenticazione di base, possiamo passare all'implementazione della funzionalità di pubblicazione. Aggiungiamo una nuova vista e controller per l'aggiunta di post.
yo angular:view addpost
<!-- addpost.html --> <form name="postForm" ng-submit="post()" novalidate> <div> <label for="subject">Subject</label> <input name="subject" class="form-control" type="subject" placeholder="Subject" ng-model="subject"> </div> <div> <label for="content">Post</label> <textarea name="content" class="form-control" placeholder="Content" ng-model="content"></textarea> </div> <button type="submit" class="btn btn-primary">Submit post</button> </form>
yo angular:controller addpost
// addpost.js angular.module('clientApp') .controller('AddpostCtrl', function ($scope, $http, alertService, $location) { $scope.post = function() { var payload = { subject : $scope.subject, content: $scope.content }; $http.post('/app/post', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'subject' || key === 'content') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { $scope.subject = ''; $scope.content = ''; alertService.add('success', data.success.message); }); }; });
Quindi aggiorniamo "app.js" per includere:
.when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })
Successivamente, modifichiamo "index.html" per aggiungere un collegamento per la nostra vista "addpost" nel menu del dashboard:
<ul class="dropdown-menu" role="menu"> <li><a ng-href="/#/dashboard">Dashboard</a></li> <li><a ng-href="/#/addpost">Add post</a></li> <li class="divider"></li> <li><a href="#" ng-click="logout()">Logout</a></li> </ul>
Ora dal lato dell'applicazione Play, creiamo un nuovo controller Post con il metodo addPost:
// Post.java public class Post extends Controller { public static Result addPost() { Form<PostForm> postForm = Form.form(PostForm.class).bindFromRequest(); if (postForm.hasErrors()) { return badRequest(postForm.errorsAsJson()); } else { BlogPost newBlogPost = new BlogPost(); newBlogPost.commentCount = 0L; newBlogPost.subject = postForm.get().subject; newBlogPost.content = postForm.get().content; newBlogPost.user = getUser(); newBlogPost.save(); } return ok(Application.buildJsonResponse("success", "Post added successfully")); } private static User getUser() { return User.findByEmail(session().get("username")); } public static class PostForm { @Constraints.Required @Constraints.MaxLength(255) public String subject; @Constraints.Required public String content; } }
Aggiungi una nuova voce al file dei percorsi per poter gestire i metodi appena aggiunti nel percorso:
POST /app/post controllers.Post.addPost
A questo punto dovresti essere in grado di aggiungere nuovi post.
Visualizzazione dei post
L'aggiunta di post ha poco valore, se non possiamo visualizzarli. Quello che vogliamo fare è elencare tutti i post nella pagina principale. Iniziamo aggiungendo un nuovo metodo nel nostro controller dell'applicazione:
// Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }
E registrandolo nel nostro file di percorsi:
GET /app/posts controllers.Application.getPosts
Successivamente, nella nostra applicazione AngularJS modifichiamo il nostro controller principale:
// main.js angular.module('clientApp') .controller('MainCtrl', function ($scope, $http) { $scope.getPosts = function() { $http.get('app/posts') .success(function(data) { $scope.posts = data; }); }; $scope.getPosts(); });
Infine, rimuovi tutto da "main.html" e aggiungi questo:
<div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | <a ng-href="/#/viewpost/{{ post.id }}">Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>
Ora se carichi la home page della tua applicazione dovresti vedere qualcosa di simile a questo:
Probabilmente dovremmo anche avere una vista separata per i singoli post.
yo angular:controller viewpost yo angular:view viewpost
// viewpost.js angular.module('clientApp') .controller('ViewpostCtrl', function ($scope, $http, alertService, userService, $location) { $scope.user = userService; $scope.params = $routeParams; $scope.postId = $scope.params.postId; $scope.viewPost = function() { $http.get('/app/post/' + $scope.postId) .error(function(data) { alertService.add('danger', data.error.message); }) .success(function(data) { $scope.post = data; }); }; $scope.viewPost(); });
<!-- viewpost.html --> <div class="panel panel-default" ng-show="post"> <div class="panel-body"> <h4>{{ post.subject }}</h4> <p> {{ post.content }} </p> </div> <div class="panel-footer">Post by: {{ post.user.email }} | Comments <span class="badge">{{ post.commentCount }}</span></a></div> </div>
E la rotta AngularJS:
app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })
Come prima, aggiungiamo un nuovo metodo al nostro controller dell'applicazione:
// Application.java public static Result getPost(Long id) { BlogPost blogPost = BlogPost.findBlogPostById(id); if(blogPost == null) { return notFound(buildJsonResponse("error", "Post not found")); } return ok(Json.toJson(blogPost)); }
… E un nuovo percorso:
GET /app/post/:id controllers.Application.getPost(id: Long)
Ora, se vai a http://localhost:9000/#/viewpost/1, sarai in grado di caricare una vista per un particolare post. Successivamente, aggiungiamo la possibilità di vedere i post degli utenti nella dashboard:
// dashboard.js angular.module('clientApp') .controller('DashboardCtrl', function ($scope, $log, $http, alertService, $location) { $scope.loadPosts = function() { $http.get('/app/userposts') .error(function(data, status) { if(status === 401) { $location.path('/login'); } else { alertService.add('danger', data.error.message); } }) .success(function(data) { $scope.posts = data; }); }; $scope.loadPosts(); });
<!-- dashboard.html --> <h4>My Posts</h4> <div ng-hide="posts.length">No posts yet. <a ng-href="/#/addpost">Add a post</a></div> <div class="panel panel-default" ng-repeat="post in posts"> <div class="panel-body"> <a ng-href="/#/viewpost/{{ post.id }}">{{ post.subject }}</a> | Comments <span class="badge">{{ post.commentCount }}</span> </div> </div>
Aggiungi anche un nuovo metodo a Post controller, seguito da un percorso corrispondente a questo metodo:
// Post.java public static Result getUserPosts() { User user = getUser(); if(user == null) { return badRequest(Application.buildJsonResponse("error", "No such user")); } return ok(Json.toJson(BlogPost.findBlogPostsByUser(user))); }
GET /app/userposts controllers.Post.getUserPosts
Ora, quando crei post, questi verranno elencati nella dashboard:
Funzionalità di commento
Per implementare la funzionalità di commento, inizieremo aggiungendo un nuovo metodo in Post controller:
// Post.java public static Result addComment() { Form<CommentForm> commentForm = Form.form(CommentForm.class).bindFromRequest(); if (commentForm.hasErrors()) { return badRequest(commentForm.errorsAsJson()); } else { PostComment newComment = new PostComment(); BlogPost blogPost = BlogPost.findBlogPostById(commentForm.get().postId); blogPost.commentCount++; blogPost.save(); newComment.blogPost = blogPost; newComment.user = getUser(); newComment.content = commentForm.get().comment; newComment.save(); return ok(Application.buildJsonResponse("success", "Comment added successfully")); } } public static class CommentForm { @Constraints.Required public Long postId; @Constraints.Required public String comment; }
And as always, we need to register a new route for this method:
POST /app/comment controllers.Post.addComment
In our AngularJS application, we add the following to “viewpost.js”:
$scope.addComment = function() { var payload = { postId: $scope.postId, comment: $scope.comment }; $http.post('/app/comment', payload) .error(function(data, status) { if(status === 400) { angular.forEach(data, function(value, key) { if(key === 'comment') { alertService.add('danger', key + ' : ' + value); } else { alertService.add('danger', value.message); } }); } else if(status === 401) { $location.path('/login'); } else if(status === 500) { alertService.add('danger', 'Internal server error!'); } else { alertService.add('danger', data); } }) .success(function(data) { alertService.add('success', data.success.message); $scope.comment = ''; $scope.viewPost(); }); };
And finally add the following lines to “viewpost.html”:
<div class="well" ng-repeat="comment in post.comments"> <span class="label label-default">By: {{ comment.user.email }}</span> <br/> {{ comment.content }} </div> <div ng-hide="user.username || !post"><h4><a ng-href="/#/login">Login</a> to comment</h4></div> <form name="addCommentForm" ng-submit="addComment()" novalidate ng-show="user.username"> <div><h4>Add comment</h4></div> <div> <label for="comment">Comment</label> <textarea name="comment" class="form-control" placeholder="Comment" ng-model="comment"></textarea> </div> <button type="submit" class="btn btn-primary">Add comment</button> </form>
Now if you open any post, you will be able to add and view comments.
What's Next?
In this tutorial, we have built an AngularJS blog with a Play application serving as a REST API back-end. Although the application lacks robust data validation (especially on the client side) and security, these topics were out of the scope of this tutorial. It was aiming to demonstrate one of the many possible ways of building an application of this kind. For convenience, the source code of this application has been uploaded to a GitHub repository.
If you find this combination of AngularJS and Play in web application development interesting, I highly recommend you review the following topics further:
- AngularJS documentation
- Play documentation
- Recommended security approach in one page JS app with Play as back-end (contains example)
- Secure REST API without OAuth
- Ready Play authentication plug-in (might be not fully usable for single-page JavaScript applications, but can be used as a good example)