AngularJSとPlayFrameworkを使用した最新のWebアプリケーションの構築

公開: 2022-03-11

特に最新のWebアプリケーションを構築する場合、適切な目的に適切なツールを選択することは大いに役立ちます。 私たちの多くは、AngularJSと、それが堅牢なWebアプリケーションフロントエンドの開発をいかに簡単にするかをよく知っています。 多くの人がこの人気のあるWebフレームワークの使用に反対しますが、それは確かに提供するものがたくさんあり、幅広いニーズに適した選択肢になる可能性があります。 一方、バックエンドで使用するコンポーネントは、全体的なユーザーエクスペリエンスに影響を与えるため、Webアプリケーションのパフォーマンスに大きく影響します。 Playは、JavaおよびScala用の高速Webフレームワークです。 これは、軽量でステートレスなWebフレンドリーなアーキテクチャに基づいており、RailsやDjangoと同様のMVCパターンと原則に従います。

anglejsとplayフレームワークを備えたWebアプリケーション

この記事では、AngularJSとPlayを使用して、基本認証メカニズムと投稿やコメントを作成する機能を備えたシンプルなブログアプリケーションを構築する方法を見ていきます。 AngularJSの開発といくつかのTwitterBootstrapの機能により、PlayベースのRESTAPIバックエンドに加えてシングルページアプリケーションエクスペリエンスを強化できます。

Webアプリケーション-はじめに

AngularJSアプリケーションスケルトン

AngularJSおよびPlayアプリは、それに応じてクライアントおよびサーバーディレクトリに常駐します。 ここでは、「client」ディレクトリを作成します。

 mkdir -p blogapp/client

AngularJSアプリケーションスケルトンを作成するには、すばらしいスキャフォールディングツールであるYeomanを使用します。 ヨーマンのインストールは簡単です。 単純な骨格のAngularJSアプリケーションをスキャフォールディングするために使用すると、おそらくさらに簡単になります。

 cd blogapp/client yo angular

2番目のコマンドを実行すると、選択する必要のあるいくつかのオプションが続きます。 このプロジェクトでは、「Sass(コンパス付き)」は必要ありません。 Boostrapと次のAngularJSプラグインが必要になります。

  • angle-animate.js
  • angle-cookies.js
  • angle-resource.js
  • angle-route.js
  • angle-sanitize.js
  • angle-touch.js

この時点で、選択を確定すると、端末にNPMとBowerの出力が表示されるようになります。 ダウンロードが完了し、パッケージがインストールされると、AngularJSアプリケーションスケルトンを使用できるようになります。

PlayFrameworkアプリケーションスケルトン

新しいPlayアプリケーションを作成する公式の方法には、TypesafeActivatorツールを使用することが含まれます。 使用する前に、ダウンロードしてコンピュータにインストールする必要があります。 Mac OSを使用していて、Homebrewを使用している場合は、次の1行のコマンドでこのツールをインストールできます。

 brew install typesafe-activator

コマンドラインからPlayアプリケーションを作成するのはとても簡単です。

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

IDEへのインポート

EclipseやIntelliJなどのIDEにアプリケーションをインポートするには、アプリケーションを「Eclipse化」または「理想化」する必要があります。 これを行うには、次のコマンドを実行します。

 activator

新しいプロンプトが表示されたら、「eclipse」または「idea」と入力し、Enterキーを押して、それぞれEclipseまたはIntelliJのアプリケーションコードを準備します。

簡潔にするために、この記事ではプロジェクトをIntelliJにインポートするプロセスのみを取り上げます。 それをEclipseにインポートするプロセスも同様に単純でなければなりません。 プロジェクトをIntelliJにインポートするには、「ファイル->新規」の下にある「既存のソースからのプロジェクト…」オプションをアクティブ化することから始めます。 次に、build.sbtファイルを選択し、「OK」をクリックします。 次のダイアログでもう一度[OK]をクリックすると、IntelliJはPlayアプリケーションをSBTプロジェクトとしてインポートし始めるはずです。

Typesafe Activatorには、この骨格アプリケーションコードを作成するために使用できるグラフィカルユーザーインターフェイスも付属しています。

PlayアプリケーションをIntelliJにインポートしたので、AngularJSアプリケーションもワークスペースにインポートする必要があります。 別のプロジェクトとして、またはPlayアプリケーションが存在する既存のプロジェクトへのモジュールとしてインポートできます。

ここでは、Angularアプリケーションをモジュールとしてインポートします。 「ファイル」メニューの下で、「新規->既存のソースからのモジュール…」オプションを選択します。 ダイアログから「クライアント」ディレクトリを選択し、「OK」をクリックします。 次の2つの画面で、それぞれ[次へ]と[完了]をクリックします。

