HTML5テクニカルノート
AngularJS: コントローラから切り分けたサービスでサーバーのデータを読み込む
- ID: FN1604001
- Technique: HTML5 / JavaScript
- Library: AngularJS 1.5.3
AngularJSでは、使い回しできる機能(プロパティやメソッド)が備わったモジュールをサービスと呼んでいます。AngularJSにあらかじめ備わったサービスだけでなく、独自のサービスを定めて利用することもできます。本稿は、為替レートにもとづく簡単な計算書を作例として、サービスの使い方やつくり方を解説します。
01 数量と単価から合計額を求める
AngularJSを使う準備として、<head>
要素につぎのようなCSSやJavaScriptのファイルを読み込んでおきます。詳しくは、「AngularJS入門 01: AngularJSを始める」01「AngularJSのダウンロードと設定」をお読みください。
<!-- Latest compiled and minified CSS --> <link rel="stylesheet" href="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/css/bootstrap.min.css" integrity="sha384-1q8mTJOASx8j1Au+a5WDVnPi2lkFfwwEAa8hDDdjZlpLegxhjVME1fgjWPGmkzs7" crossorigin="anonymous"> <script src="https://ajax.googleapis.com/ajax/libs/jquery/1.12.2/jquery.min.js"></script> <!-- Latest compiled and minified JavaScript --> <script src="https://maxcdn.bootstrapcdn.com/bootstrap/3.3.6/js/bootstrap.min.js" integrity="sha384-0mSbJDEHialfmuBBQP6A4Qrprq5OVfW37PRR3j5ELqxss1yVqOtnepnHVP9aJ7xS" crossorigin="anonymous"></script> <script src="https://ajax.googleapis.com/ajax/libs/angularjs/1.5.3/angular.min.js"></script>
まずは、入力された数量と単価をデータバインディングにより掛け合わせて、合計額を示しましょう(図001)。以下のコード1のとおり、項目の要素すべてを含む<div>
要素にはngApp
ディレクティブを与えます。そして、数量と単価の<input>
要素(type
属性"number")に、それぞれngModel
ディレクティブ("qty"と"cost")を定めました。それらの値を二重波括弧{{ }}
で参照して掛け合わせれば、合計額が導かれて表示されます(「AngularJS入門 01: AngularJSを始める」03「AngularJSでデータバインディングする」参照)。
なお、{{ }}
の構文は、つぎのように式の値に|
でフィルタが加えられます。currency
は通貨の表記となり、小数点以下2桁の数値にドル記号($)が添えられます。また、ngInit
ディレクティブは、与えられた式を初めに評価します。ここでは、初期値の定めに用いました。
{{ 式 | フィルタ}}
図001■数量と単価の合計がドル表記で示される
コード001■入力された数量と単価からデータバインディングで合計を示す
<div class="container" ng-app ng-init="qty=1;cost=2">
<h3>請求書</h3>
<div>
<label>数量</label>
<input type="number" min="0" ng-model="qty">
</div>
<div>
<label>単価</label>
<input type="number" min="0" ng-model="cost">
</div>
<div>
<strong>合計</strong>
{{qty * cost | currency}}
</div>
</div>
02 コントローラで通貨に応じた合計金額を計算する
つぎに、JavaScriptコードでコントローラを定めて、合計金額を求めるだけでなく、通貨が複数選べるようにします。<body>
要素のコードは、以下の抜き書きのように手直ししましょう。ディレクティブngApp
と新たに加えたngController
には、後にJavaScriptコードで定める名前("invoiceApp"と"InvoiceController")をそれぞれ与えます(「AngularJS入門 03: コントローラで値を与える」参照)。そして、ngModel
ディレクティブに与える値は、コントローラのオブジェクト(invoice)から得ることにしました。したがって、初期値を与えるngInit
ディレクティブは要らなくなります。
また、新たに加えた<select>
要素に含める<option>
要素のテキストも、コントローラから得る配列にもとづいてngRepeat
ディレクティブで与えます(「AngularJS入門 04: 動的にリストをつくる」03「モジュールとコントローラをHTMLドキュメントに定める」) 。さらに、合計額も同じくngRepeat
ディレクティブで通貨ごとに、コントローラのメソッド(total())で求めました。フィルタのcurrency
にコロン(:
)で加えた値は通貨記号として数値に添えられます。そして、button
要素にはngClick
ディレクティブで、クリックされたときに呼び出すコントローラのメソッド(pay())を定めました(「AngularJS入門 07: 項目を調べて削除する」03「チェックのついた項目を要素のクリックで削除する」参照)
<!--<div class="container" ng-app ng-init="qty=1;cost=2">--> <div class="container" ng-app="invoiceApp" ng-controller="InvoiceController as invoice"> <label>数量</label> <!--<input type="number" min="0" ng-model="qty">--> <input type="number" min="0" ng-model="invoice.qty" required> <label>単価</label> <!--<input type="number" min="0" ng-model="cost">--> <input type="number" min="0" ng-model="invoice.cost" required> <select ng-model="invoice.currencyIn"> <option ng-repeat="currency in invoice.currencies">{{currency}}</option> </select> <strong>合計</strong> <span ng-repeat="currency in invoice.currencies"> <!--{{qty * cost | currency}}--> {{invoice.total(currency) | currency:currency}} </span> <button class="btn btn-primary btn-sm" ng-click="invoice.pay()">支払</button> </div>
コントローラは以下のコード002のように、<script>
要素に加えました。angular.module()
関数でモジュール(invoiceApp)をつくり、戻り値のオブジェクトにcontroller()
メソッドでコントローラ(InvoiceController)を定めます。メソッド第2引数の関数本体でthis
参照(変数invoice)に与えたプロパティやメソッドがコントローラから呼び出せます。これで通貨を選ぶと、それぞれの為替レートに応じた合計額が計算されます(図002)。なお、コード002には<body>
要素の記述も併せてまとめました。
図002■通貨を選ぶとそれぞれの為替レートに応じた合計が計算される
コード002■選んだ通貨の為替レートに応じてそれぞれの合計を計算する
<script>
angular.module('invoiceApp', [])
.controller('InvoiceController', function() {
var invoice = this;
invoice.qty = 1;
invoice.cost = 2;
invoice.currencies = ['USD', 'EUR', 'JPY'];
invoice.currencyIn = invoice.currencies[1];
invoice.usdToForeignRates = {
USD: 1,
EUR: 0.88,
JPY: 111.48
};
invoice.total = function(currencyOut) {
return invoice.convertCurrency(invoice.qty * invoice.cost, invoice.currencyIn, currencyOut);
};
invoice.convertCurrency = function(amount, currencyIn, currencyOut) {
var usdToForeignRates = invoice.usdToForeignRates;
return amount * usdToForeignRates[currencyOut] / usdToForeignRates[currencyIn];
};
invoice.pay = function() {
window.alert("ありがとうございます");
};
});
</script>
<div class="container" ng-app="invoiceApp" ng-controller="InvoiceController as invoice">
<h3>請求書</h3>
<div>
<label>数量</label>
<input type="number" min="0" ng-model="invoice.qty" required>
</div>
<div>
<label>単価</label>
<input type="number" min="0" ng-model="invoice.cost" required>
<select ng-model="invoice.currencyIn">
<option ng-repeat="currency in invoice.currencies">{{currency}}</option>
</select>
</div>
<div>
<strong>合計</strong>
<span ng-repeat="currency in invoice.currencies">
{{invoice.total(currency) | currency:currency}}
</span>
<button class="btn btn-primary btn-sm" ng-click="invoice.pay()">支払</button>
</div>
</div>
03 コントローラからサービスを切り分ける
コードが増えてきたり、使い回しのできる処理が加わったときには、JavaScriptコードを分けると生産性が高まります。それだけでなく、メンテナンスもしやすく、機能の拡張にも柔軟に対応できるでしょう。AngularJSでは、プロパティやメソッドが使い回しできるように切り分けられたモジュールをサービスと呼びます(「Developer Guide」「Services」の項参照)。とくに、AngularJSのコントローラはおもな役割がページへのデータの表示(ビュー)なので、データの処理に関わるコードはサービスに分けると見とおしがよくなります。
今回の作例ではコントローラのコードは少ないものの、為替レートにもとづく合計額の計算をサービスとして切り分けてみることにしましょう。まず、angular.module()
関数の第2引数に渡す配列には、つぎのようにサービスのモジュール名(finance)を加えます。つぎに、controller()
メソッドも第2引数を配列とし、モジュールから用いるサービスの名前(currencyConverter)をエレメントに納めます。すると、コントローラのコンストラクタはそのサービスのオブジェクトを引数に受け取れるのです。したがって、コントローラの中でサービスが使えます。そこで、これから定めるサービスのプロパティやメソッドで、コントローラの処理を先に書き替えました。
<script src="js/finance.js"></script> <script> // angular.module('invoiceApp', []) angular.module('invoiceApp', ['finance']) // .controller('InvoiceController', function() { .controller('InvoiceController', ['currencyConverter', function(currencyConverter) { var invoice = this; invoice.currencies = currencyConverter.currencies; // ['USD', 'EUR', 'JPY']; /*invoice.usdToForeignRates = { USD: 1, EUR: 0.88, JPY: 111.48 };*/ invoice.total = function(currencyOut) { // return invoice.convertCurrency(invoice.qty * invoice.cost, invoice.currencyIn, currencyOut); return currencyConverter.convert(invoice.qty * invoice.cost, invoice.currencyIn, currencyOut); }; /*invoice.convertCurrency = function(amount, currencyIn, currencyOut) { var usdToForeignRates = invoice.usdToForeignRates; return amount * usdToForeignRates[currencyOut] / usdToForeignRates[currencyIn]; };*/ // }); }]); </script>
サービスは使い回す想定です。したがって、JavaScript(JS)ファイル(finance.js)に以下のコード003のように定め、HTMLドキュメントの<head>
要素に読み込みました。サービスはモジュール(finance)にfactory()
メソッドで加えます。メソッドの第2引数は、利用するプロパティやメソッドが備わったオブジェクトを返す関数です。戻り値のオブジェクトに納めたプロパティ(currencies)とメソッド(convert())が、サービスから使えるようになります。サービスを切り分けただけですので、処理そのものは前傾コード002と異なりません。
factory(サービス名, サービス関数)
コード003■通貨ごとの為替にもとづく計算をサービスに切り分けた
<script src="js/finance.js"></script>
<script>
angular.module('invoiceApp', ['finance'])
.controller('InvoiceController', ['currencyConverter',
function(currencyConverter) {
var invoice = this;
invoice.qty = 1;
invoice.cost = 2;
invoice.currencies = currencyConverter.currencies;
invoice.currencyIn = invoice.currencies[1];
invoice.total = function(currencyOut) {
return currencyConverter.convert(invoice.qty * invoice.cost, invoice.currencyIn, currencyOut);
};
invoice.pay = function() {
window.alert("ありがとうございます");
};
}]);
</script>
// finance.js
angular.module('finance', [])
.factory('currencyConverter', function() {
var currencies = ['USD', 'EUR', 'JPY'];
var usdToForeignRates = {
USD: 1,
EUR: 0.88,
JPY: 111.48
};
var convert = function(amount, currencyIn, currencyOut) {
return amount * usdToForeignRates[currencyOut] / usdToForeignRates[currencyIn];
};
return {
currencies: currencies,
convert: convert
};
});
04 サービスの機能を高める
処理をサービスに切り分けると使い回せて効率が上がるだけでなく、機能を加えたり高めたりすることもできます。とくに参照されるプロパティやメソッド(API)を変えなければ、使う側とは切り離して改善が加えられます。そこで、前掲コード003のサービス(currencyConverter)では決め打ちになっていた為替レートを、Webからオンラインで読み込むようにしてみましょう。今回使うのは、Yahoo Query Language (YQL)です。[Console]につぎのように入力すると、今の円/ドルレートのデータが得られます(図003)。
select * from yahoo.finance.xchange where pair in ("USDJPY")
図003■Yahoo Query Languageで得た円/ドルレートのデータ
YQLの"pair in"に("USDJPY","USDEUR")のように複数の通貨の組み合わせを与えれば、それぞれの為替レートが得られます。画面の下に示されるのが、データを得るためのURLです。受け取る形式をJSONにすると、それぞれの為替レートはrate
プロパティに配列で納められます。サービスがこのAPIから為替レートを読み込むように書き替えても、コントローラには何も手を加えずに済みます。ただ今回は、前掲コード003と比べやすいように、サービスのモジュール(financeService)とJavaScriptファイル(financeService.js)の名前を変えておきます。
<script src="js/financeService.js"></script> <script> // angular.module('invoiceApp', ['finance']) angular.module('invoiceApp', ['financeService']) .controller('InvoiceController', ['currencyConverter', function(currencyConverter) { }]); </script>
HTTPサーバーからJSONデータを読み込むには、AngularJSに備わっているサービス
$http
サービスを用います。以下に抜き書きしたコードのように、factory()
メソッドの第2引数を配列にすれば、エレメントに加えた$http
サービスが関数の引数として受け取れます。すると、$http.jsonp()
メソッドにURLを渡してJSONデータが読み込めるのです。URLはYQLで得たパスを変数(YAHOO_FINANCE_URL_PATTERN)に納めておきました。ただし、通貨の組み合わせは仮の文字列("PAIRS")になっています。
factory()
メソッドで定めた関数には、初期化の関数(refresh())を加えました。その本体でURLの仮の通貨の文字列は、String.replace()
メソッドにより通貨の文字列をエレメントとした配列(currencies)にもとづいてYQLの通貨の組み合わせに書き替えられます。$http.jsonp()
メソッドがデータを正しく受け取ると、戻り値に定めたthen()
メソッドが呼び出され、引数の関数に結果(response)が渡されます。前述のとおり、通貨ごとの為替レートの情報はJOSNデータのrate
プロパティから配列として取り出せます。そこで、angular.forEach()
メソッドにより、通貨と為替レートの値を得て変数(usdToForeignRates)のオブジェクトにプロパティとして納めました。// financeService.js // angular.module('finance', []) angular.module('financeService', []) // .factory('currencyConverter', function() { .factory('currencyConverter', ['$http', function($http) { var YAHOO_FINANCE_URL_PATTERN = '//query.yahooapis.com/v1/public/yql?q=select * from ' + 'yahoo.finance.xchange where pair in ("PAIRS")&format=json&' + 'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK'; var currencies = ['USD', 'EUR', 'JPY']; var usdToForeignRates = { /*USD: 1, EUR: 0.88, JPY: 111.48*/ }; var refresh = function() { var url = YAHOO_FINANCE_URL_PATTERN .replace('PAIRS', 'USD' + currencies.join('","USD')); return $http.jsonp(url).then(function(response) { var newUsdToForeignRates = {}; angular.forEach(response.data.query.results.rate, function(rate) { var currency = rate.id.substring(3,6); newUsdToForeignRates[currency] = window.parseFloat(rate.Rate); }); usdToForeignRates = newUsdToForeignRates; }); }; refresh(); // }); }]);
これで、最新の為替レートにもとづいて合計金額が計算されます。切り分けたサービス(currencyConverter)の変数の値を変えただけですから、コントローラ(InvoiceController)には何も影響はありません。実際の動きを確かめられるように、後掲サンプル001にファイルをフレームで加えてました(フレームのソースでコードが確かめられます)。なお、為替レートは初期化の関数で得ていますので、値を更新するにはページの再読み込みが必要です。
コード004■Webからオンラインで読み込んだ為替レートにもとづいて合計金額を計算する
angular.module('invoiceApp', ['financeService'])
.controller('InvoiceController', ['currencyConverter',
function(currencyConverter) {
var invoice = this;
invoice.qty = 1;
invoice.cost = 2;
invoice.currencies = currencyConverter.currencies;
invoice.currencyIn = invoice.currencies[1];;
invoice.total = function(currencyOut) {
return currencyConverter.convert(invoice.qty * invoice.cost, invoice.currencyIn, currencyOut);
};
invoice.pay = function() {
window.alert("ありがとうございます");
};
}]);
angular.module('financeService', [])
.factory('currencyConverter', ['$http',
function($http) {
var YAHOO_FINANCE_URL_PATTERN =
'//query.yahooapis.com/v1/public/yql?q=select * from ' +
'yahoo.finance.xchange where pair in ("PAIRS")&format=json&' +
'env=store://datatables.org/alltableswithkeys&callback=JSON_CALLBACK';
var currencies = ['USD', 'EUR', 'JPY'];
var usdToForeignRates = {};
var convert = function(amount, currencyIn, currencyOut) {
return amount * usdToForeignRates[currencyOut] / usdToForeignRates[currencyIn];
};
var refresh = function() {
var url = YAHOO_FINANCE_URL_PATTERN
.replace('PAIRS', 'USD' + currencies.join('","USD'));
return $http.jsonp(url).then(function(response) {
var newUsdToForeignRates = {};
angular.forEach(response.data.query.results.rate, function(rate) {
var currency = rate.id.substring(3,6);
newUsdToForeignRates[currency] = window.parseFloat(rate.Rate);
});
usdToForeignRates = newUsdToForeignRates;
});
};
refresh();
return {
currencies: currencies,
convert: convert
};
}]);
サンプル001■Webからオンラインで読み込んだ為替レートにもとづいて合計金額を計算する
作成者: 野中文雄
作成日: 2016年4月6日
Copyright © 2001-2016 Fumio Nonaka. All rights reserved.