Создание современных веб-приложений с помощью AngularJS и Play Framework
Опубликовано: 2022-03-11Выбор правильного инструмента для правильной цели имеет большое значение, особенно когда речь идет о создании современных веб-приложений. Многие из нас знакомы с AngularJS и с тем, как легко он упрощает разработку надежных интерфейсов веб-приложений. Хотя многие будут возражать против использования этого популярного веб-фреймворка, он, безусловно, может многое предложить и может быть подходящим выбором для широкого круга потребностей. С другой стороны, компоненты, которые вы используете на серверной части, во многом определяют производительность веб-приложения, поскольку они влияют на общее взаимодействие с пользователем. Play — это высокоскоростная веб-инфраструктура для Java и Scala. Он основан на облегченной, удобной для Интернета архитектуре без сохранения состояния и следует шаблонам и принципам MVC, аналогичным Rails и Django.
В этой статье мы рассмотрим, как мы можем использовать AngularJS и Play для создания простого приложения для блога с базовым механизмом аутентификации и возможностью публиковать сообщения и комментарии. Разработка на AngularJS с некоторыми преимуществами Twitter Bootstrap позволит нам реализовать одностраничное приложение поверх серверной части REST API на основе Play.
Веб-приложения — начало работы
Каркас приложения AngularJS
Приложения AngularJS и Play будут находиться в каталогах клиента и сервера соответственно. Сейчас мы создадим каталог «client».
mkdir -p blogapp/client
Для создания скелета приложения AngularJS мы будем использовать Yeoman — замечательный инструмент для создания шаблонов. Установить Yeoman очень просто. Использовать его для создания простого скелетного приложения AngularJS, вероятно, еще проще:
cd blogapp/client yo angular
Запуск второй команды будет сопровождаться несколькими вариантами, которые вам нужно выбрать. Для этого проекта нам не нужен «Sass (с компасом)». Нам понадобится Boostrap вместе со следующими плагинами AngularJS:
- angular-animate.js
- angular-cookies.js
- angular-resource.js
- angular-route.js
- angular-sanitize.js
- angular-touch.js
На этом этапе, как только вы завершите свой выбор, вы начнете видеть выходные данные NPM и Bower на своем терминале. Когда загрузка будет завершена и пакеты будут установлены, у вас будет готовый к использованию каркас приложения AngularJS.
Каркас приложения Play Framework
Официальный способ создания нового приложения Play предполагает использование инструмента Typesafe Activator. Прежде чем вы сможете использовать его, вы должны загрузить и установить его на свой компьютер. Если вы работаете в Mac OS и используете Homebrew, вы можете установить этот инструмент с помощью одной строки команды:
brew install typesafe-activator
Создать приложение Play из командной строки очень просто:
cd blogapp/ activator new server play-java cd server/
Импорт в IDE
Чтобы импортировать приложение в IDE, например Eclipse или IntelliJ, вам необходимо «затмить» или «идеализировать» ваше приложение. Для этого выполните следующую команду:
activator
Когда вы увидите новое приглашение, введите «eclipse» или «idea» и нажмите Enter, чтобы подготовить код приложения для Eclipse или IntelliJ соответственно.
Для краткости в этой статье мы рассмотрим только процесс импорта проекта в IntelliJ. Процесс импорта его в Eclipse должен быть таким же простым. Чтобы импортировать проект в IntelliJ, начните с активации параметра «Проект из существующих источников…», который находится в разделе «Файл -> Новый». Затем выберите файл build.sbt и нажмите «ОК». После повторного нажатия «ОК» в следующем диалоговом окне IntelliJ должен начать импорт вашего приложения Play в качестве проекта SBT.
Typesafe Activator также поставляется с графическим пользовательским интерфейсом, который вы можете использовать для создания скелетного кода приложения.
Теперь, когда мы импортировали наше приложение Play в IntelliJ, мы также должны импортировать наше приложение AngularJS в рабочую область. Мы можем импортировать его либо как отдельный проект, либо как модуль в существующий проект, в котором находится приложение Play.
Здесь мы импортируем приложение Angular как модуль. В меню «Файл» мы выберем опцию «Создать -> Модуль из существующих источников…». В диалоговом окне мы выберем каталог «клиент» и нажмите «ОК». На следующих двух экранах нажмите «Далее» и «Готово» соответственно.
Создание локальных серверов
На этом этапе должно быть возможно запустить приложение AngularJS как задачу Grunt из IDE. Разверните папку клиента и щелкните правой кнопкой мыши файл Gruntfile.js. Во всплывающем меню выберите «Показать задачи Grunt». Появится панель с надписью «Grunt» со списком задач:
Чтобы начать обслуживание приложения, дважды щелкните «обслуживать». Это должно немедленно открыть веб-браузер по умолчанию и указать адрес локального хоста. Вы должны увидеть заглушку страницы AngularJS с логотипом Yeoman.
Далее нам нужно запустить наш внутренний сервер приложений. Прежде чем мы сможем продолжить, мы должны решить пару вопросов:
- По умолчанию и приложение AngularJS (загруженное Yeoman), и приложение Play пытаются работать на порту 9000.
- В производственной среде оба приложения, скорее всего, будут работать под одним доменом, и мы, вероятно, будем использовать Nginx для соответствующей маршрутизации запросов. Но в режиме разработки, когда мы меняем номер порта одного из этих приложений, веб-браузеры будут рассматривать их так, как будто они работают в разных доменах.
Чтобы обойти обе эти проблемы, все, что нам нужно сделать, это использовать прокси-сервер Grunt, чтобы все запросы AJAX к приложению Play пересылались через прокси. При этом, по сути, оба этих сервера приложений будут доступны с одним и тем же очевидным номером порта.
Давайте сначала изменим номер порта сервера приложений Play на 9090. Для этого откройте окно «Выполнить/отладить конфигурации», нажав «Выполнить -> Редактировать конфигурации». Далее измените номер порта в поле «URL для открытия». Нажмите «ОК», чтобы подтвердить это изменение и закрыть окно. Нажатие на кнопку «Выполнить» должно запустить процесс разрешения зависимостей — начнут появляться журналы этого процесса.
Как только это будет сделано, вы можете перейти по адресу http://localhost:9090 в своем веб-браузере, и через несколько секунд вы сможете увидеть свое приложение Play. Чтобы настроить прокси-сервер Grunt, нам сначала нужно установить небольшой пакет Node.js с помощью NPM:
cd blogapp/client npm install grunt-connect-proxy --save-dev
Далее нам нужно настроить наш Gruntfile.js. В этом файле найдите задачу «connect» и вставьте ключ/значение «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 теперь будет проксировать все запросы к «/app/*» на серверное приложение Play. Это избавит нас от необходимости заносить в белый список каждый вызов серверной части. Кроме того, нам также нужно настроить поведение 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; } } },
Наконец, нам нужно добавить новую зависимость «configureProxies: server» к задаче «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' ]); });
После перезапуска Grunt вы должны заметить следующие строки в своих журналах, указывающие на то, что прокси-сервер работает:
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
Создание формы регистрации
Мы начнем с создания формы регистрации для нашего блог-приложения. Это также позволит нам убедиться, что все работает как надо. Мы можем использовать Yeoman для создания контроллера регистрации и просмотра в AngularJS:
yo angular:controller signup yo angular:view signup
Затем мы должны обновить маршрутизацию нашего приложения, чтобы ссылаться на это вновь созданное представление, и удалить избыточный автоматически сгенерированный контроллер и представление «о нас». Из файла «app/scripts/app.js» удалите ссылки на «app/scripts/controllers/about.js» и «app/views/about.html», оставив в нем:
.config(function ($routeProvider) { $routeProvider .when('/', { templateUrl: 'views/main.html', controller: 'MainCtrl' }) .when('/signup', { templateUrl: 'views/signup.html', controller: 'SignupCtrl' }) .otherwise({ redirectTo: '/' });
Аналогичным образом обновите файл «app/index.html», чтобы удалить лишние ссылки, и добавьте ссылку на страницу регистрации:
<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>
Кроме того, удалите тег script для «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>
Затем добавьте форму в наш файл «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>
Нам нужно, чтобы форма обрабатывалась контроллером Angular. Стоит отметить, что нам не нужно специально добавлять атрибут «ng-controller» в наши представления, поскольку наша логика маршрутизации в «app.js» автоматически запускает контроллер перед загрузкой нашего представления. Все, что нам нужно сделать, чтобы подключить эту форму, — это иметь правильную функцию «регистрации», определенную в $scope. Это нужно сделать в файле «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); }); }; });
Теперь давайте откроем консоль разработчика Chrome, переключимся на вкладку «Сеть» и попробуем отправить форму регистрации.
Мы увидим, что серверная часть Play, естественно, отвечает страницей ошибки «Действие не найдено». Это ожидается, так как это еще не реализовано. Но это также означает, что наша настройка прокси-сервера Grunt работает правильно!
Далее мы собираемся добавить «Действие», которое по сути является методом в контроллере приложения Play. В классе «Приложение» в пакете «app/controllers» добавьте новый метод «signup»:
public static Result signup() { return ok("Success!"); }
Теперь откройте файл «conf/routes» и добавьте следующую строку:
POST /app/signup controllers.Application.signup
Наконец, мы возвращаемся в наш веб-браузер, http://localhost:9000/#/signup. При нажатии на кнопку «Отправить» на этот раз должно получиться что-то другое:
Вы должны увидеть возвращенное жестко закодированное значение, которое мы написали в методе регистрации. Если это так, мы готовы двигаться дальше, поскольку наша среда разработки готова и работает как с приложениями Angular, так и с Play.
Определение моделей Ebean в игре
Прежде чем определять модели, давайте сначала выберем хранилище данных. В этой статье мы будем использовать базу данных H2 в памяти. Чтобы включить это, найдите и раскомментируйте следующие строки в файле «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.*"
И добавьте следующую строку:
applyEvolutions.default=true
Модель домена нашего блога довольно проста. Во-первых, у нас есть пользователи, которые могут создавать сообщения, а затем каждое сообщение может быть прокомментировано любым авторизованным пользователем. Давайте создадим наши модели Ebean.
Пользователь
// 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); } } }
Сообщение блога
// 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(); } }
Оставить комментарий
// 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(); } }
Реальное действие при регистрации
Теперь давайте создадим наше первое настоящее действие, позволяющее пользователям зарегистрироваться:
// 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; }
Обратите внимание, что аутентификация, используемая в этом приложении, очень проста и не рекомендуется для использования в рабочей среде.
Интересно то, что мы используем формы Play для обработки форм регистрации. Мы установили пару ограничений для нашего класса формы SignUp. Проверка будет выполнена для нас автоматически без необходимости явной логики проверки.
Если мы вернемся к нашему приложению AngularJS в веб-браузере и снова нажмем «Отправить», мы увидим, что сервер теперь отвечает соответствующей ошибкой — эти поля обязательны.
Обработка ошибок сервера в AngularJS
Итак, мы получаем ошибку от сервера, но пользователь приложения понятия не имеет, что происходит. Меньшее, что мы можем сделать, это показать ошибку нашему пользователю. В идеале нам нужно понять, какую ошибку мы получаем, и отобразить удобное для пользователя сообщение. Давайте создадим простой сервис оповещений, который поможет нам отобразить ошибку.
Во-первых, нам нужно сгенерировать шаблон сервиса с помощью Yeoman:
yo angular:service alerts
Затем добавьте этот код в «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; } );
Теперь создадим отдельный контроллер, отвечающий за алерты:
yo angular:controller alerts
angular.module('clientApp') .controller('AlertsCtrl', function ($scope, alertService) { $scope.alerts = alertService.get(); });
Теперь нам нужно показать красивые сообщения об ошибках Bootstrap. Самый простой способ — использовать Angular UI. Мы можем использовать Bower для его установки:
bower install angular-bootstrap --save
В вашем «app.js» добавьте модуль Angular UI:
angular .module('clientApp', [ 'ngAnimate', 'ngCookies', 'ngResource', 'ngRoute', 'ngSanitize', 'ngTouch', 'ui.bootstrap' ])
Давайте добавим директиву оповещения в наш файл «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>
Наконец, нам нужно обновить контроллер 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!'); } }) }; });
Теперь, если мы снова отправим пустую форму, мы увидим ошибки, отображаемые над формой:

Теперь, когда ошибки обработаны, нам нужно что-то сделать, когда регистрация пользователя прошла успешно. Мы можем перенаправить пользователя на страницу панели инструментов, где он может добавлять сообщения. Но сначала мы должны его создать:
yo angular:view dashboard yo angular:controller dashboard
Измените метод регистрации контроллера «signup.js», чтобы в случае успеха он перенаправлял пользователя:
angular.module('clientApp') .controller('SignupCtrl', function ($scope, $http, $log, alertService, $location) { // .. .success(function(data) { if(data.hasOwnProperty('success')) { $location.path('/dashboard'); } });
Добавьте новый маршрут в «apps.js»:
.when('/dashboard', { templateUrl: 'views/dashboard.html', controller: 'DashboardCtrl' })
Также нам нужно отслеживать, авторизовался ли пользователь. Создадим для этого отдельный сервис:
yo angular:service user
// user.js angular.module('clientApp') .factory('userService', function() { var username = ''; return { username : username }; });
А также измените контроллер регистрации, чтобы настроить пользователя на того, кто только что зарегистрировался:
.success(function(data) { if(data.hasOwnProperty('success')) { userService.username = $scope.email; $location.path('/dashboard');; } });
Прежде чем мы добавим основную функциональность добавления сообщений, давайте позаботимся о некоторых других важных функциях, таких как возможность входа и выхода из системы, отображение информации о пользователе на панели инструментов, а также добавление поддержки аутентификации в серверной части.
Базовая аутентификация
Давайте перейдем к нашему приложению Play и реализуем действия входа и выхода. Добавьте эти строки в «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; }
Далее давайте добавим возможность разрешать определенные внутренние вызовы только пользователям, прошедшим проверку подлинности. Создайте «Secured.java» со следующим кодом:
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(); } }
Позже мы будем использовать этот класс для защиты новых действий. Затем мы должны настроить главное меню нашего приложения AngularJS, чтобы оно отображало имя пользователя и ссылки для выхода. Для этого нам нужно создать контроллер:
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; } }); });
Нам также нужны представление и контроллер для страницы входа:
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'); } }); }; });
Затем мы настраиваем меню, чтобы оно могло отображать пользовательские данные:
<!-- 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>
Теперь, если вы войдете в приложение, вы увидите следующий экран:
Добавление сообщений
Теперь, когда у нас есть основные механизмы регистрации и аутентификации, мы можем приступить к реализации функции публикации. Давайте добавим новое представление и контроллер для добавления сообщений.
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); }); }; });
Затем мы обновляем «app.js», чтобы включить:
.when('/addpost', { templateUrl: 'views/addpost.html', controller: 'AddpostCtrl' })
Затем мы модифицируем «index.html», чтобы добавить ссылку на наше представление «addpost» в меню панели инструментов:
<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>
Теперь на стороне приложения Play давайте создадим новый контроллер Post с методом 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; } }
Добавьте новую запись в файл маршрутов, чтобы иметь возможность обрабатывать недавно добавленные методы маршрутизации:
POST /app/post controllers.Post.addPost
На этом этапе вы должны иметь возможность добавлять новые сообщения.
Отображение сообщений
Добавление сообщений не имеет большого значения, если мы не можем их отобразить. Что мы хотим сделать, так это перечислить все сообщения на главной странице. Начнем с добавления нового метода в наш контроллер приложения:
// Application.java public static Result getPosts() { return ok(Json.toJson(BlogPost.find.findList())); }
И регистрируем его в нашем файле маршрутов:
GET /app/posts controllers.Application.getPosts
Далее в нашем приложении AngularJS мы модифицируем наш основной контроллер:
// 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(); });
Наконец, удалите все из «main.html» и добавьте это:
<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>
Теперь, если вы загрузите домашнюю страницу приложения, вы должны увидеть что-то похожее на это:
Нам также, вероятно, следует иметь отдельный вид для отдельных сообщений.
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>
И маршрут AngularJS:
app.js: .when('/viewpost/:postId', { templateUrl: 'views/viewpost.html', controller: 'ViewpostCtrl' })
Как и раньше, мы добавляем новый метод в наш контроллер приложения:
// 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)); }
… И новый маршрут:
GET /app/post/:id controllers.Application.getPost(id: Long)
Теперь, если вы перейдете по адресу http://localhost:9000/#/viewpost/1, вы сможете загрузить представление для определенного сообщения. Далее добавим возможность видеть сообщения пользователя в дашборде:
// 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>
Также добавьте новый метод в Post-контроллер, за которым следует маршрут, соответствующий этому методу:
// 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
Теперь, когда вы создаете сообщения, они будут отображаться на панели инструментов:
Функциональность комментирования
Чтобы реализовать функцию комментирования, мы начнем с добавления нового метода в контроллер 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.
Что дальше?
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)