ローカルサーバーの生成

この時点で、IDEからGruntタスクとしてAngularJSアプリケーションを開始できるはずです。 クライアントフォルダを展開し、Gruntfile.jsを右クリックします。 ポップアップメニューで「GruntTasksを表示」を選択します。 「Grunt」というラベルの付いたパネルが、タスクのリストとともに表示されます。

ローカルサーバーの生成

アプリケーションの提供を開始するには、「提供」をダブルクリックします。 これにより、すぐにデフォルトのWebブラウザーが開き、ローカルホストアドレスを指すようになります。 Yeomanのロゴが付いたスタブAngularJSページが表示されます。

次に、バックエンドアプリケーションサーバーを起動する必要があります。 先に進む前に、いくつかの問題に対処する必要があります。

  1. デフォルトでは、AngularJSアプリケーション(Yeomanによってブートストラップされた)とPlayアプリケーションの両方がポート9000で実行しようとします。
  2. 本番環境では、両方のアプリケーションが1つのドメインで実行される可能性が高く、それに応じてNginxを使用してリクエストをルーティングします。 ただし、開発モードでは、これらのアプリケーションの1つのポート番号を変更すると、Webブラウザーはそれらを別のドメインで実行されているかのように扱います。

これらの両方の問題を回避するには、Gruntプロキシを使用して、PlayアプリケーションへのすべてのAJAXリクエストがプロキシされるようにするだけです。 これにより、本質的に、これらのアプリケーションサーバーは両方とも同じ見かけのポート番号で使用できるようになります。

まず、Playアプリケーションサーバーのポート番号を9090に変更します。これを行うには、[実行]-> [構成の編集]をクリックして、[構成の実行/デバッグ]ウィンドウを開きます。 次に、「UrlToOpen」フィールドのポート番号を変更します。 「OK」をクリックしてこの変更を承認し、ウィンドウを閉じます。 「実行」ボタンをクリックすると、依存関係の解決プロセスが開始されます。このプロセスのログが表示され始めます。

ローカルサーバーの生成

完了すると、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; } } },

最後に、新しい依存関係「'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

次に、この新しく作成されたビューを参照するようにアプリケーションのルーティングを更新し、冗長な自動生成された「about」コントローラーとビューを削除する必要があります。 ファイル「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コントローラーで処理する必要があります。 「app.js」のルーティングロジックは、ビューが読み込まれる前にコントローラーを自動的に起動するため、ビューに「ng-controller」属性を具体的に追加する必要がないことに注意してください。 このフォームを接続するために必要なのは、$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デベロッパーコンソールを開き、[ネットワーク]タブに切り替えて、登録フォームを送信してみましょう。

AngularPlayサインアップフォームの例

Playバックエンドが「アクションが見つかりません」というエラーページで自然に応答することがわかります。 まだ実装されていないため、これは予想されます。 しかし、それが意味するのは、Gruntプロキシのセットアップが正しく機能しているということです。

次に、基本的にPlayアプリケーションコントローラーのメソッドである「アクション」を追加します。 「app/controllers」パッケージの「Application」クラスに、新しいメソッド「signup」を追加します。

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

次に、ファイル「conf / routers」を開き、次の行を追加します。

 POST /app/signup controllers.Application.signup

最後に、Webブラウザhttp:// localhost:9000 /#/signupに戻ります。 今回は「送信」ボタンをクリックすると、別の結果が得られるはずです。

ブラウザでのAngularPlayサインアップフォームの例

サインアップメソッドで記述した、ハードコードされた値が返されるのがわかります。 その場合は、開発環境の準備が整い、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

 // 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エラーメッセージを表示する必要があります。 最も簡単な方法は、AngularUIを使用することです。 Bowerを使用してインストールできます。

 bower install angular-bootstrap --save

「app.js」にAngularUIモジュールを追加します。

 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!'); } }) }; });

ここで、空のフォームを再度送信すると、フォームの上にエラーが表示されます。

AngularPlayエラー

エラーが処理されたので、ユーザーのサインアップが成功したときに何かをする必要があります。 ユーザーをダッシュ​​ボードページにリダイレクトして、投稿を追加することができます。 しかし、最初に、それを作成する必要があります。

 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>

これで、アプリケーションのホームページをロードすると、次のようなものが表示されるはずです。

AngularjsPlayの例が読み込まれました

また、おそらく個々の投稿に対して個別のビューを用意する必要があります。

 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
  • ドキュメントを再生する
  • 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)