使用 AngularJS 和 Play 框架构建现代 Web 应用程序

已发表: 2022-03-11

为正确的目的选择正确的工具有很长的路要走,尤其是在构建现代 Web 应用程序时。 我们很多人都熟悉 AngularJS,以及它使开发健壮的 Web 应用程序前端变得多么容易。 尽管许多人会反对使用这个流行的 Web 框架,但它确实可以提供很多东西,并且可以成为满足各种需求的合适选择。 另一方面,您在后端使用的组件将决定 Web 应用程序的性能,因为它们会影响整体用户体验。 Play 是一个用于 Java 和 Scala 的高速 Web 框架。 它基于轻量级、无状态、网络友好的架构,并遵循类似于 Rails 和 Django 的 MVC 模式和原则。

带有 angularjs 和 play 框架的 web 应用程序

在本文中,我们将了解如何使用 AngularJS 和 Play 构建一个简单的博客应用程序,该应用程序具有基本的身份验证机制以及发表帖子和评论的能力。 AngularJS 开发,以及一些 Twitter Bootstrap 好东西,将允许我们在基于 Play 的 REST API 后端之上提供单页应用程序体验。

Web 应用程序 - 入门

AngularJS 应用程序骨架

AngularJS 和 Play 应用程序将相应地驻留在客户端和服务器目录中。 现在,我们将创建“client”目录。

 mkdir -p blogapp/client

要创建 AngularJS 应用程序框架,我们将使用 Yeoman - 一个了不起的脚手架工具。 安装 Yeoman 很容易。 使用它来构建一个简单的骨架 AngularJS 应用程序可能更容易:

 cd blogapp/client yo angular

运行第二个命令后会出现一些您需要从中选择的选项。 对于这个项目,我们不需要“Sass (with Compass)”。 我们将需要 Boostrap 以及以下 AngularJS 插件:

  • 角动画.js
  • 角cookies.js
  • 角资源.js
  • 角路由.js
  • 角清理.js
  • 角触摸.js

此时,一旦您完成选择,您将开始在终端上看到 NPM 和 Bower 输出。 下载完成并安装包后,您将拥有一个可供使用的 AngularJS 应用程序框架。

Play 框架应用骨架

创建新 Play 应用程序的官方方法涉及使用工具 Typesafe Activator。 在您可以使用它之前,您必须在您的计算机上下载并安装它。 如果您在 Mac OS 上并使用 Homebrew,则可以使用一行命令安装此工具:

 brew install typesafe-activator

从命令行创建 Play 应用程序非常简单:

 cd blogapp/ activator new server play-java cd server/

导入到 IDE

要在 Eclipse 或 IntelliJ 等 IDE 中导入应用程序,您需要“eclipsify”或“idealize”您的应用程序。 为此,请运行以下命令:

 activator

看到新提示后,键入“eclipse”或“idea”并按 Enter 键分别为 Eclipse 或 IntelliJ 准备应用程序代码。

为简洁起见,我们将在本文中仅介绍将项目导入 IntelliJ 的过程。 将其导入 Eclipse 的过程应该同样简单。 要将项目导入 IntelliJ,首先激活“File -> New”下的“Project from existing sources...”选项。 接下来,选择您的 build.sbt 文件并单击“确定”。 在下一个对话框中再次单击“确定”后,IntelliJ 应该开始将您的 Play 应用程序作为 SBT 项目导入。

Typesafe Activator 还带有一个图形用户界面,您可以使用它来创建这个骨架应用程序代码。

现在我们已经将 Play 应用程序导入 IntelliJ,我们还应该将 AngularJS 应用程序导入工作区。 我们可以将其作为单独的项目或作为模块导入到 Play 应用程序所在的现有项目中。

在这里,我们将 Angular 应用程序作为模块导入。 在“文件”菜单下,我们将选择“新建 -> 来自现有源的模块...”选项。 从对话框中,我们将选择“客户端”目录,然后单击“确定”。 在接下来的两个屏幕上,分别单击“下一步”和“完成”。

