【AngularJS】AngularJSで任意のイベント通知を送信・ハンドリングする
AngularJSのイベントハンドリング
JavaScriptでは多くのイベントをハンドリングする手段がありますが、AngularJSも独自にイベントハンドリングするための機能を備えています。 例えば、AngularJSでのスクロールイベントのハンドリングは以下のように行います。
// Controller angular.module('myApp') .controller('MyController', ['$scope', function($scope){ $scope.$on('scroll', function($event, args){ // イベントハンドリング }); }]);
JS標準やJQueryなどと同様、イベント通知を受け取ったときに走らせるハンドラ関数を登録できます。
ところで、AngularJSではscroll
などの規定のイベントだけではなく、任意のイベント通知を送信することができます。
この仕組みを用いることで、例えば「特定のリスト項目が押されたとき」などのイベントを不特定多数のハンドラに通知することができます。
$rootScope.$broadcast
AngularJSでは$rootScope.$broadcast
サービスを使うことでイベントの通知を行うことが
できます。
以下のコードは、あるコントローラCtrlA
が管理するDOM領域内のボタンが押すとイベント通知が発生し、別のコントローラCtrlB
がそれをハンドリングする・・・という例を表しています。
HTML
<div ng-app="myApp"> <div class="ctrl1 mui-panel" ng-controller="Ctrl_A"> <h3>Controller1</h3> value : <input id="input" type="text" ng-model="value"/> <br> <input type="button" value="broadcast" class="mui-btn mui-btn--raised mui-btn--accent" ng-click="onButtonClicked()" /> </div> <div class="ctrl2 mui-panel" ng-controller="Ctrl_B"> <h3>Controller2</h3> value : {{value}} <br> </div> </div>
JS
let mod = angular.module('myApp',[]); // 状態変数を管理し、イベント通知も行うService mod.factory('broadcastService', [ '$rootScope', function($rootScope) { // 状態変数 let sharedValue = null; // イベントラベル const VALUE_CHANGED = 'VALUE_CHANGED'; // 状態変化+イベントを通知 const changeValue = function broadCast(newValue) { sharedValue = newValue; $rootScope.$broadcast(VALUE_CHANGED, newValue); }; // イベントハンドラ登録 const assignEventHandler = function assignEventHandler($scope, handler) { $scope.$on(VALUE_CHANGED, function(event, newValue) { handler(newValue); }); }; return { changeValue, getValue, assignEventHandler }; }]); // コントローラA // ボタンをクリックすると、broadcastServiceの持つ状態変数にフォーム値をセットする // 同時に状態変数が変化したことに対する通知処理が走る mod.controller('Ctrl_A', ['$scope', 'broadcastService' , function($scope, broadcastService){ $scope.onButtonClicked = function() { broadcastService.changeValue($scope.value); } }]); // コントローラB // 「broadcastServiceの持つ状態変数が変化する」というイベントに対するハンドラを登録 mod.controller('Ctrl_B', ['$scope', 'broadcastService', function($scope, broadcastService){ broadcastService.assignEventHandler($scope, function(newValue){ $scope.value = newValue; }); }]);
実行結果
解説
まずHTML側を見ていきます。
HTML上ではng-controller
が修飾された箇所が2つ存在し、これらの領域は別々のコントローラによって管理されています。そのため、値value
を参照する箇所が2つありますが、参照先は両者で異なります。例えば、Ctrl_A
配下のinput
フォームに値を入力しても、Ctrl_B
配下の{{value}}
は変化しません。Ctrl_B
配下の{{value}}
が変化するのは、Ctrl_B
が持つ$scope.value
に値をセットしたときになります。
<div class="ctrl1 mui-panel" ng-controller="Ctrl_A"> <h3>Controller1</h3> value : <input id="input" type="text" ng-model="value"/> <br> <input type="button" value="broadcast" class="mui-btn mui-btn--raised mui-btn--accent" ng-click="onButtonClicked()" /> </div>
<div class="ctrl2 mui-panel" ng-controller="Ctrl_B"> <h3>Controller2</h3> value : {{value}} <br> </div>
Ctrl_A
では、HTML上のボタンに対してクリックリスナーを提供しています。
リスナーが発火すると、input
フォームに入力された値が後述するbroadcastSerice
の管理する値にセットされます。同時に、値が変更した旨を知らせる通知処理が実行されます。
// コントローラA // ボタンをクリックすると、`broadcastService`の持つ状態変数にフォーム値をセットする // 同時に状態変数が変化したことに対する通知処理が走る mod.controller('Ctrl_A', ['$scope', 'broadcastService' , function($scope, broadcastService){ $scope.onButtonClicked = function() { broadcastService.changeValue($scope.value); } }]);
Ctrl_B
ではbroadcastService
の持つ状態変数が変化したことを監視するイベントハンドラを設定しています。
状態変数が変化したとき、すなわちCtrl_A
のクリックリスナーが走ったときに、$scope.value
に値がセットされてブラウザ上に表示されます。
// コントローラB // 「broadcastServiceの持つ状態変数が変化する」というイベントに対するハンドラを登録 mod.controller('Ctrl_B', ['$scope', 'broadcastService', function($scope, broadcastService){ broadcastService.assignEventHandler($scope, function(newValue){ $scope.value = newValue; }); }]);
最後にbroadcastService
です。
broadcastService
はsharedValue
と呼ばれる状態変数を持っており、この値はbroadcastService
内で管理されます。
changeValue
メソッドはこの状態変数を更新します。同時に、「状態変数を更新した」というイベントの通知を、$rootScope.$broadcast
を使って文字通りブロードキャストします。
assignEventHandler
はchangeValue
が発行した通知をキャッチし、任意の処理を実行するイベントハンドラを設定します。イベントハンドラは$scope.$on
で設定できます。また、実行する処理は予めassignEventHandler
の呼び出し側から設定します。
// 状態変数 let sharedValue = null; // ... // 状態変化+イベントを通知 const changeValue = function broadCast(newValue) { sharedValue = newValue; $rootScope.$broadcast(VALUE_CHANGED, newValue); }; // イベントハンドラ登録 const assignEventHandler = function assignEventHandler($scope, handler) { $scope.$on(VALUE_CHANGED, function(event, newValue) { handler(newValue); }); };
このイベント通知の仕組みによって、任意のイベントを発行できることが確認できました。
また、$scope.$on
によるイベントハンドリングでは特定のUIコンポーネントやモジュールを監視し続けるということをしなくても良いので、より疎結合なコード設計にしやすくできると考えられます。
参考
【React】ReactとVirtual DOM
React
ReactはFacebook社が開発するWebUIに特化したJavascriptのライブラリです。WebアプリケーションのアーキテクチャをMVCで構成した場合、ReactがサポートするのはViewの部分になります。Reactを用いることで効率的にUIコンポーネントを描画したり、UIコンポーネントを管理する負担を軽減することができます。
Virtual DOM
Reactでは、Virtual DOMと呼ばれる仕組みよって効率的なUIコンポーネントの描画を行います。通常、JavaScriptではDOMによってHTML要素にアクセスし、変更を加えます。
DOMが操作されると、ブラウザ上でレンダリング(の一部の処理)が実行されます。
ブラウザのレンダリングは以下のフローで行われるのですが、DOM操作によって「Layout」と「Paint」工程の処理が走ります。
DOM操作を行うたびに処理が走るので、頻繁にDOM操作が実行されるとブラウザの描画パフォーマンスは悪くなってしまいます。
そこでVirtual DOMを利用します。Virtual DOMは実際のDOMとは別物であるものの、同じ構造を持ったオブジェクトです。ReactによるDOM操作が発生すると、DOMの代わりにVirtual DOMが操作されます。
その後、操作されたVirtual DOMの変更差分を算出して、変更部分に対応する実際のDOMを変化させます。*1これにより、DOM操作回数を通常よりも抑えることができ、上記レンダリング処理を何度も走らせない仕組みになっています。
ちなみに、Virtual DOM自体はReact固有の技術ではなく、Viewレンダリングを最適化する目的で他のライブラリ等でも実装されているみたいです。
参考
Document Object Model - Wikipedia
*1:どのタイミングでVirtual DOMの差分をDOMに反映させるかについてはまだ未調査です...
【Java】Java8で文字列をBase64変換する
java.util.Base64
Base64.Encoder (Java Platform SE 8)
Java8より、Base64によってバイトデータをエンコードできるBase64
クラスが実装されました。
このUtilityクラスを利用することで、Java上で容易にBase64エンコード/デコードが利用できます。
Usage
import java.util.Base64; public class Main { public static void main(String... args) { String message = "Hello, Base64!"; // Base64文字列へエンコード String base64 = Base64.getEncoder().encodeToString(message.getBytes()); // Base64文字列をデコード String decodedMessage = new String(Base64.getDecoder().decode(base64)); } }
参考文献
【Java】Jacksonを使ってJSON文字列←→オブジェクト変換を行う
Jacksonとは
JacksonはJavaで使えるJSONライブラリです。
JSON形式の文字列をJavaのオブジェクトに直接マッピングしたり、逆にJavaオブジェクトをJSON文字列に変換できたりします。
HTTPレスポンスをパースしたりオブジェクトをBase64エンコーディングする時などに便利です。
Usage
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; public class Main { public static class Obj { private String name; private int value; public Obj(){ } public Obj(String name, int value){ this.name = name; this.value = value; } } public static void main(String... args){ Obj obj = new Obj("apple", 100); String json = getJSONFromObj(obj); Obj newObj = getObjFromJSON(json); } // JavaオブジェクトをJSONに変換 private String getJSONFromObj(Obj obj) { String json = null; ObjectMapper mapper = new ObjectMapper(); try { json = mapper.writeValueAsString(obj); } catch (JsonProcessingException e) { e.printStackTrace(); } return json; } // JSONをJavaオブジェクトに変換 private Obj getObjFromJSON(String json) { ObjectMapper mapper = new ObjectMapper(); Obj obj = null; try { obj = mapper.readValue(json, Obj.class); } catch (Exception e) { e.printStackTrace(); } return obj; } }
パース時の注意
JacksonのObjectMapper
でデシリアライズする際のクラスは、
- 引数なしコンストラクタが定義されていなければならない
- インナークラスはstaticでなければならない(アウタークラスがインスタンス化されないとクラス定義がオンメモリにならないため)
- JSONプロパティはすべて変換後のクラスメンバに存在しなければならない
という制約をクリアしている必要があります。
これらの制約に反していた場合には例外がスローされます。
具体的な注意点は、こちらの方の記事でとてもわかり易くまとめてあったので引用させていただきます。
【OAuth】Google/Facebook/TwitterのOAuth認証APIに設定するリダイレクトURLにパラメータを含める
Google/Facebook/Twitter のOAuth API
Using OAuth 2.0 to Access Google APIs | Google Identity Platform | Google Developers
ウェブ - Facebookログイン - ドキュメンテーション - 開発者向けFacebook
Oauth with the Twitter API — Twitter Developers
SNS認証におけるリダイレクト
GoogleやFacebook、TwitterのOAuth認証用APIを利用して各SNSで認可を行う際、自身が管理するアプリのサーバにリダイレクトさせて認証状況を確認したい場合があります。
このとき、各SNSのOAuth認証用APIを叩く前にリダイレクトしてもらうURLを設定します。
# リダイレクトURL # APIサーバが提供するパスなど https://hoge.net/oauth/callout/google
もし、リダイレクト前の状態をリダイレクト先に渡したい場合、
リクエストパラメータをクエリストリングに追加したい・・・等考えると思いますが、一部のSNSはリダイレクトURLにパラメータを直接指定すると404を返してきます。
# NG(404が返る) https://hoge.net/oauth/callout/google?param=hoge
どうやら、リダイレクト先にパラメータを渡すためにはちょっとした工夫が必要なようです。
リダイレクト先にパラメータを渡す方法:Google
GoogleのOAuthAPIのリダイレクトURLには、直接パラメータを指定することができません。
かわりに、認証サーバへのリクエストURLであるAuthorizationURLにパラメータstate
を介してパラメータを渡すことができます。
Javaで実装する場合こんな感じ。
ServiceBuilder builder = new ServiceBuilder() .apiKey(apiKey) .apiSecret(apiSecret) .scope(scope) .state(state) // リダイレクトURLに含めたいパラメータを指定可能(文字列) .callback(callback) // リダイレクトURL .build(GoogleApi20.instance());
上記コードではScriveJavaというJavaのOAuthクライアントライブラリを使用しています。
OAuthに対する認証リクエストを投げるのに便利です。
リダイレクト先にパラメータを渡す方法:Facebook
Facebookも同様に、AuthorizationURLのパラメータstate
を指定することでリダイレクトURLパラメータを挿入します。
ServiceBuilder builder = new ServiceBuilder() .apiKey(apiKey) .apiSecret(apiSecret) .scope(scope) .state(state) // リダイレクトURLに含めたいパラメータを指定可能(文字列) .callback(callback) // リダイレクトURL .build(FacebookApi.instance());
Googleと大差ありませんね。
リダイレクト先にパラメータを渡す方法:Twitter
一方で、TwitterはリダイレクトURLへのパラメータ指定を許しているみたいです。
# NG(404が返る) String callback = "https://hoge.net/oauth/callout/google?param=hoge"; ServiceBuilder builder = new ServiceBuilder() .apiKey(apiKey) .apiSecret(apiSecret) .scope(scope) .callback(callback) // リダイレクトURL .build(TwitterApi.Authenticate.instance());
逆に、state
にパラメータを設定してもリダイレクトURLにパラメータが挿入されません。
GoogleやFacebookと異なるので注意したいところです。
参考
【AngularJS】未コンパイル状態のHTMLテンプレートが一瞬表示されるのを防ぐ
テンプレートのちらつき
AngularJSのHTMLテンプレートを用いたページを表示したときに、一瞬だけ元のテンプレート表記がちらついてしまう場合があります。
例えば、以下のようなコードでHTML上の{{message}}
に動的に文字列を挿入しようとしたとき、下の画像のように一瞬だけ元の文字列{{message}}
の状態で表示されてしまうことがあります。
<div ng-app="myApp"> <div ng-controller="MyAppCtrl"> {{message}} </div> </div>
let mod = angular.module('myApp',[]); mod.controller('MyAppCtrl', ['$scope', '$timeout', function($scope, $timeout){ $scope.message = "Hello!"; }]);
結果
ng-cloak
このチラツキを防ぐために、ng-cloak
ディレクティブを使用します。
このディレクティブを指定したタグは、テンプレートにデータが挿入されるまで非表示状態になります。テンプレートにデータが挿入されたときタグは表示状態となり、未コンパイル状態のテンプレートが露出されずに済みます。
<div ng-app="myApp"> <div ng-controller="MyAppCtrl" ng-cloak> {{message}} </div> </div>
結果
内部では、画面読み込み時にng-cloak
を指定したタグに対して下記のCSSルールが適用されています。そしてng-cloak
ディレクティブを指定したタグの内側に含むテンプレートがコンパイルされるとき、AngularJSはng-cloak
属性を破棄します。
[ng:cloak], [ng-cloak], [data-ng-cloak], [x-ng-cloak], .ng-cloak, .x-ng-cloak { display: none !important; }
この一連の操作によって、「テンプレートにデータが挿入されるまでタグを表示しない」処理を実現しています。
なおng-cloak
は指定したタグの子要素もすべて非表示にしてしまいます。そのため<body ng-cloak>
とするなど、指定の仕方によってはページの表示が遅延しているように見えてしまうため、ユーザ体験を悪化させる要因になりかねません。
ちらつく可能性のあるテンプレートが特定の領域に限られている場合には、その領域のタグに限定してng-cloak
を付加するのが望ましいです。
参考文献
【AngularJS】$httpによる通信を任意のタイミングで中断する
$httpのタイムアウト
AngularJSではHTTPリクエストを投げるためのサービス$http
が提供されていますが、
引数config
のプロパティtimeout
を設定することで、HTTPリクエストをタイムアウトさせることができます。
timeout – {number|Promise} – timeout in milliseconds, or promise that should abort the request when resolved.
timeout
には整数型とPromise型オブジェクトを設定することができます。
整数型を設定した場合には指定整数ミリ秒後にリクエストをタイムアウトします。
Promise型を設定した場合には、Promise生成元のDefered
オブジェクトに対してDefered.resolve()
がコールされたタイミングでリクエストがタイムアウトされます。
Usage
let mod = angular.module('myApp', []); mod.controller('MyAppCtrl', ['$scope', '$http', '$q', '$timeout', function($scope, $http, $q, $timeout){ $scope.onButtonClick = function onButtonClick() { let defered = $q.defer(); let success = function(response) { $scope.message = "success!"; $scope.response = response; }; let error = function(error) { $timeout(function(){ $scope.message = "error."; $scope.response = error; }); }; // 1) ミリ秒指定 $http({ method: "GET", url : "/hoge/fuga/api", timeout: 3000 }) .then(success) .catch(error); // 2) Promiseオブジェクトを指定 $http({ method: "GET", url : "/hoge/fuga/api", timeout: defered.promise }) .then(success) .catch(error); // Promiseオブジェクトをtimeoutに指定した場合、Defered.resolve()を呼ぶとタイムアウトする // 1) と同じ挙動 $timeout(function(){ defered.resolve(); }, 3000); }; }]);