Erstellen moderner Webanwendungen mit AngularJS und Play Framework
Veröffentlicht: 2022-03-11Die Wahl des richtigen Tools für den richtigen Zweck ist ein langer Weg, insbesondere wenn es um die Erstellung moderner Webanwendungen geht. Viele von uns sind mit AngularJS vertraut und wissen, wie einfach es die Entwicklung robuster Frontends für Webanwendungen macht. Obwohl viele gegen die Verwendung dieses beliebten Web-Frameworks argumentieren werden, hat es sicherlich viel zu bieten und kann eine geeignete Wahl für eine Vielzahl von Anforderungen sein. Andererseits bestimmen die Komponenten, die Sie im Backend verwenden, viel über die Leistung der Webanwendung, da sie Einfluss auf die allgemeine Benutzererfahrung haben. Play ist ein Hochgeschwindigkeits-Webframework für Java und Scala. Es basiert auf einer leichtgewichtigen, zustandslosen, webfreundlichen Architektur und folgt MVC-Mustern und -Prinzipien ähnlich wie Rails und Django.
In diesem Artikel werfen wir einen Blick darauf, wie wir AngularJS und Play verwenden können, um eine einfache Blog-Anwendung mit einem grundlegenden Authentifizierungsmechanismus und der Möglichkeit, Beiträge und Kommentare zu erstellen, zu erstellen. Die AngularJS-Entwicklung mit einigen Twitter Bootstrap-Goodies wird es uns ermöglichen, eine Single-Page-Anwendungserfahrung auf einem Play-basierten REST-API-Back-End zu betreiben.
Webanwendungen - Erste Schritte
AngularJS-Anwendungsskelett
AngularJS- und Play-Apps befinden sich entsprechend in Client- und Serververzeichnissen. Zunächst erstellen wir das „client“-Verzeichnis.
mkdir -p blogapp/client
Um ein AngularJS-Anwendungsskelett zu erstellen, verwenden wir Yeoman - ein erstaunliches Gerüstwerkzeug. Die Installation von Yeoman ist einfach. Es ist wahrscheinlich noch einfacher, es als Gerüst für eine einfache AngularJS-Skelettanwendung zu verwenden:
cd blogapp/client yo angular
Auf das Ausführen des zweiten Befehls folgen einige Optionen, aus denen Sie auswählen müssen. Für dieses Projekt brauchen wir „Sass (mit Kompass)“ nicht. Wir benötigen Boostrap zusammen mit den folgenden AngularJS-Plugins:
- angle-animate.js
- angle-cookies.js
- angle-resource.js
- angle-route.js
- angle-sanitize.js
- angle-touch.js
An diesem Punkt, sobald Sie Ihre Auswahl abgeschlossen haben, sehen Sie die NPM- und Bower-Ausgabe auf Ihrem Terminal. Wenn die Downloads abgeschlossen sind und die Pakete installiert wurden, haben Sie ein einsatzbereites AngularJS-Anwendungsskelett.
Play Framework-Anwendungsskelett
Der offizielle Weg, eine neue Play-Anwendung zu erstellen, beinhaltet die Verwendung des Tools Typesafe Activator. Bevor Sie es verwenden können, müssen Sie es herunterladen und auf Ihrem Computer installieren. Wenn Sie Mac OS verwenden und Homebrew verwenden, können Sie dieses Tool mit einer einzigen Befehlszeile installieren:
brew install typesafe-activator
Das Erstellen einer Play-Anwendung über die Befehlszeile ist super einfach:
cd blogapp/ activator new server play-java cd server/
Importieren in eine IDE
Um die Anwendung in eine IDE wie Eclipse oder IntelliJ zu importieren, müssen Sie Ihre Anwendung „eclipsifizieren“ oder „idealisieren“. Führen Sie dazu den folgenden Befehl aus:
activator
Sobald Sie eine neue Eingabeaufforderung sehen, geben Sie entweder „eclipse“ oder „idea“ ein und drücken Sie die Eingabetaste, um den Anwendungscode für Eclipse bzw. IntelliJ vorzubereiten.
Der Kürze halber behandeln wir in diesem Artikel nur den Prozess des Importierens des Projekts in IntelliJ. Der Prozess des Imports in Eclipse sollte ebenso einfach sein. Um das Projekt in IntelliJ zu importieren, aktivieren Sie zunächst die Option „Projekt aus vorhandenen Quellen…“ unter „Datei -> Neu“. Wählen Sie als Nächstes Ihre build.sbt-Datei aus und klicken Sie auf „OK“. Nachdem Sie im nächsten Dialogfeld erneut auf „OK“ geklickt haben, sollte IntelliJ mit dem Import Ihrer Play-Anwendung als SBT-Projekt beginnen.
Typesafe Activator wird auch mit einer grafischen Benutzeroberfläche geliefert, mit der Sie diesen Anwendungscode erstellen können.
Nachdem wir unsere Play-Anwendung in IntelliJ importiert haben, sollten wir auch unsere AngularJS-Anwendung in den Workspace importieren. Wir können es entweder als separates Projekt oder als Modul in das vorhandene Projekt importieren, in dem sich die Play-Anwendung befindet.
Hier importieren wir die Angular-Anwendung als Modul. Unter dem Menü „Datei“ wählen wir die Option „Neu -> Modul aus vorhandenen Quellen…“. Aus dem Dialog wählen wir das Verzeichnis „Client“ und klicken auf „OK“. Klicken Sie auf den nächsten beiden Bildschirmen auf „Weiter“ bzw. „Fertig stellen“.
Spawnen lokaler Server
An dieser Stelle sollte es möglich sein, die AngularJS-Anwendung als Grunt-Task aus der IDE heraus zu starten. Erweitern Sie Ihren Client-Ordner und klicken Sie mit der rechten Maustaste auf Gruntfile.js. Wählen Sie im Popup-Menü „Grunt-Aufgaben anzeigen“. Ein Bereich mit der Bezeichnung „Grunt“ wird mit einer Liste von Aufgaben angezeigt:
Um mit der Bereitstellung der Anwendung zu beginnen, doppelklicken Sie auf „servieren“. Dies sollte sofort Ihren Standard-Webbrowser öffnen und ihn auf eine localhost-Adresse verweisen. Sie sollten eine Stub-AngularJS-Seite mit Yeomans Logo darauf sehen.
Als nächstes müssen wir unseren Back-End-Anwendungsserver starten. Bevor wir fortfahren können, müssen wir einige Probleme lösen:
- Standardmäßig versuchen sowohl die AngularJS-Anwendung (Bootstrapping von Yeoman) als auch die Play-Anwendung, auf Port 9000 ausgeführt zu werden.
- In der Produktion werden wahrscheinlich beide Anwendungen unter einer Domäne ausgeführt, und wir werden wahrscheinlich Nginx verwenden, um die Anfragen entsprechend weiterzuleiten. Wenn wir jedoch im Entwicklungsmodus die Portnummer einer dieser Anwendungen ändern, behandeln Webbrowser sie so, als würden sie auf verschiedenen Domänen ausgeführt.
Um diese beiden Probleme zu umgehen, müssen wir lediglich einen Grunt-Proxy verwenden, damit alle AJAX-Anforderungen an die Play-Anwendung weitergeleitet werden. Damit sind im Wesentlichen diese beiden Anwendungsserver unter der gleichen scheinbaren Portnummer verfügbar.
Ändern wir zunächst die Portnummer des Play-Anwendungsservers auf 9090. Öffnen Sie dazu das Fenster „Run/Debug Configurations“, indem Sie auf „Run -> Edit Configurations“ klicken. Als nächstes ändern Sie die Portnummer im Feld „Url To Open“. Klicken Sie auf „OK“, um diese Änderung zu bestätigen und das Fenster zu schließen. Durch Klicken auf die Schaltfläche „Ausführen“ sollte der Abhängigkeitsauflösungsprozess gestartet werden – Protokolle dieses Prozesses werden angezeigt.
Sobald dies erledigt ist, können Sie in Ihrem Webbrowser zu http://localhost:9090 navigieren, und in wenigen Sekunden sollten Sie Ihre Play-Anwendung sehen können. Um einen Grunt-Proxy zu konfigurieren, müssen wir zunächst ein kleines Node.js-Paket mit NPM installieren:
cd blogapp/client npm install grunt-connect-proxy --save-dev
Als nächstes müssen wir unsere Gruntfile.js anpassen. Suchen Sie in dieser Datei die Aufgabe „Verbinden“ und fügen Sie den Schlüssel/Wert „Proxies“ danach ein:
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 leitet nun alle Anfragen an „/app/*“ an die Back-End-Play-Anwendung weiter. Dies erspart uns, jeden Aufruf an das Back-End auf die Whitelist setzen zu müssen. Darüber hinaus müssen wir auch unser Livereload-Verhalten optimieren:
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; } } },
Schließlich müssen wir der Aufgabe „serve“ eine neue Abhängigkeit „configureProxies:server“ hinzufügen:
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' ]); });
Nach dem Neustart von Grunt sollten Sie die folgenden Zeilen in Ihren Protokollen bemerken, die darauf hinweisen, dass der Proxy ausgeführt wird:
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
Erstellen eines Anmeldeformulars
Wir beginnen mit der Erstellung eines Anmeldeformulars für unsere Blog-Anwendung. Dadurch können wir auch überprüfen, ob alles so funktioniert, wie es sollte. Wir können Yeoman verwenden, um einen Registrierungscontroller zu erstellen und in AngularJS anzuzeigen:
yo angular:controller signup yo angular:view signup
Als Nächstes sollten wir das Routing unserer Anwendung aktualisieren, um auf diese neu erstellte Ansicht zu verweisen, und den redundanten automatisch generierten „about“-Controller und die Ansicht entfernen. Entfernen Sie in der Datei „app/scripts/app.js“ Verweise auf „app/scripts/controllers/about.js“ und „app/views/about.html“ und belassen Sie Folgendes:
.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });
Aktualisieren Sie auf ähnliche Weise die Datei „app/index.html“, um die redundanten Links zu entfernen, und fügen Sie einen Link zur Anmeldeseite hinzu:
<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>
Entfernen Sie außerdem das script-Tag für „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>
Fügen Sie als Nächstes ein Formular zu unserer Datei „signup.html“ hinzu:
<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>
Wir müssen dafür sorgen, dass das Formular vom Angular-Controller verarbeitet wird. Es ist erwähnenswert, dass wir das Attribut „ng-controller“ nicht ausdrücklich in unseren Ansichten hinzufügen müssen, da unsere Routing-Logik in „app.js“ automatisch einen Controller startet, bevor unsere Ansicht geladen wird. Alles, was wir tun müssen, um dieses Formular zu verdrahten, ist, eine richtige „Signup“-Funktion in $scope zu definieren. Dies sollte in der Datei „signup.js“ erfolgen:
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); }); }; });
Lassen Sie uns nun die Chrome-Entwicklerkonsole öffnen, zur Registerkarte „Netzwerk“ wechseln und versuchen, das Anmeldeformular zu senden.
Wir werden sehen, dass das Play-Backend natürlich mit einer „Aktion nicht gefunden“-Fehlerseite antwortet. Dies wird erwartet, da es noch nicht implementiert wurde. Das bedeutet aber auch, dass unser Grunt-Proxy-Setup korrekt funktioniert!
Als Nächstes fügen wir eine „Aktion“ hinzu, die im Wesentlichen eine Methode im Play-Anwendungscontroller ist. Fügen Sie in der Klasse „Application“ im Paket „app/controllers“ eine neue Methode „signup“ hinzu:
public static Result signup() { return ok("Success!"); }
Öffnen Sie nun die Datei „conf/routes“ und fügen Sie folgende Zeile hinzu:
POST /app/signup controllers.Application.signup
Schließlich kehren wir zu unserem Webbrowser http://localhost:9000/#/signup zurück. Wenn Sie diesmal auf die Schaltfläche „Senden“ klicken, sollte sich etwas anderes ergeben:
Sie sollten den hartcodierten zurückgegebenen Wert sehen, den wir in der Anmeldemethode geschrieben haben. Wenn dies der Fall ist, sind wir bereit, weiterzumachen, da unsere Entwicklungsumgebung bereit ist und sowohl für die Angular- als auch für die Play-Anwendung funktioniert.
Definieren von Ebean-Modellen im Spiel
Lassen Sie uns vor dem Definieren von Modellen zunächst einen Datenspeicher auswählen. In diesem Artikel verwenden wir die In-Memory-Datenbank von H2. Um dies zu aktivieren, suchen und kommentieren Sie die folgenden Zeilen in der Datei „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.*"
Und fügen Sie die folgende Zeile hinzu:
applyEvolutions.default=true
Unser Blog-Domain-Modell ist ziemlich einfach. Zunächst einmal haben wir Benutzer, die Beiträge erstellen dürfen, und dann kann jeder Beitrag von jedem angemeldeten Benutzer kommentiert werden. Lassen Sie uns unsere Ebean-Modelle erstellen.
Benutzer
// 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); } } }
Blogeintrag
// 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(); } }
Kommentar hinzufügen
// 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(); } }
Echte Anmeldeaktion
Lassen Sie uns nun unsere erste echte Aktion erstellen, die es den Benutzern ermöglicht, sich anzumelden:
// 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; }
Beachten Sie, dass die in dieser App verwendete Authentifizierung sehr einfach ist und nicht für die Verwendung in der Produktion empfohlen wird.
Der interessante Teil ist, dass wir Play-Formulare verwenden, um Anmeldeformulare zu bearbeiten. Wir haben ein paar Einschränkungen für unsere SignUp-Formularklasse festgelegt. Die Validierung wird für uns automatisch durchgeführt, ohne dass eine explizite Validierungslogik erforderlich ist.
Wenn wir im Webbrowser zu unserer AngularJS-Anwendung zurückkehren und erneut auf „Submit“ klicken, sehen wir, dass der Server nun mit einem entsprechenden Fehler antwortet – dass diese Felder erforderlich sind.
Behandlung von Serverfehlern in AngularJS
Wir erhalten also einen Fehler vom Server, aber der Anwendungsbenutzer hat keine Ahnung, was los ist. Das Mindeste, was wir tun können, ist, den Fehler unserem Benutzer anzuzeigen. Im Idealfall müssten wir verstehen, welche Art von Fehler wir erhalten, und eine benutzerfreundliche Nachricht anzeigen. Lassen Sie uns einen einfachen Benachrichtigungsdienst erstellen, der uns hilft, den Fehler anzuzeigen.
Zuerst müssen wir mit Yeoman ein Service-Template generieren:
yo angular:service alerts
Fügen Sie als Nächstes diesen Code zu „alerts.js“ hinzu:
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; } );
Lassen Sie uns nun einen separaten Controller erstellen, der für Warnungen verantwortlich ist:
yo angular:controller alerts
angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });
Jetzt müssen wir tatsächlich nette Bootstrap-Fehlermeldungen anzeigen. Der einfachste Weg ist die Verwendung von Angular UI. Wir können Bower verwenden, um es zu installieren:
bower install angular-bootstrap --save
Fügen Sie in Ihrer „app.js“ das Angular UI-Modul an:
angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])
Fügen wir unserer Datei „index.html“ eine Alert-Direktive hinzu:
<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>
Schließlich müssen wir den SignUp-Controller aktualisieren:

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!'); } }) }; });
Wenn wir nun das leere Formular erneut senden, werden über dem Formular Fehler angezeigt:
Da nun Fehler behandelt werden, müssen wir etwas unternehmen, wenn die Benutzeranmeldung erfolgreich ist. Wir können den Benutzer auf eine Dashboard-Seite umleiten, auf der er Beiträge hinzufügen kann. Aber zuerst müssen wir es erstellen:
yo angular:view dashboard yo angular:controller dashboard
Ändern Sie die Registrierungsmethode des Controllers „signup.js“ so, dass der Benutzer bei Erfolg umgeleitet wird:
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });
Fügen Sie eine neue Route in „apps.js“ hinzu:
.when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })
Wir müssen auch verfolgen, ob der Benutzer angemeldet ist. Lassen Sie uns dafür einen separaten Dienst erstellen:
yo angular:service user
// user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });
Und ändern Sie auch den Registrierungscontroller, um den Benutzer auf einen festzulegen, der sich gerade registriert hat:
.success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });
Bevor wir die Hauptfunktionalität zum Hinzufügen von Beiträgen hinzufügen, kümmern wir uns um einige andere wichtige Funktionen wie die Möglichkeit zum An- und Abmelden, das Anzeigen von Benutzerinformationen auf dem Dashboard und das Hinzufügen von Authentifizierungsunterstützung im Backend.
Grundlegende Authentifizierung
Lassen Sie uns zu unserer Play-Anwendung springen und die Anmelde- und Abmeldeaktionen implementieren. Fügen Sie diese Zeilen zu „Application.java“ hinzu:
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; }
Als Nächstes fügen wir die Möglichkeit hinzu, bestimmte Back-End-Aufrufe nur für authentifizierte Benutzer zuzulassen. Erstellen Sie „Secured.java“ mit folgendem Code:
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(); } }
Wir werden diese Klasse später verwenden, um neue Aktionen zu schützen. Als nächstes sollten wir das Hauptmenü unserer AngularJS-Anwendung so anpassen, dass der Benutzername und die Abmeldelinks angezeigt werden. Dazu müssen wir einen Controller erstellen:
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; } }); });
Wir brauchen auch einen View und einen Controller für die Login-Seite:
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'); } }); }; });
Als nächstes optimieren wir das Menü, damit es Benutzerdaten anzeigen kann:
<!-- 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>
Wenn Sie sich jetzt bei der Anwendung anmelden, sollten Sie den folgenden Bildschirm sehen können:
Beiträge hinzufügen
Nachdem wir nun grundlegende Anmelde- und Authentifizierungsmechanismen eingerichtet haben, können wir uns an die Implementierung der Posting-Funktion machen. Lassen Sie uns eine neue Ansicht und einen Controller zum Hinzufügen von Beiträgen hinzufügen.
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); }); }; });
Dann aktualisieren wir „app.js“, um Folgendes einzuschließen:
.when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })
Als nächstes ändern wir „index.html“, um einen Link für unsere „addpost“-Ansicht im Dashboard-Menü hinzuzufügen:
<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>
Lassen Sie uns nun auf der Play-Anwendungsseite einen neuen Controller-Post mit der addPost-Methode erstellen:
// 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; } }
Fügen Sie der Routendatei einen neuen Eintrag hinzu, um neu hinzugefügte Methoden beim Routing verarbeiten zu können:
POST /app/post controllers.Post.addPost
An dieser Stelle sollten Sie in der Lage sein, neue Beiträge hinzuzufügen.
Beiträge anzeigen
Das Hinzufügen von Beiträgen hat wenig Wert, wenn wir sie nicht anzeigen können. Wir möchten alle Beiträge auf der Hauptseite auflisten. Wir beginnen mit dem Hinzufügen einer neuen Methode in unserem Anwendungscontroller:
// Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }
Und die Registrierung in unserer Routendatei:
GET /app/posts controllers.Application.getPosts
Als nächstes modifizieren wir in unserer AngularJS-Anwendung unseren Hauptcontroller:
// 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(); });
Entfernen Sie schließlich alles aus „main.html“ und fügen Sie Folgendes hinzu:
<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>
Wenn Sie jetzt Ihre Anwendungshomepage laden, sollten Sie etwas Ähnliches sehen:
Wir sollten wahrscheinlich auch eine separate Ansicht für einzelne Beiträge haben.
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>
Und die AngularJS-Route:
app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })
Wie zuvor fügen wir unserem Application Controller eine neue Methode hinzu:
// 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)); }
… und eine neue Strecke:
GET /app/post/:id controllers.Application.getPost(id: Long)
Wenn Sie jetzt zu http://localhost:9000/#/viewpost/1 navigieren, können Sie eine Ansicht für einen bestimmten Beitrag laden. Als Nächstes fügen wir die Möglichkeit hinzu, die Beiträge des Benutzers im Dashboard anzuzeigen:
// 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>
Fügen Sie dem Post-Controller auch eine neue Methode hinzu, gefolgt von einer Route, die dieser Methode entspricht:
// 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
Wenn Sie jetzt Beiträge erstellen, werden diese im Dashboard aufgelistet:
Kommentarfunktion
Um die Kommentarfunktion zu implementieren, fügen wir zunächst eine neue Methode im Post-Controller hinzu:
// 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.
Was kommt als nächstes?
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)