使用 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)