本文概述
为正确的目的选择正确的工具要走很长一段路, 特别是在构建现代Web应用程序时。我们很多人都熟悉AngularJS, 它使开发健壮的Web应用程序前端变得多么容易。尽管许多人会反对使用这种流行的Web框架, 但它肯定可以提供很多功能, 并且可以满足各种需求。另一方面, 你在后端使用的组件会影响Web应用程序的性能, 因为它们会影响整体用户体验。 Play是Java和Scala的高速Web框架。它基于轻量级, 无状态, Web友好的体系结构, 并遵循类似于Rails和Django的MVC模式和原理。
在本文中, 我们将研究如何使用AngularJS和Play构建具有基本身份验证机制以及发表帖子和评论功能的简单博客应用程序。 AngularJS开发以及一些Twitter Bootstrap优点将使我们能够在基于Play的REST API后端的基础上提供单页应用程序体验。
Web应用程序-入门
AngularJS应用程序框架
AngularJS和Play应用程序将相应地驻留在客户端和服务器目录中。现在, 我们将创建”客户端”目录。
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应用程序的官方方法包括使用工具Typesafe Activator。在使用它之前, 你必须下载并将其安装在计算机上。如果你在Mac OS上并且使用Homebrew, 则可以使用单行命令安装此工具:
brew install typesafe-activator
从命令行创建Play应用程序非常简单:
cd blogapp/
activator new server play-java
cd server/
导入到IDE
要在诸如Eclipse或IntelliJ的IDE中导入应用程序, 你需要”遮掩”或”理想化”你的应用程序。为此, 请运行以下命令:
activator
看到新提示后, 键入” eclipse”或” idea”, 然后按Enter键以分别为Eclipse或IntelliJ准备应用程序代码。
为简便起见, 本文仅介绍将项目导入IntelliJ的过程。将其导入Eclipse的过程应该同样简单。要将项目导入IntelliJ, 请首先激活”文件->新建”下的”来自现有资源的项目…”选项。接下来, 选择你的build.sbt文件, 然后单击”确定”。在下一个对话框中再次单击”确定”后, IntelliJ应该开始将Play应用程序导入为SBT项目。
Typesafe Activator还带有图形用户界面, 可用于创建此骨架应用程序代码。
现在, 我们已将Play应用程序导入到IntelliJ中, 我们还应该将AngularJS应用程序导入到工作区中。我们可以将其作为一个单独的项目或作为模块导入到Play应用程序所在的现有项目中。
在这里, 我们将Angular应用程序作为模块导入。在”文件”菜单下, 我们将选择选项”新建->来自现有源的模块…”。从对话框中, 我们将选择”客户端”目录, 然后单击”确定”。在接下来的两个屏幕上, 分别单击”下一步”和”完成”。
生成本地服务器
在这一点上, 应该可以从IDE将AngularJS应用程序作为Grunt任务启动。展开你的客户端文件夹, 然后右键单击Gruntfile.js。在弹出菜单中选择”显示任务”。将出现一个标有” Grunt”的面板, 其中包含任务列表:
要开始提供该应用程序, 请双击”服务”。这应该立即打开你的默认Web浏览器并将其指向本地主机地址。你应该会在AngularJS存根页面上看到带有Yeoman徽标的页面。
接下来, 我们需要启动我们的后端应用服务器。在继续之前, 我们必须解决几个问题:
- 默认情况下, AngularJS应用程序(由Yeoman引导)和Play应用程序都尝试在端口9000上运行。
- 在生产中, 两个应用程序都可能在一个域中运行, 并且我们可能会使用Nginx相应地路由请求。但是在开发模式下, 当我们更改其中一个应用程序的端口号时, Web浏览器会将它们视为在不同域上运行。
要解决这两个问题, 我们所需要做的就是使用Grunt代理, 以便代理对Play应用程序的所有AJAX请求。这样, 从本质上讲, 这两个应用服务器都可以在相同的视在端口号上使用。
让我们首先将Play应用程序服务器的端口号更改为9090。为此, 请通过单击”运行->编辑配置”打开”运行/调试配置”窗口。接下来, 在” Url To Open”字段中更改端口号。单击”确定”以批准此更改并关闭窗口。单击”运行”按钮应启动依赖关系解决过程-该过程的日志将开始出现。
完成后, 你可以在网络浏览器上导航至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;
}
}
},
最后, 我们需要在”服务”任务中添加一个新的依赖项”’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创建一个Sign-up控制器并在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" id="js-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" id="email" placeholder="Email"
ng-model="email">
</div>
<div>
<label for="password">Password</label>
<input name="password" class="form-control" type="password" id="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”包中的” Application”类中, 添加一个新的方法” signup”:
public static Result signup() {
return ok("Success!");
}
现在打开文件” conf / routes”并添加以下行:
POST /app/signup controllers.Application.signup
最后, 我们返回到Web浏览器http:// localhost:9000 /#/ signup。这次单击”提交”按钮将产生不同的结果:
你应该看到返回的硬编码值, 即我们在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" id="email" placeholder="Email"
ng-model="email">
</div>
<div>
<label for="password">Password</label>
<input name="password" class="form-control" type="password" id="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" id="js-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" id="subject" placeholder="Subject"
ng-model="subject">
</div>
<div>
<label for="content">Post</label>
<textarea name="content" class="form-control" id="content" 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
此时, 你应该可以添加新帖子。
显示帖子
如果我们无法显示, 添加帖子的价值很小。我们要做的是在主页上列出所有帖子。我们首先在应用程序控制器中添加一个新方法:
// 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;
}
和往常一样, 我们需要为此方法注册一条新路线:
POST /app/comment controllers.Post.addComment
在我们的AngularJS应用程序中, 我们将以下内容添加到” 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();
});
};
最后, 将以下几行添加到” 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" id="comment" placeholder="Comment"
ng-model="comment"></textarea>
</div>
<button type="submit" class="btn btn-primary">Add comment</button>
</form>
现在, 如果你打开任何帖子, 就可以添加和查看评论。
下一步是什么?
在本教程中, 我们使用Play应用程序作为REST API后端构建了AngularJS博客。尽管该应用程序缺乏可靠的数据验证(尤其是在客户端)和安全性, 但是这些主题不在本教程的讨论范围之内。其目的是演示构建此类应用程序的许多可能方法之一。为了方便起见, 此应用程序的源代码已上传到GitHub存储库。
如果你在网络应用程序开发中发现AngularJS和Play的这种组合很有趣, 我强烈建议你进一步检查以下主题:
- AngularJS文档
- 播放文件
- 在页面JS应用中以Play作为后端的推荐安全方法(包含示例)
- 没有OAuth的安全REST API
- Ready Play身份验证插件(可能无法完全用于单页JavaScript应用程序, 但可以用作一个很好的示例)
评论前必须登录!
注册