Works by

Ren's blog

@rennnosuke_rk 技術ブログです

【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;
  });
}]);
実行結果

codepen.io

解説

まず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です。 broadcastServicesharedValueと呼ばれる状態変数を持っており、この値はbroadcastService内で管理されます。

changeValueメソッドはこの状態変数を更新します。同時に、「状態変数を更新した」というイベントの通知を、$rootScope.$broadcastを使って文字通りブロードキャストします。

assignEventHandlerchangeValueが発行した通知をキャッチし、任意の処理を実行するイベントハンドラを設定します。イベントハンドラ$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コンポーネントやモジュールを監視し続けるということをしなくても良いので、より疎結合なコード設計にしやすくできると考えられます。

参考

AngularJS

www.muicss.com

【React】ReactとVirtual DOM

React

f:id:rennnosukesann:20180515230713p:plain:w300

ReactはFacebook社が開発するWebUIに特化したJavascriptのライブラリです。WebアプリケーションのアーキテクチャMVCで構成した場合、ReactがサポートするのはViewの部分になります。Reactを用いることで効率的にUIコンポーネントを描画したり、UIコンポーネントを管理する負担を軽減することができます。

Virtual DOM

Reactでは、Virtual DOMと呼ばれる仕組みよって効率的なUIコンポーネントの描画を行います。通常、JavaScriptではDOMによってHTML要素にアクセスし、変更を加えます。

f:id:rennnosukesann:20180516002415p:plain:w300

DOMが操作されると、ブラウザ上でレンダリング(の一部の処理)が実行されます。
ブラウザのレンダリングは以下のフローで行われるのですが、DOM操作によって「Layout」と「Paint」工程の処理が走ります。

f:id:rennnosukesann:20180516002458p:plain:w300

DOM操作を行うたびに処理が走るので、頻繁にDOM操作が実行されるとブラウザの描画パフォーマンスは悪くなってしまいます。

そこでVirtual DOMを利用します。Virtual DOMは実際のDOMとは別物であるものの、同じ構造を持ったオブジェクトです。ReactによるDOM操作が発生すると、DOMの代わりにVirtual DOMが操作されます。

f:id:rennnosukesann:20180516003214p:plain

その後、操作されたVirtual DOMの変更差分を算出して、変更部分に対応する実際のDOMを変化させます。*1これにより、DOM操作回数を通常よりも抑えることができ、上記レンダリング処理を何度も走らせない仕組みになっています。


ちなみに、Virtual DOM自体はReact固有の技術ではなく、Viewレンダリングを最適化する目的で他のライブラリ等でも実装されているみたいです。

github.com

参考

reactjs.org

Document Object Model - Wikipedia

www.shoeisha.co.jp

*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));
    }
}

参考文献

Base64.Encoder (Java Platform SE 8)

【Java】Jacksonを使ってJSON文字列←→オブジェクト変換を行う

Jacksonとは

github.com

JacksonJavaで使える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プロパティはすべて変換後のクラスメンバに存在しなければならない

という制約をクリアしている必要があります。
これらの制約に反していた場合には例外がスローされます。

具体的な注意点は、こちらの方の記事でとてもわかり易くまとめてあったので引用させていただきます。

qiita.com

【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認証におけるリダイレクト

GoogleFacebookTwitterOAuth認証APIを利用して各SNSで認可を行う際、自身が管理するアプリのサーバにリダイレクトさせて認証状況を確認したい場合があります。

このとき、各SNSOAuth認証APIを叩く前にリダイレクトしてもらうURLを設定します。

# リダイレクトURL
# APIサーバが提供するパスなど
https://hoge.net/oauth/callout/google

もし、リダイレクト前の状態をリダイレクト先に渡したい場合、
リクエストパラメータをクエリストリングに追加したい・・・等考えると思いますが、一部のSNSはリダイレクトURLにパラメータを直接指定すると404を返してきます。

# NG(404が返る)
https://hoge.net/oauth/callout/google?param=hoge

f:id:rennnosukesann:20180511140026p:plain

どうやら、リダイレクト先にパラメータを渡すためにはちょっとした工夫が必要なようです。

リダイレクト先にパラメータを渡す方法: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に対する認証リクエストを投げるのに便利です。

github.com

リダイレクト先にパラメータを渡す方法: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にパラメータが挿入されません。
GoogleFacebookと異なるので注意したいところです。

参考

stackoverflow.com

stackoverflow.com

stackoverflow.com

【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!";  
}]);
結果

f:id:rennnosukesann:20180509221001p:plain

ng-cloak

このチラツキを防ぐために、ng-cloakディレクティブを使用します。
このディレクティブを指定したタグは、テンプレートにデータが挿入されるまで非表示状態になります。テンプレートにデータが挿入されたときタグは表示状態となり、未コンパイル状態のテンプレートが露出されずに済みます。

<div ng-app="myApp">
  <div ng-controller="MyAppCtrl" ng-cloak>
    {{message}}
  </div>
</div>
結果

f:id:rennnosukesann:20180509222125p:plain

内部では、画面読み込み時に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を付加するのが望ましいです。

参考文献

ngCloak | AngularJS 1.2 日本語リファレンス | js STUDIO

【AngularJS】$httpによる通信を任意のタイミングで中断する

$httpのタイムアウト

AngularJSではHTTPリクエストを投げるためのサービス$httpが提供されていますが、
引数configのプロパティtimeoutを設定することで、HTTPリクエストをタイムアウトさせることができます。

timeout – {number|Promise} – timeout in milliseconds, or promise that should abort the request when resolved.

AngularJS: API: $httpより引用

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);

  };
 
}]);