Tworzenie nowoczesnych aplikacji internetowych za pomocą AngularJS i Play Framework
Opublikowany: 2022-03-11Wybór odpowiedniego narzędzia do właściwego celu to długa droga, zwłaszcza jeśli chodzi o tworzenie nowoczesnych aplikacji internetowych. Wielu z nas jest zaznajomionych z AngularJS i zna to, jak łatwe jest tworzenie solidnych front-endów aplikacji internetowych. Chociaż wielu będzie sprzeciwiać się korzystaniu z tej popularnej platformy internetowej, z pewnością ma ona wiele do zaoferowania i może być odpowiednim wyborem dla szerokiego zakresu potrzeb. Z drugiej strony komponenty, których używasz na zapleczu, będą decydować o wydajności aplikacji internetowej, ponieważ mają wpływ na ogólne wrażenia użytkownika. Play to szybki framework sieciowy dla Javy i Scali. Opiera się na lekkiej, bezstanowej, przyjaznej dla sieci architekturze i przestrzega wzorców i zasad MVC podobnych do Rails i Django.
W tym artykule przyjrzymy się, w jaki sposób możemy użyć AngularJS i Play do zbudowania prostej aplikacji blogowej z podstawowym mechanizmem uwierzytelniania oraz możliwością tworzenia postów i komentarzy. Rozwój AngularJS, z kilkoma gadżetami Twitter Bootstrap, pozwoli nam na obsługę jednostronicowej aplikacji opartej na back-endzie API REST opartym na Play.
Aplikacje internetowe — wprowadzenie
Szkielet aplikacji AngularJS
Aplikacje AngularJS i Play będą znajdować się odpowiednio w katalogach klienta i serwera. Na razie stworzymy katalog „klient”.
mkdir -p blogapp/client
Do stworzenia szkieletu aplikacji AngularJS użyjemy Yeoman - niesamowitego narzędzia do tworzenia rusztowań. Instalacja Yeoman jest łatwa. Używanie go do tworzenia szkieletu prostej aplikacji szkieletowej AngularJS jest prawdopodobnie jeszcze łatwiejsze:
cd blogapp/client yo angular
Po uruchomieniu drugiego polecenia pojawi się kilka opcji, z których musisz wybrać. Do tego projektu nie potrzebujemy „Sass (z kompasem)”. Będziemy potrzebować Boostrapa wraz z następującymi wtyczkami AngularJS:
- angular-animate.js
- angular-cookies.js
- angular-resource.js
- angular-route.js
- angular-sanitize.js
- angular-touch.js
W tym momencie, po sfinalizowaniu wyborów, zaczniesz widzieć dane wyjściowe NPM i Bower na swoim terminalu. Po zakończeniu pobierania i zainstalowaniu pakietów będziesz mieć szkielet aplikacji AngularJS gotowy do użycia.
Szkielet aplikacji Play Framework
Oficjalny sposób tworzenia nowej aplikacji Play polega na wykorzystaniu narzędzia Typesafe Activator. Zanim będziesz mógł z niego korzystać, musisz go pobrać i zainstalować na swoim komputerze. Jeśli korzystasz z systemu Mac OS i używasz Homebrew, możesz zainstalować to narzędzie za pomocą jednego wiersza polecenia:
brew install typesafe-activator
Tworzenie aplikacji Play z wiersza poleceń jest bardzo proste:
cd blogapp/ activator new server play-java cd server/
Importowanie do IDE
Aby zaimportować aplikację do IDE, takiego jak Eclipse lub IntelliJ, musisz „eclipsify” lub „idealizować” swoją aplikację. Aby to zrobić, uruchom następujące polecenie:
activator
Gdy zobaczysz nowy monit, wpisz „eclipse” lub „idea” i naciśnij Enter, aby przygotować kod aplikacji odpowiednio dla Eclipse lub IntelliJ.
Dla zwięzłości w tym artykule omówimy tylko proces importowania projektu do IntelliJ. Proces importowania go do Eclipse powinien być równie prosty. Aby zaimportować projekt do IntelliJ, zacznij od aktywacji opcji „Projekt z istniejących źródeł…” znajdującej się w „Plik -> Nowy”. Następnie wybierz plik build.sbt i kliknij "OK". Po ponownym kliknięciu „OK” w następnym oknie dialogowym, IntelliJ powinien rozpocząć importowanie aplikacji Play jako projektu SBT.
Typesafe Activator jest również dostarczany z graficznym interfejsem użytkownika, którego można użyć do stworzenia tego szkieletowego kodu aplikacji.
Teraz, gdy zaimportowaliśmy naszą aplikację Play do IntelliJ, powinniśmy również zaimportować naszą aplikację AngularJS do obszaru roboczego. Możemy go zaimportować jako osobny projekt lub jako moduł do istniejącego projektu, w którym znajduje się aplikacja Play.
Tutaj zaimportujemy aplikację Angular jako moduł. W menu „Plik” wybierzemy opcję „Nowy -> Moduł z istniejących źródeł…”. W oknie dialogowym wybierzemy katalog „klient” i klikniemy „OK”. Na kolejnych dwóch ekranach kliknij odpowiednio „Dalej” i „Zakończ”.
Spawnowanie lokalnych serwerów
W tym momencie powinno być możliwe uruchomienie aplikacji AngularJS jako zadania Grunt z IDE. Rozwiń folder klienta i kliknij prawym przyciskiem myszy Gruntfile.js. W wyskakującym menu wybierz "Pokaż zadania Grunt". Pojawi się panel o nazwie „Grunt” z listą zadań:
Aby rozpocząć obsługę aplikacji, kliknij dwukrotnie „obsługuj”. Powinno to natychmiast otworzyć domyślną przeglądarkę internetową i wskazać adres lokalnego hosta. Powinieneś zobaczyć skrótową stronę AngularJS z logo Yeoman.
Następnie musimy uruchomić nasz serwer aplikacji zaplecza. Zanim będziemy mogli kontynuować, musimy zająć się kilkoma kwestiami:
- Domyślnie zarówno aplikacja AngularJS (ładowana przez Yeoman), jak i aplikacja Play, próbują działać na porcie 9000.
- W środowisku produkcyjnym obie aplikacje będą prawdopodobnie uruchamiane w jednej domenie i prawdopodobnie użyjemy Nginx do odpowiedniego kierowania żądań. Ale w trybie deweloperskim, gdy zmienimy numer portu jednej z tych aplikacji, przeglądarki internetowe potraktują je tak, jakby działały na różnych domenach.
Aby obejść oba te problemy, wystarczy użyć proxy Grunt, aby wszystkie żądania AJAX do aplikacji Play były proxy. Dzięki temu w zasadzie oba te serwery aplikacji będą dostępne pod tym samym pozornym numerem portu.
Najpierw zmieńmy numer portu serwera aplikacji Play na 9090. Aby to zrobić, otwórz okno „Uruchom/Debuguj konfiguracje”, klikając „Uruchom -> Edytuj konfiguracje”. Następnie zmień numer portu w polu „Url do otwarcia”. Kliknij „OK”, aby zatwierdzić tę zmianę i zamknąć okno. Kliknięcie przycisku „Uruchom” powinno rozpocząć proces rozwiązywania zależności - zaczną się pojawiać logi tego procesu.
Po zakończeniu możesz przejść do http://localhost:9090 w przeglądarce internetowej, a za kilka sekund powinieneś zobaczyć swoją aplikację Play. Aby skonfigurować proxy Grunt, najpierw musimy zainstalować mały pakiet Node.js za pomocą NPM:
cd blogapp/client npm install grunt-connect-proxy --save-dev
Następnie musimy poprawić nasz plik Gruntfile.js. W tym pliku zlokalizuj zadanie „connect” i wstaw po nim klucz/wartość „proxy”:
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 będzie teraz przesyłał wszystkie żądania do „/app/*” do aplikacji zaplecza Play. Dzięki temu nie będziemy musieli umieszczać na białej liście każdego połączenia z zapleczem. Co więcej, musimy również poprawić nasze zachowanie wczytywania:
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; } } },
Na koniec musimy dodać nową zależność „'configureProxies:server” do zadania „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' ]); });
Po ponownym uruchomieniu Grunta powinieneś zauważyć następujące wiersze w swoich dziennikach wskazujące, że proxy działa:
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
Tworzenie formularza rejestracyjnego
Zaczniemy od stworzenia formularza zapisu do naszej aplikacji blogowej. Pozwoli nam to również zweryfikować, czy wszystko działa tak, jak powinno. Możemy użyć Yeoman do stworzenia kontrolera rejestracji i widoku w AngularJS:
yo angular:controller signup yo angular:view signup
Następnie powinniśmy zaktualizować routing naszej aplikacji, aby odwoływał się do nowo utworzonego widoku i usunąć nadmiarowy automatycznie generowany kontroler i widok „informacje”. Z pliku „app/scripts/app.js” usuń odniesienia do „app/scripts/controllers/about.js” i „app/views/about.html”, pozostawiając je z:
.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });
Podobnie zaktualizuj plik „app/index.html”, aby usunąć zbędne łącza i dodaj łącze do strony rejestracji:
<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>
Usuń również tag skryptu dla „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>
Następnie dodaj formularz do naszego pliku „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>
Musimy sprawić, by formularz został przetworzony przez kontroler Angular. Warto zauważyć, że nie musimy specjalnie dodawać atrybutu „ng-controller” w naszych widokach, ponieważ nasza logika routingu w „app.js” automatycznie uruchamia kontroler przed załadowaniem naszego widoku. Wszystko, co musimy zrobić, aby połączyć ten formularz, to mieć odpowiednią funkcję „rejestracji” zdefiniowaną w $scope. Należy to zrobić w pliku „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); }); }; });
Otwórzmy teraz konsolę programisty Chrome, przejdźmy do zakładki „Sieć” i spróbujmy przesłać formularz rejestracyjny.
Zobaczymy, że zaplecze Play w naturalny sposób odpowiada stroną błędu „Nie znaleziono akcji”. Jest to oczekiwane, ponieważ nie zostało jeszcze wdrożone. Ale oznacza to również, że nasza konfiguracja proxy Grunt działa poprawnie!
Następnie dodamy „Akcję”, która jest zasadniczo metodą w kontrolerze aplikacji Play. W klasie „Aplikacja” w pakiecie „app/controllers” dodaj nową metodę „signup”:
public static Result signup() { return ok("Success!"); }
Teraz otwórz plik „conf/routes” i dodaj następujący wiersz:
POST /app/signup controllers.Application.signup
Na koniec wracamy do naszej przeglądarki internetowej http://localhost:9000/#/signup. Kliknięcie przycisku „Prześlij” tym razem powinno dać coś innego:
Powinieneś zobaczyć zwróconą wartość zakodowaną na sztywno, tę, którą napisaliśmy w metodzie rejestracji. Jeśli tak jest, jesteśmy gotowi, aby przejść dalej, ponieważ nasze środowisko programistyczne jest gotowe i pracuje zarówno dla aplikacji Angular, jak i Play.
Definiowanie modeli Ebean w Play
Przed zdefiniowaniem modeli wybierzmy najpierw datastore. W tym artykule wykorzystamy bazę danych w pamięci H2. Aby to umożliwić, znajdź i odkomentuj następujące wiersze w pliku „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 dodaj następującą linię:
applyEvolutions.default=true
Nasz model domeny blogowej jest dość prosty. Przede wszystkim mamy użytkowników, którzy mogą tworzyć posty, a następnie każdy wpis może być komentowany przez dowolnego zalogowanego użytkownika. Stwórzmy nasze modele Ebean.
Użytkownik
// 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 na blogu
// 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(); } }
Wyślij komentarz
// 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(); } }
Prawdziwa akcja rejestracji
Teraz stwórzmy naszą pierwszą prawdziwą akcję, pozwalającą użytkownikom na rejestrację:
// 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; }
Pamiętaj, że uwierzytelnianie używane w tej aplikacji jest bardzo proste i nie jest zalecane do użytku produkcyjnego.
Interesujące jest to, że do obsługi formularzy rejestracyjnych używamy formularzy Play. Ustawiliśmy kilka ograniczeń w naszej klasie formularzy SignUp. Walidacja zostanie wykonana za nas automatycznie, bez potrzeby stosowania jawnej logiki walidacji.
Jeśli wrócimy do naszej aplikacji AngularJS w przeglądarce internetowej i ponownie klikniemy „Wyślij”, zobaczymy, że serwer odpowiada teraz odpowiednim błędem – że te pola są wymagane.
Obsługa błędów serwera w AngularJS
Otrzymujemy więc błąd z serwera, ale użytkownik aplikacji nie ma pojęcia, co się dzieje. Co najmniej możemy zrobić, to wyświetlić błąd naszemu użytkownikowi. Idealnie byłoby, gdybyśmy musieli zrozumieć, jaki rodzaj błędu otrzymujemy, i wyświetlić przyjazny dla użytkownika komunikat. Stwórzmy prostą usługę alertów, która pomoże nam wyświetlić błąd.
Najpierw musimy wygenerować szablon usługi za pomocą Yeoman:
yo angular:service alerts
Następnie dodaj ten kod do „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; } );
Teraz stwórzmy osobny kontroler odpowiedzialny za alerty:
yo angular:controller alerts
angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });
Teraz musimy pokazać ładne komunikaty o błędach Bootstrap. Najłatwiej jest użyć Angular UI. Do instalacji możemy użyć Bowera:
bower install angular-bootstrap --save
W swoim „app.js” dołącz moduł Angular UI:
angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])
Dodajmy dyrektywę alert do naszego pliku „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>
Na koniec musimy zaktualizować kontroler 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!'); } }) }; });
Teraz, jeśli ponownie wyślemy pusty formularz, zobaczymy błędy wyświetlane nad formularzem:

Teraz, gdy błędy są już obsługiwane, musimy coś zrobić, gdy rejestracja użytkownika się powiedzie. Możemy przekierować użytkownika na stronę dashboardu, gdzie może dodawać posty. Ale najpierw musimy go stworzyć:
yo angular:view dashboard yo angular:controller dashboard
Zmodyfikuj metodę rejestracji kontrolera „signup.js” tak, aby w przypadku powodzenia przekierowywała użytkownika:
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });
Dodaj nową trasę w „apps.js”:
.when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })
Musimy również śledzić, czy użytkownik jest zalogowany. Stwórzmy do tego osobną usługę:
yo angular:service user
// user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });
A także zmodyfikuj kontroler rejestracji, aby ustawić użytkownika na tego, który właśnie się zarejestrował:
.success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });
Zanim dodamy główną funkcjonalność dodawania postów, zajmijmy się kilkoma innymi ważnymi funkcjami, takimi jak możliwość logowania i wylogowania, wyświetlanie informacji o użytkowniku na dashboardzie, a także dodanie obsługi uwierzytelniania w back-endzie.
Uwierzytelnianie podstawowe
Przejdźmy do naszej aplikacji Play i zaimplementujmy akcje logowania i wylogowania. Dodaj te wiersze do „Aplikacji.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; }
Następnie dodajmy możliwość zezwalania na określone wywołania zaplecza tylko uwierzytelnionym użytkownikom. Utwórz „Secured.java” z następującym kodem:
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(); } }
Użyjemy tej klasy później do ochrony nowych akcji. Następnie powinniśmy dostosować nasze menu główne aplikacji AngularJS tak, aby wyświetlało nazwę użytkownika i linki do wylogowania. W tym celu musimy stworzyć kontroler:
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; } }); });
Potrzebujemy również widoku i kontrolera dla strony logowania:
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'); } }); }; });
Następnie dostosowujemy menu, aby mogło wyświetlać dane użytkownika:
<!-- 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>
Teraz, jeśli zalogujesz się do aplikacji, powinieneś zobaczyć następujący ekran:
Dodawanie postów
Teraz, gdy mamy już podstawowe mechanizmy rejestracji i uwierzytelniania, możemy zabrać się do wdrożenia funkcji publikowania. Dodajmy nowy widok i kontroler do dodawania postów.
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); }); }; });
Następnie aktualizujemy „app.js”, aby uwzględnić:
.when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })
Następnie modyfikujemy „index.html”, aby dodać link do naszego widoku „addpost” w menu pulpitu nawigacyjnego:
<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>
Teraz po stronie aplikacji Play stwórzmy nowy post kontrolera za pomocą metody 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; } }
Dodaj nowy wpis do pliku tras, aby móc obsługiwać nowo dodane metody w routingu:
POST /app/post controllers.Post.addPost
W tym momencie powinieneś być w stanie dodawać nowe posty.
Wyświetlanie postów
Dodawanie postów ma niewielką wartość, jeśli nie możemy ich wyświetlić. To, co chcemy zrobić, to wymienić wszystkie posty na stronie głównej. Zaczynamy od dodania nowej metody w naszym kontrolerze aplikacji:
// Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }
I zarejestrowanie go w naszym pliku tras:
GET /app/posts controllers.Application.getPosts
Następnie w naszej aplikacji AngularJS modyfikujemy nasz główny kontroler:
// 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(); });
Na koniec usuń wszystko z „main.html” i dodaj to:
<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>
Teraz, jeśli załadujesz stronę główną aplikacji, powinieneś zobaczyć coś podobnego do tego:
Powinniśmy też prawdopodobnie mieć osobny widok na poszczególne posty.
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>
A trasa AngularJS:
app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })
Tak jak poprzednio, do naszego kontrolera aplikacji dodajemy nową metodę:
// 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 nowa trasa:
GET /app/post/:id controllers.Application.getPost(id: Long)
Teraz, jeśli przejdziesz do http://localhost:9000/#/viewpost/1, będziesz mógł załadować widok dla konkretnego posta. Następnie dodajmy możliwość przeglądania postów użytkownika w dashboardzie:
// 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>
Dodaj również nową metodę do kontrolera Post, a następnie trasę odpowiadającą tej metodzie:
// 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
Teraz, gdy tworzysz posty, będą one wyświetlane na pulpicie nawigacyjnym:
Funkcjonalność komentowania
Aby zaimplementować funkcjonalność komentowania, zaczniemy od dodania nowej metody w kontrolerze 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)