今回は前回に引き続きSingle Page Application をAngularJSで構築する際のTIPS「後編」をお届けします。
前編はこちらです。これからAngularJSを勉強しよう、という入門者の方は、前編の「AngularJSについてこれから勉強する方」をご覧下さい。
【前編】
1.ServiceとFactoryの使い分け
2.URLのルーティングにはUI-Routerを使う
3.画面操作の多い画面では複数のController、Viewに分割した設計をする
【後編】
4.複数のController間のデータ共有にはShared Serviceを使う
5.サーバーサイドのURLをRESTで設計できる場合、AngularJSのRESTクライアントを利用できる。
6.Ajaxリクエストの共通処理はインターセプターで実装する
4.複数のController間のデータ共有にはShared Serviceを使う
Controllerを分割すると、Controller間のデータ共有をどうするか?という問題が出てきます。 いくつか方法はあるのですが、最もシンプルな方法としては、共有のデータ領域として使うServiceを用意することです。
こちらのサイトがController間のデータ共有についての方法を記載されており、3つのデータ共有の方法を紹介してくださっています。
「Qiitaの複数Controller間のデータ共有について」
http://qiita.com/sunny4381/items/aeae1e154346b5cf6009
IDCFの社内のいくつかのプロジェクトではShared Serviceによって共有データ領域を実現しました。 各データ共有の方法と、なぜそれを選んだか?という点を解説します。
Shared Service
AngularJSでは、Controllerでthis(または$scope)にプロパティを設定することで、ControllerとViewのデータの受け渡しをすることができます。 Shared Serviceを使う方法は、thisにShared Serviceを登録します。 Shared ServiceにControllerとViewで受け渡ししたいプロパティを設定します。
Shared ServiceをインジェクションすればどのControllerからもアクセスできます。 これはメリットでもあり、アクセスのルールを決めておかなければ、 どこからアクセスしているのかわからなくなるスパゲッティコードを招く危険もありますので、アクセスのルールを決めておく必要があります。
オブジェクトストレージのプロジェクトでは下記のようなアクセスルールを決めました。
- 1つの機能のカテゴリーにひとつのShared Service
- 同じカテゴリーのControllerから以外はアクセスしない(APIユーザーを扱うControllerからはオブジェクトリストShared Serviceにアクセスしないなど。)
その他のデータ共有の方法も確認しながら利用方法を比較してみます。
Parent Scope
親クラスの$scopeを利用するデータ共有の方法です。 AngularJSでは、Viewの中でControllerを入れ子にすると、内側のControllerが外側のControllerを継承しする親子関係になります。 内側の子のControllerは親の$scopeの値を参照でききます。 このように自分のプロパティのようにアクセスすることができます。
Pub / Sub
イベントを利用したデータ共有の方法です。
データ共有方法の比較
比較したところ、それぞれ下記の課題があがりました。
Shared Service | ・Controllerにインジェクションすれば、どのControllerからも共有のデータ領域を利用できる。 ・どのControllerからもアクセスできるため、アクセスのルールをプロジェクトで決める必要がある。 |
Parent Scope | ・値を共有するために、継承を利用する設計が足かせにならないか。 |
Pub / Sub | ・SharedServiceに比べると大掛かりなしくみ ・値を取得するときにはSharedDataServiceから取得する必要がある。 値を渡すときには$bloadcastで送信し、bloadcastを受け取った側がSharedDataServiceに保存する必要がある。 |
最終的には、Shared Serviceを選びました。理由としては以下になります。
- Shared Serviceが最も仕組みがシンプルなこと。
- アクセスのルール化がしやすかったこと。
- Serviceを使う仕組みのため、AngularJSのインジェクションの仕組みを利用したテストもしやすかったこと。
5.サーバーサイドのURLをRESTで設計できる場合、AngularJSのRESTクライアントを利用できる。
AngularJSには、サーバーへリクエストを送るAPIとして$httpライブラリーが用意されています。 これを使ってデータのリスト、登録、更新、削除などのHTTPクライアントを作るのは少し手間がかかります。 もしサーバー側のAPIをRESTで設計出来る場合、AngularJSのREST用クライアントの$resourceライブラリが便利です。HTTPクライアントを簡単に実装できます。
$resourceの例
idとtextというふたつのパラメータを持つNoteを管理するURLがこのような場合の例をみてみます。
URL | HTTPメソッド | 機能 |
---|---|---|
/api/notes/ | GET | リソースの一覧を取得 |
/api/notes/:id | GET | リソースを取得 |
/api/notes/ | POST | 新規登録する |
/api/notes/:id | PUT | 更新する |
/api/notes/:id | DELETE | 削除する |
$resourceを使ったRESTクライアントの定義
(function (angular) { angular.module('myApp.note').factory('Note',Note); function Note ($resource) { return $resource( '/api/notes/:id', {id: '@id'}, {'update': { method:'PUT' } } ); } })(angular);
$httpを使ったRESTクライアントの定義
$resourceを使わずに、$httpでRESTクライアントを作ることもできますが、 GET、PUT、POST、DELETEのメソッドを$httpを利用して自分で実装する必要があります。
(function (angular) { angular.module('myApp.note').factory('Note',Note); function Note ($q, $http) { var Note = function(params) { this.id = params.id; this.text = params.text; this.query = function() { var defer = $q.defer(); $http.get('/api/notes/'). success(function(result) { defer.resolve(result); }). error(function(result) { defer.reject(result);}); return defer.promise; }; this.save = function () { var defer = $q.defer(); $http.post('/api/notes/',? {id:this.id, text:this.text}). success(function(result) { defer.resolve(result); }). error(function(result) { defer.reject(result);}); return defer.promise; }; this.update = function () { var defer = $q.defer(); $http.put('/api/notes/' + this.id , {id:this.id, text:this.text}). success(function(result) { defer.resolve(result); }). error(function(result) { defer.reject(result);}); return defer.promise; }; this.delete = function () { var defer = $q.defer(); $http.delete('/api/notes/' + this.id). success(function(result) { defer.resolve(result); }). error(function(result) { defer.reject(result);}); return defer.promise; } } Note.query = function() { var defer = $q.defer(); $http.get('/api/notes/').success(function(result) { var ret = result.map(function(r) { return new Note({id:r.id, text:r.text}); }) defer.resolve(ret); }).error(function(result) { defer.reject(result); }); return defer.promise; }; return Note; } })(angular);
$resourceで定義したRESTクライアントの利用例
Controllerでの$resourceの簡単な利用例です。
//MainController.js function MainController($scope, $state, Note, NoteSharedData) { /* 一覧 */ Note.query().$promise.then(function(notes) { data.notes = notes; }); /* 登録 */ this.add = function () { note.$save().then(function(res) { /* リクエスト完了後の処理 */ }); } /* 更新 */ this.update = function (note) { note.$update().then(function(res) { /* リクエスト完了後の処理 */ }); } /* 削除 */ this.delete = function (note) { note.$delete().then(function(res) { /* リクエスト完了後の処理 */ }); } }
参考
https://docs.angularjs.org/api/ngResource/service/$resource
6.Ajaxリクエストの共通処理はインターセプターで実装する
$http、$resourceの両方の対してAjaxのリクエスト前、レスポンス取得時の共通処理を実装出来ます。下記のような処理をしたい場合に有効です。
- 全てのリクエストに共通パラメータを付与したい場合
- レスポンスの共通的なエラーハンドリング
- リクエストやレスポンスの内容をブラウザのコンソールログに出力したい
インターセプターの作成
(function (angular) { 'use strict'; angular.module('myApp.note').factory('HttpInterceptor', HttpInterceptor); function HttpInterceptor($q, $log, $rootScope) { /** * エラーハンドリング */ function handleError(rejection){ $log.error("HttpInterceptor.errorhundler", rejection.status, rejection); if (rejection.status == 400) { /* error handling */ return; } if (rejection.status == 500) { /* error handling */ return; } } var service = { // リクエスト前 'request': function(config) { $log.debug('HttpInterceptor.request:',config); return config || $q.when(config); }, // レスポンス success 'response': function(response) { var headers = response.headers(); $log.debug("HttpInterceptor.response:", response, "header:", response.headers()); return response || $q.when(response); }, // レスポンス error 'responseError': function(rejection) { var headers = rejection.headers(); $log.debug("HttpInterceptor.responseError:", rejection, "status:",rejection.status, "headers:",rejection.headers(), "config:",rejection.config); handleError(rejection); return $q.reject(rejection); } }; return service; } })(angular);
作成したインターセプターの登録
(function(angular) { 'use strict'; angular.module('myApp.note', ['ui.router', 'ngResource']).config(Config); function Config ($httpProvider, $stateProvider) { //HTTPリクエストの共通処理の設定 $httpProvider.interceptors.push('HttpInterceptor'); })(angular);
まとめ
AngularJSは機能が多い分、どれをどのように使っていくか、、という点で悩みました。 現在は色々な参考開発スタイルガイドが出ていますので、自分のチームに合うものを選択されるのが良いと思います。
これからの利用にあたってはAngular2を意識した実装を考える例が多くでてきます。 こちらの公式ブログの記事のようにAngular1から2へ移行しやすい機能をそろえていくようですので、現時点ではAngularJSのガイドラインに従った開発が良いのではないかと思います。
「Angular 1 and Angular 2 integration: the path to seamless upgrade」
http://angularjs.blogspot.jp/2015/08/angular-1-and-angular-2-coexistence.html?view=classic
「AngularJS Style Guide」
https://github.com/johnpapa/angular-styleguide
Angular2のリリースはまだしばらく先になりそうですが、一時期バッシングされたところからどのように変わっていくのか、、という点が楽しみです。
Angular2への移行機能などをまた試して紹介していければと思います。
以上、UX開発部の樋代でした。
参考
こちらの記事を書く際に参考にさせていただきました。
「AngularJS blog」
http://angularjs.blogspot.jp/
「AngularJS Advent Calendar 2014」
http://qiita.com/advent-calendar/2014/angularjs
「AngularJS モダンプラクティス」
http://qiita.com/armorik83/items/5542daed0c408cb9f605
です。こちらも試してみると面白いと思います。
<連載記事>
- AngularJS開発TIPS(前編)
- AngularJS開発TIPS(後編)