Construirea de aplicații web moderne cu AngularJS și Play Framework
Publicat: 2022-03-11Alegerea instrumentului potrivit pentru scopul potrivit este un drum lung, mai ales când vine vorba de construirea de aplicații web moderne. Mulți dintre noi sunt familiarizați cu AngularJS și cât de ușor face dezvoltarea front-end-urilor robuste de aplicații web. Deși mulți vor argumenta împotriva utilizării acestui cadru web popular, cu siguranță are multe de oferit și poate fi o alegere potrivită pentru o gamă largă de nevoi. Pe de altă parte, componentele pe care le utilizați pe back-end vor dicta foarte mult despre performanța aplicației web, deoarece au influență asupra experienței generale a utilizatorului. Play este un cadru web de mare viteză pentru Java și Scala. Se bazează pe o arhitectură ușoară, fără stat, prietenoasă cu web și urmează modele și principii MVC similare cu Rails și Django.
În acest articol, vom arunca o privire asupra modului în care putem folosi AngularJS și Play pentru a construi o aplicație simplă de blog cu un mecanism de autentificare de bază și capacitatea de a face postări și comentarii. Dezvoltarea AngularJS, cu câteva bunătăți Twitter Bootstrap, ne va permite să dezvoltăm o experiență de aplicație pe o singură pagină, pe lângă un back-end API REST bazat pe Play.
Aplicații web - Noțiuni introductive
Scheletul aplicației AngularJS
Aplicațiile AngularJS și Play vor locui în directoarele client și server în consecință. Pentru moment, vom crea directorul „client”.
mkdir -p blogapp/client
Pentru a crea un schelet de aplicație AngularJS, vom folosi Yeoman - un instrument uimitor de schele. Instalarea Yeoman este ușoară. Folosirea acesteia pentru a schelei o aplicație simplă AngularJS este probabil și mai ușoară:
cd blogapp/client yo angular
Rularea celei de-a doua comenzi va fi urmată de câteva opțiuni din care trebuie să alegeți. Pentru acest proiect, nu avem nevoie de „Sass (with Compass)”. Vom avea nevoie de Boostrap împreună cu următoarele plugin-uri AngularJS:
- angular-animate.js
- angular-cookies.js
- angular-resource.js
- angular-route.js
- angular-sanitize.js
- unghiular-touch.js
În acest moment, odată ce finalizați selecțiile, veți începe să vedeți rezultate NPM și Bower pe terminalul dvs. Când descărcările sunt complete și pachetele au fost instalate, veți avea un schelet de aplicație AngularJS gata de utilizare.
Play Framework Application Skeleton
Modul oficial de a crea o nouă aplicație Play implică utilizarea instrumentului Typesafe Activator. Înainte de a-l putea folosi, trebuie să îl descărcați și să îl instalați pe computer. Dacă sunteți pe Mac OS și utilizați Homebrew, puteți instala acest instrument cu o singură linie de comandă:
brew install typesafe-activator
Crearea unei aplicații Play din linia de comandă este foarte simplă:
cd blogapp/ activator new server play-java cd server/
Importul într-un IDE
Pentru a importa aplicația într-un IDE, cum ar fi Eclipse sau IntelliJ, trebuie să „eclipsați” sau să „idealizați” aplicația. Pentru a face asta, executați următoarea comandă:
activator
Odată ce vedeți o nouă solicitare, tastați fie „eclipse”, fie „idee” și apăsați enter pentru a pregăti codul aplicației pentru Eclipse sau, respectiv, IntelliJ.
Pentru concizie, vom acoperi doar procesul de import a proiectului în IntelliJ în acest articol. Procesul de importare a acestuia în Eclipse ar trebui să fie la fel de simplu. Pentru a importa proiectul în IntelliJ, începeți prin a activa opțiunea „Proiect din surse existente…” aflată sub „Fișier -> Nou”. Apoi, selectați fișierul build.sbt și faceți clic pe „OK”. După ce faceți clic din nou pe „OK” în dialogul următor, IntelliJ ar trebui să înceapă să importe aplicația dvs. Play ca proiect SBT.
Typesafe Activator vine, de asemenea, cu o interfață grafică cu utilizatorul, pe care o puteți utiliza pentru a crea acest cod al aplicației scheletice.
Acum că am importat aplicația noastră Play în IntelliJ, ar trebui să importam și aplicația noastră AngularJS în spațiul de lucru. Îl putem importa fie ca proiect separat, fie ca modul în proiectul existent în care se află aplicația Play.
Aici, vom importa aplicația Angular ca modul. În meniul „Fișier”, vom selecta opțiunea „Nou -> Modul din surse existente...”. Din dialog, vom alege directorul „client” și vom face clic pe „OK”. În următoarele două ecrane, faceți clic pe „Următorul” și, respectiv, pe „Terminare”.
Generarea de servere locale
În acest moment, ar trebui să fie posibilă pornirea aplicației AngularJS ca sarcină Grunt din IDE. Extindeți folderul client și faceți clic dreapta pe Gruntfile.js. În meniul pop-up selectați „Afișați sarcini Grunt”. Va apărea un panou etichetat „Grunt” cu o listă de sarcini:
Pentru a începe să difuzați aplicația, faceți dublu clic pe „servire”. Acest lucru ar trebui să deschidă imediat browserul dvs. web implicit și să îl direcționeze către o adresă localhost. Ar trebui să vedeți o pagină AngularJS stub cu logo-ul lui Yeoman pe ea.
Apoi, trebuie să lansăm serverul nostru de aplicații back-end. Înainte de a putea continua, trebuie să abordăm câteva probleme:
- În mod implicit, atât aplicația AngularJS (bootstrap de Yeoman) cât și aplicația Play încearcă să ruleze pe portul 9000.
- În producție, ambele aplicații vor fi probabil rulate sub un singur domeniu și probabil că vom folosi Nginx pentru a direcționa cererile în consecință. Dar în modul de dezvoltare, atunci când schimbăm numărul portului uneia dintre aceste aplicații, browserele web le vor trata ca și cum ar rula pe domenii diferite.
Pentru a rezolva ambele probleme, tot ce trebuie să facem este să folosim un proxy Grunt, astfel încât toate solicitările AJAX către aplicația Play să fie proxy. Cu aceasta, în esență, ambele servere de aplicații vor fi disponibile la același număr de port aparent.
Să modificăm mai întâi numărul de port al serverului de aplicații Play la 9090. Pentru a face acest lucru, deschideți fereastra „Run/Debug Configurations” făcând clic pe „Run -> Edit Configurations”. Apoi, modificați numărul portului în câmpul „Url To Open”. Faceți clic pe „OK” pentru a aproba această modificare și închideți fereastra. Făcând clic pe butonul „Run” ar trebui să înceapă procesul de rezolvare a dependenței - vor începe să apară jurnalele acestui proces.
Odată ce ați terminat, puteți naviga la http://localhost:9090 pe browserul dvs. web și în câteva secunde ar trebui să puteți vedea aplicația dvs. Play. Pentru a configura un proxy Grunt, mai întâi trebuie să instalăm un mic pachet Node.js folosind NPM:
cd blogapp/client npm install grunt-connect-proxy --save-dev
Apoi, trebuie să modificăm Gruntfile.js. În acel fișier, localizați sarcina „conectare” și introduceți cheia/valoarea „proxies” după aceasta:
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 va trimite acum toate solicitările către „/app/*” către aplicația de back-end Play. Acest lucru ne va scuti de a fi nevoiți să punem pe lista albă fiecare apel către back-end. În plus, trebuie să ne modificăm și comportamentul livereload:
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; } } },
În cele din urmă, trebuie să adăugăm o nouă dependență „'configureProxies:server” la sarcina „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' ]); });
La repornirea Grunt, ar trebui să observați următoarele rânduri în jurnalele dvs. care indică faptul că proxy-ul rulează:
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
Crearea unui formular de înscriere
Vom începe prin a crea un formular de înscriere pentru aplicația noastră de blog. Acest lucru ne va permite, de asemenea, să verificăm că totul funcționează așa cum ar trebui. Putem folosi Yeoman pentru a crea un controler de înregistrare și a vizualiza în AngularJS:
yo angular:controller signup yo angular:view signup
În continuare, ar trebui să actualizăm rutarea aplicației noastre pentru a face referire la această vizualizare nou creată și să eliminăm controlerul și vizualizarea redundante „despre” generate automat. Din fișierul „app/scripts/app.js”, eliminați referințele la „app/scripts/controllers/about.js” și „app/views/about.html”, lăsând-o cu:
.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });
În mod similar, actualizați fișierul „app/index.html” pentru a elimina linkurile redundante și adăugați un link către pagina de înscriere:
<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>
De asemenea, eliminați eticheta de script pentru „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>
Apoi, adăugați un formular în fișierul nostru „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>
Trebuie să facem ca formularul să fie procesat de controlerul Angular. Este demn de remarcat faptul că nu trebuie să adăugăm în mod specific atributul „ng-controller” în vizualizările noastre, deoarece logica noastră de rutare în „app.js” declanșează automat un controler înainte ca vizualizarea noastră să fie încărcată. Tot ce trebuie să facem pentru a conecta acest formular este să avem o funcție de „înscriere” adecvată definită în $scope. Acest lucru ar trebui făcut în fișierul „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); }); }; });
Acum să deschidem consola pentru dezvoltatori Chrome, să comutăm la fila „Rețea” și să încercăm să trimitem formularul de înscriere.
Vom vedea că back-end-ul Play răspunde în mod natural cu o pagină de eroare „Acțiune nu a fost găsită”. Acest lucru este de așteptat deoarece nu a fost încă implementat. Dar ceea ce înseamnă, de asemenea, este că configurarea noastră Grunt proxy funcționează corect!
În continuare, vom adăuga o „Acțiune” care este în esență o metodă în controlerul aplicației Play. În clasa „Aplicație” din pachetul „aplicație/controlere”, adăugați o nouă metodă „înscriere”:
public static Result signup() { return ok("Success!"); }
Acum deschideți fișierul „conf/routes” și adăugați următoarea linie:
POST /app/signup controllers.Application.signup
În cele din urmă, revenim la browserul nostru web, http://localhost:9000/#/signup. Făcând clic pe butonul „Trimite” de data aceasta, ar trebui să rezulte ceva diferit:
Ar trebui să vedeți valoarea codificată returnată, cea pe care am scris-o în metoda de înscriere. Dacă acesta este cazul, suntem gata să mergem mai departe, deoarece mediul nostru de dezvoltare este pregătit și funcționează atât pentru aplicațiile Angular, cât și pentru Play.
Definirea modelelor Ebean în joc
Înainte de a defini modelele, să alegem mai întâi un depozit de date. În acest articol, vom folosi baza de date H2 în memorie. Pentru a activa acest lucru, găsiți și decomentați următoarele rânduri în fișierul „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.*"
Și adăugați următoarea linie:
applyEvolutions.default=true
Modelul nostru de domeniu de blog este destul de simplu. În primul rând, avem utilizatori care pot crea postări și apoi fiecare postare poate fi comentată de orice utilizator conectat. Să creăm modelele noastre Ebean.
Utilizator
// 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); } } }
Postare pe 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(); } }
Posteaza comentariu
// 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(); } }
Acțiune reală de înscriere
Acum să creăm prima noastră acțiune reală, permițând utilizatorilor să se înscrie:
// 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; }
Rețineți că autentificarea utilizată în această aplicație este foarte simplă și nu este recomandată pentru utilizare în producție.
Partea interesantă este că folosim formularele Play pentru a gestiona formularele de înscriere. Am stabilit câteva constrângeri pentru clasa noastră de formulare de înscriere. Validarea se va face automat pentru noi, fără a fi nevoie de o logică explicită de validare.
Dacă revenim la aplicația noastră AngularJS în browserul web și facem clic din nou pe „Trimite”, vom vedea că serverul răspunde acum cu o eroare adecvată - că aceste câmpuri sunt obligatorii.
Gestionarea erorilor de server în AngularJS
Așa că primim o eroare de la server, dar utilizatorul aplicației habar nu are ce se întâmplă. Cel mai puțin putem face este să afișăm eroarea utilizatorului nostru. În mod ideal, ar trebui să înțelegem ce fel de eroare primim și să afișăm un mesaj ușor de utilizat. Să creăm un serviciu simplu de alertă care ne va ajuta să afișăm eroarea.
În primul rând, trebuie să generăm un șablon de serviciu cu Yeoman:
yo angular:service alerts
Apoi, adăugați acest cod la „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; } );
Acum, să creăm un controler separat responsabil pentru alerte:
yo angular:controller alerts
angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });
Acum trebuie să arătăm mesaje de eroare frumoase Bootstrap. Cel mai simplu mod este să utilizați Angular UI. Putem folosi Bower pentru a-l instala:
bower install angular-bootstrap --save
În modulul „app.js” anexați Angular UI:
angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])
Să adăugăm o directivă de alertă în fișierul nostru „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>
În cele din urmă, trebuie să actualizăm controlerul SignUp:
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!'); } }) }; });
Acum, dacă trimitem din nou formularul gol, vom vedea erori afișate deasupra formularului:

Acum că erorile sunt gestionate, trebuie să facem ceva când înregistrarea utilizatorului are succes. Putem redirecționa utilizatorul către o pagină de bord unde poate adăuga postări. Dar mai întâi, trebuie să-l creăm:
yo angular:view dashboard yo angular:controller dashboard
Modificați metoda de înscriere a controlerului „signup.js”, astfel încât, la succes, redirecționează utilizatorul:
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });
Adăugați o rută nouă în „apps.js”:
.when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })
De asemenea, trebuie să urmărim dacă utilizatorul este autentificat. Să creăm un serviciu separat pentru asta:
yo angular:service user
// user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });
Și, de asemenea, modificați controlerul de înscriere pentru a seta utilizatorul la unul care tocmai s-a înregistrat:
.success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });
Înainte de a adăuga funcționalitatea principală de adăugare a postărilor, să ne ocupăm de alte caracteristici importante, cum ar fi capacitatea de a vă autentifica și de deconecta, afișarea informațiilor despre utilizator pe tabloul de bord și, de asemenea, adăugarea suportului de autentificare în back-end.
Autentificare de bază
Să trecem la aplicația noastră Play și să implementăm acțiunile de conectare și deconectare. Adăugați aceste linii la „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; }
În continuare, să adăugăm posibilitatea de a permite anumite apeluri back-end numai utilizatorilor autentificați. Creați „Secured.java” cu următorul cod:
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(); } }
Vom folosi această clasă mai târziu pentru a proteja acțiuni noi. Apoi, ar trebui să modificăm meniul principal al aplicației AngularJS, astfel încât să afișeze numele de utilizator și linkurile de deconectare. Pentru asta, trebuie să creăm un controler:
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; } }); });
De asemenea, avem nevoie de o vizualizare și un controler pentru pagina de autentificare:
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'); } }); }; });
În continuare, modificăm meniul astfel încât să poată afișa datele utilizatorului:
<!-- 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>
Acum, dacă vă conectați la aplicație, ar trebui să puteți vedea următorul ecran:
Adăugarea de postări
Acum că avem mecanisme de bază de înregistrare și autentificare, putem trece la implementarea funcționalității de postare. Să adăugăm o nouă vizualizare și un controler pentru adăugarea postărilor.
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); }); }; });
Apoi actualizăm „app.js” pentru a include:
.when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })
Apoi, modificăm „index.html” pentru a adăuga un link pentru vizualizarea noastră „addpost” în meniul tabloului de bord:
<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>
Acum, din partea aplicației Play, să creăm un nou controler Post cu metoda 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; } }
Adăugați o nouă intrare în fișierul rute pentru a putea gestiona metodele nou adăugate în rutare:
POST /app/post controllers.Post.addPost
În acest moment, ar trebui să puteți adăuga postări noi.
Afișarea postărilor
Adăugarea de postări are o valoare mică, dacă nu le putem afișa. Ceea ce vrem să facem este să listăm toate postările de pe pagina principală. Începem prin a adăuga o nouă metodă în controlerul nostru de aplicație:
// Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }
Și înregistrându-l în fișierul nostru de rute:
GET /app/posts controllers.Application.getPosts
Apoi, în aplicația noastră AngularJS ne modificăm controlerul principal:
// 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(); });
În cele din urmă, eliminați totul din „main.html” și adăugați asta:
<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>
Acum, dacă încărcați pagina de pornire a aplicației, ar trebui să vedeți ceva similar cu acesta:
De asemenea, probabil că ar trebui să avem o vedere separată pentru postările individuale.
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>
Și ruta AngularJS:
app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })
Ca și înainte, adăugăm o nouă metodă controlerului nostru de aplicație:
// 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)); }
… Și un nou traseu:
GET /app/post/:id controllers.Application.getPost(id: Long)
Acum, dacă navigați la http://localhost:9000/#/viewpost/1, veți putea încărca o vizualizare pentru o anumită postare. Apoi, să adăugăm posibilitatea de a vedea postările utilizatorului în tabloul de bord:
// 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>
De asemenea, adăugați o nouă metodă la Post controller, urmată de o rută corespunzătoare acestei metode:
// 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
Acum, când creați postări, acestea vor fi listate pe tabloul de bord:
Funcționalitatea de comentare
Pentru a implementa funcționalitatea de comentare, vom începe prin a adăuga o nouă metodă în controlerul Post:
// 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)