生成本地服务器

此时,应该可以从 IDE 将 AngularJS 应用程序作为 Grunt 任务启动。 展开您的客户端文件夹并右键单击 Gruntfile.js。 在弹出菜单中选择“显示 Grunt 任务”。 将出现一个标有“Grunt”的面板,其中包含任务列表:

生成本地服务器

要开始为应用程序提供服务,请双击“服务”。 这应该会立即打开您的默认 Web 浏览器并将其指向 localhost 地址。 您应该会看到一个带有 Yeoman 徽标的 AngularJS 存根页面。

接下来,我们需要启动我们的后端应用服务器。 在我们继续之前,我们必须解决几个问题:

  1. 默认情况下,AngularJS 应用程序(由 Yeoman 引导)和 Play 应用程序都尝试在端口 9000 上运行。
  2. 在生产中,这两个应用程序可能会在一个域下运行,我们可能会使用 Nginx 来相应地路由请求。 但是在开发模式下,当我们更改其中一个应用程序的端口号时,Web 浏览器会将它们视为运行在不同的域上。

要解决这两个问题,我们需要做的就是使用 Grunt 代理,以便代理对 Play 应用程序的所有 AJAX 请求。 有了这个,本质上这两个应用程序服务器将在相同的明显端口号上可用。

让我们首先将 Play 应用程序服务器的端口号更改为 9090。为此,请单击“运行 -> 编辑配置”打开“运行/调试配置”窗口。 接下来,更改“Url To Open”字段中的端口号。 单击“确定”以批准此更改并关闭窗口。 单击“运行”按钮应启动依赖项解析过程 - 该过程的日志将开始出现。

生成本地服务器

完成后,您可以在 Web 浏览器上导航到 http://localhost:9090,几秒钟后您应该能够看到您的 Play 应用程序。 要配置 Grunt 代理,我们首先需要使用 NPM 安装一个小的 Node.js 包:

 cd blogapp/client npm install grunt-connect-proxy --save-dev

接下来我们需要调整我们的 Gruntfile.js。 在该文件中,找到“连接”任务,并在其后插入“代理”键/值:

 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; } } },

最后,我们需要在“serve”任务中添加一个新的依赖“'configureProxies:server”:

 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>

此外,删除“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 开发者控制台,切换到“网络”选项卡,然后尝试提交注册表单。

Angular Play 注册表单示例

我们将看到 Play 后端自然地回复“未找到操作”错误页面。 这是预期的,因为它尚未实施。 但这也意味着我们的 Grunt 代理设置工作正常!

接下来,我们将添加一个“Action”,它本质上是 Play 应用程序控制器中的一个方法。 在“app/controllers”包的“Application”类中,添加一个新方法“signup”:

 public static Result signup() { return ok("Success!"); }

现在打开文件“conf/routes”并添加以下行:

 POST /app/signup controllers.Application.signup

最后,我们返回到 Web 浏览器 http://localhost:9000/#/signup。 这次单击“提交”按钮应该会产生不同的结果:

浏览器中的 Angular Play 注册表单示例

您应该会看到返回的硬编码值,即我们在注册方法中编写的值。 如果是这种情况,我们已经准备好继续前进,因为我们的开发环境已经准备好并且可以同时用于 Angular 和 Play 应用程序。

在 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 表单类上设置了几个约束。 验证将自动为我们完成,无需显式验证逻辑。

如果我们在 Web 浏览器中返回 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 应用程序端,让我们使用 addPost 方法创建一个新的控制器 Post:

 // 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

此时,您应该能够添加新帖子。

在 Play 中添加新帖子

显示帖子

添加帖子没有什么价值,如果我们不能显示它们。 我们要做的是在主页上列出所有帖子。 我们首先在应用程序控制器中添加一个新方法:

 // 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>

现在,如果您加载应用程序主页,您应该会看到类似于以下内容:

Angularjs Play 示例已加载

我们可能还应该对个别帖子有一个单独的视图。

 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.

View comments screenshot

下一步是什么?

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)