会社でAngularJSをわっしわっし使ってみて、そろそろ某かの意見を言えるようになったのではないか?と思ったので、推薦記事を書いてみます。AngularJSマンセー!
一般のAngularJS解説記事はまぁ巷にそれなりに出回っていると思うので、僕の周りに蔓延る混沌Androidクラスタ向けの説明記事を書いてみようと思います。
というわけで、AngularJSをAndroid用語を駆使して説明します。正確さはあまり気にしません。
読者に要求されるもの
- Androidについての基本的な知識
- JavaScriptやHTMLなどに対する基本的な知識
参考
AngularJSってなに?
AngularJSはGoogleが作成しているWebアプリのクライアント側用フレームワークです。
遠い仲間としてBackbone.jsが、近い仲間としてKnockout.jsやFlightがあります。
(まぁわかめはこの中ではBackbone.jsしか使ったことがないんですが。)
近年のWebアプリ界隈の発展と注文の増加っぷりは目覚ましく、JavaScriptで実装する対象がどんどん拡大しているといえます。静的ページで対応できる要素なんてタカが知れているんだ…!
要するに、要求がリッチになってきているので、クライアント側も発展しないとつらぽよだということです。
そこでJavaScript用のフレームワークだ!フリーダムすぎるJavaScriptで徒手空拳とか全裸と絆創膏だけでシベリアに遊びに行くようなもんですよ。
なぜAngularJSを選ぶべきなのか?
サーバ側動的ページ生成とか古いですよ!
ぶっちゃけ、今更新規にアプリ作るのにJSPとか使って、何か操作するたびサーバ側でページ生成して…みたいな作り方することってないですよね。サーバはJSON吐く機械。ユーザに対する見た目(UI)を組み立て制御するのはJavaScriptでやるのが定番です。
JavaScriptで何かするにはフレームワークが欲しいですよ!
JavaScriptは大変自由に書くことのできる言語で一定のルールを設けずに書くとなかなかヒドイことになります。わかめがTG社に入社したばかりの約3年前は、高階関数っぽい書き方をした中二病感漂うヤツと、ちゃんとOOPっぽく書くヤツと、C言語のように構造体+関数で書くヤツが混在してたような記憶があります。それはもう酷い有様です。(俺がどれかはあえて述べない)
そこで、何らかのフレームワークを与え定まった書き方ができるのはメリットです。これはAngularJSに限った話ではないですが。
pushStateを自然に使うのが簡単になるよ!
githubでは、リポジトリ内で移動する時にはページを読みなおして移動するのではなく、必要な部分のみ更新していて、大変シームレスに移動することができます。なおかつ、ブラウザの戻るや進むもキチンと使えます。この機能をしっかり提供するためには、あるページが読み込まれた時に現在のURLから適切にページをJavaScriptで構築しなければなりません。これを自分で1から作るのは結構な苦労です。
AngularJSであれば、URLに対してどういう画面を表示するかを指定する簡単な方法が提供されています。
テストを書くのが楽だよ!
あとはAngularJSはテストが楽なのも好ポイントです。DI(Dependency Injection 依存性注入)をベースに色々な部品間でデータの受け渡しをしているので、テストの時にはMockを利用するようにするのも簡単です。このお陰でAjaxが絡んだテストもやりやすくなっています。
プログラムとDOMの分離が行いやすいよ!
さらに、通常のWebアプリではjQueryを使ってDOMをごしごし組み立てて追加して…といったように、見た目の操作にはプログラミングの知識が必須でした。ですが、AngularJSではDOMをプログラムからいじる場面が大変少なくなるので、プログラマの手を借りずにデザイナさんが独力でいじくれる範囲が他のフレームワークより広くなるのではないかと思います。想像ですが。
つまり
今までわかめが試した限りでは、画面内でのDOMの組み換えや動きなどの少ないアプリ、例えば業務系のアプリなどを手早く組み立てるのに向いているように思います。
そしてAngularJSを選ぶと苦労しそうな例
逆に、AngularJSを使うと苦労しそうなアプリはどういうアプリかを考えてみます。申し訳ないですが、まだ説明していない用語も使って書くのでこの記事を最後まで読み終わった後にもう一度読むとよくわかるかもしれません。
既存のアプリを後からAngularJS化するのはちょっと…
まず、既存のアプリをAngularJSに当てはめるのはあまりオススメできません。AngularJSに限らずですが、フレームワークというのはその世界観に100%入り浸った時に最大の恩恵が受けられるものです。なので、アプリを開発する時にAngularJSを導入する!と決めてしまうのが一番良いでしょう。無理矢理途中からAngularJSに置き換えた場合、controllerとDOMとの結合が強すぎて上手くテストできない!とかデータ通信周りのテストがあああぁ!とか$routeProvider
を使った振り分けがあぁぁぁ!みたいな問題が出てきそうです。
DOMをぐりぐりいじくり倒すようなアプリはちょっと…
また、AngularJSではservice
やcontroller
ではDOMをいじくるようなことはしてはいけない事になっています。なので、directive
を作成してDOMを操作するしかない…んですが、それがなかなかメンドクサイ。わかめは未だに作り方がピンと来ていません。逆に言うと、見た目や挙動に凝らなければDOM操作はかなり避けて作れる、ということでもあります。
そのため、例えばゲームのようなアプリや、DOMをぐりぐりいじくり回すようなアプリは苦手なのではないかと思います。
と、思ったら1000人対戦ボンバーマンのBombermineがどうやらAngularJS使ってるみたいなんですよね…ど、どういうことだってばよ…( ゚д゚)
AngularJSのコードを見てみる
HTML+AngularJSしてみる
サンプル を開くと自分で編集して試すことができます。jsFiddle便利なので、海外では結構流行っているみたいです。
まず最初のサンプルです。最初はHTMLを書くだけで出来る範囲で見てみましょう。
以下の抜き出しではjsFiddleと少し変えて、より普通のAngularJSっぽくしてあります。
<!DOCTYPE html>
<html ng-app>
<body>
<input type="text" ng-model="data" placeholder="名前?">
<hr>
{{data || 'vvakame'}} より愛をこめて
</body>
</html>
ng-app
属性が付いているものがAngularJSの管理下になります。これはHTML中に通常1回だけ出てきます。
通常、ng-
で始まる属性はAngularJSがデフォルトで用意している要素です。つまり、公式のAPIを見れば解説が載っています。ng-appについて知りたい場合はngAppで検索したほうが良いかもしれません。
次に出てくるのはng-model
です。ng-model
の中にはAngular Expressionsと呼ばれる式を書くことができます。概ねJavaScriptと似たような構文です。ここでは、data
という変数を参照しています。inputタグへの入力値がdata
に反映されるようになります。
その後にある{{}}の中もAngular Expressionsで、data || 'vvakame'
が評価され、data
の中身が表示されます。data
が空の場合は後者の'vvakame'
が表示されます。
レッスン!
試しに、"○○から××へ愛をこめて" の形式になるように遊んでみましょう。data
の部分をfromにし、もう一つtoのinputも増やしてみると上手くいきそうです。
HTML+JavaScript+AngularJSしてみる
2番目のサンプルです。ちょっとJavaScriptを書いてみます。controller
を導入してみます。
ここでは function Controller($scope)
という関数を定義し、HTML側でng-controller
に関数名を指定しています。引数の$scope
はAngularJSではよく見る名前の引数です。$scope
にセットした内容は、HTML側から参照することができます。変数のスコープまるまるそのものですね。
入れ子にした場合どうなるとか、$rootScope
と呼ばれる一番上位のスコープがあったりするのですが、この記事では割愛します。詳しくはこちらを参照してください。
AngularJSのDIは、名前に従ってDIされるので、$scope
以外の名前にすると上手く動かなくなってしまいます。どういう原理でDIをしているかというと、関数をtoStringすると関数のソースが手に入るので正規表現で無理矢理仮引数の名前を解析してDIしてます。無茶苦茶です。その場合、minifyすると引数の名前が変わってぶっ壊れちゃうんですが、そのための書き方もしっかり用意されています。しかし、めんどくさいので本記事では取り上げません。(というか、あんまり研究してない)
<div ng-app>
<div ng-controller="Controller">
<input type="text" ng-model="data" placeholder="名前?">
<button ng-click="reset()">消す</button>
<hr>
{{data}} より愛をこめて
</div>
</div>
function Controller($scope) {
$scope.data = "vvakame";
$scope.reset = function () {
$scope.data = "謎の誰か";
};
}
ng-controller
は、そのタグの配下を制御するcontroller
を指定してやります。controller
には関数の名前を指定します。Controller内部では、変数の初期設定や使える関数を定義しています。$scope.reset
ではdata
の値を書き換えています。そして、ng-click
の中ではその関数を呼んでいます。
AngularJSをAndroidで説明する
では、AngularJSで出てくる主要な仕組みや要素についてこれから説明していきます。
module = AndroidManifest.xml や library project 相当
AngularJSで何かをする時に、module
を作成することを避けて通ることはできません。
サンプルコードだとmodule
を作っていない場合も多いですが、module
はAndroidManifest.xmlやlibrary projectに相当する設定が書けるのでざくざく書いてみましょう。
angular.module("sample", [], function() {
alert("初期化!");
});
function Controller($scope) {
$scope.data = "vvakame";
$scope.reset = function () {
$scope.data = "謎の誰か";
};
}
<div ng-app="sample">
<div ng-controller="Controller">
<input type="text" ng-model="data" placeholder="名前?">
<button ng-click="reset()">消す</button>
<hr>
{{data}} より愛をこめて
</div>
</div>
このようにします。moduleを定義した後は、ng-app
にmodule名を指定すると、そのmoduleの元で初期化を行います。
この例だと、どっちかというとカスタムしたandroid.app.Applicationっぽいですねw
module で行うことが多いのは以下の設定です。
依存するmoduleの指定
ざっくり例えると、使うlibrary projectを追加する感じ。例えばInAppBillingが使いたい時はlibrary projectを使ったりするように、他の人が作ったmoduleや自作のmoduleを追加することもできます。
$routeProvider の設定
ざっくり例えると、Activityとそれに対するIntentFilterを書くのに近いです。どのURLが表示/遷移された時にどのを表示するかを指定します。
サンプル pushStateを使うので、iframeだとなんか上手く動かないぽいのでjsFiddleは無しです。 gist
angular.module("myApp", [], function ($routeProvider, $locationProvider) {
$routeProvider
.when("/foo", {
templateUrl: "/template/foo.html"
})
.when("/bar", {
templateUrl: "/template/bar.html"
})
.otherwise({
templateUrl: "/template/main.html"
});
$locationProvider.html5Mode(true);
});
<!DOCTYPE html>
<html ng-app="myApp">
<head>
<title>AngularJS + $rootProvider の説明</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<script type="text/javascript" src="jquery.js"></script>
<script type="text/javascript" src="angular.js"></script>
<script type="text/javascript" src="main.js"></script>
</head>
<body>
<marquee>こんにちは〜</marquee>
<div ng-view></div>
<script type="text/ng-template" id="/template/foo.html">
ふー!<br>
<a ng-href="/u/6581286/sample/AngularJS-routeProvider/index.html">メイン</a><br>
<a ng-href="/bar">ばー!</a>
</script>
<script type="text/ng-template" id="/template/bar.html">
ばー!<br>
<a ng-href="/u/6581286/sample/AngularJS-routeProvider/index.html">メイン</a><br>
<a ng-href="/foo">ふー!</a>
</script>
<script type="text/ng-template" id="/template/main.html">
おいでませクッパ城<br>
<a ng-href="/foo">ふー!</a><br>
<a ng-href="/bar">ばー!</a>
</script>
</body>
</html>
こんな感じです。リンクをクリックすると、ページの内容が差し替えられ、URLも書き換えられます。
$rootProvider
を使って、どのURLのパターンの時にng-view
に何を当てはめるかを指定してやります。この例では見た目を差し替えているだけですが、通常はcontroller
の指定をおこなったりもします。
実際使う時は、URLのパターンに/resource/:resourceId
などと指定すると、$routeParams
をDIで受け取った時に$routeParams.resourceId
などで値を利用することもできます。RESTっぽい設計にするとき大変便利ですね。
directive の設定
ざっくり例えると、独自Viewを作成するだけしておいてlibrary projectに投げ込んでおく感じです。実際に使うのはまた別の話。
service の設定
ざっくり例えると、自作のSystemServiceを作成して登録する感じです。AndroidでいうService(バックグラウンドでほげほげしたい!)とは全く別のものです。どっちかというとContentProviderみたいなデータをやり取りするための口、に近いです。
filter の作成
ざっくり例えようにも、Androidにはあんまり該当するものがないんですよね…。android.widget.FilterやAdapterが近いかもしれないけど、もっと柔軟で汎用的な仕組みです。Arrayから該当する要素だけを抜き出したり、JSONに変換したり、色々なことが出来るので、フィルタというよりコンバータの方が適切な言い方かもしれないですね。
紛らわしいことに、filter
というfilter
が存在して、これはまんまArrayから条件に当てはまる要素のみを抽出することができます。
directive = 独自View
directive
は、HTML上で見慣れないタグや属性があったらもう全部directive
って事でいいんじゃないでしょうか?
ng-app
もng-model
もng-click
もアレもコレも実は全部directive
として実装されているんだよ!ナ、ナンダッテー!!
AngularJSでは、DOMを明示的にいじくる事はほとんどありません。むしろ、テストのやりやすさのために後述のservice
やcontroller
中ではjQueryなどを使ってDOMをいじくるのは積極的に非推奨です。可能な限り行わないようにしましょう。
では、DOMをいじくりたい時、jQueryのDatePickerやGoogle MapsのWidgetを使いたい時、どうやってAngularJSの世界に組み込めば良いのか?そういうのはdirectiveを使って定義することになります。
AngularUIなどのサンプルをみてみると大変分かりやすいです。既存のjQuery pluginとかをAngularJSに組み込む時のサンプルにもなるでしょう。
angular.module('myApp', [])
.directive('vvMarquee', function ($parse) {
return {
restrict: 'A',
require: ['?ngModel'],
replace: true,
template: '<marquee/>',
link: function (scope, element, attrs) {
scope.$watch(attrs.ngModel, function (newVal) {
var getter = $parse(attrs.ngModel);
var model = getter(scope);
element.html(model);
});
}
};
});
function Controller($scope) {
$scope.data = "vvakame";
}
<div ng-app="myApp" ng-controller="Controller">
<div vv-marquee ng-model="data"></div>
<input type="text" ng-model="data">
</div>
このような感じで、DOMをいじくったりできます。ここでは昔なつかしmarqueeタグを使ってみました。ここでは詳しい説明を省きます。ぶっちゃけ難しくてキッチリ説明できるほどわかめも理解できていないという説が有力です。
service = Context#getSystemService(name)的な
service
は、わかめの理解ではサーバとAngularJSの世界を繋ぐためのノリのように使うのが良いようです。SystemServiceみたいとは一体なんだったのか…。事前にServiceの定義を用意しておいて、Fragmentなどから自在に呼び出してコキ使う…というのに向いています。
こうしておくと、AngularJSはDIをベースに成り立っているため、サーバとの接続部分だけをテストしたり、controller
がデータを受け取って処理をしたり…という部分を個別にテストしやすくなり、テスタビリティが向上します。
angular.module("twitter", [], function () {
})
.factory("twitterService", function ($http) {
return new TwitterService($http);
});
function TwitterService($http) {
this.getList = function (word) {
return $http({
method: "JSONP",
url: "http://search.twitter.com/search.json?q=" + word + "&callback=JSON_CALLBACK"
});
};
}
function Controller($scope, twitterService) {
twitterService.getList("AngularJS").success(function(data) {
$scope.tweets = data.results;
});
};
このような感じで、twitterServiceをservice
として登録し、Controllerから利用しています。$httpの使い方の説明はめんどくさいので説明を省きますが、TwitterのSearch APIでツイートを検索し、取得しています。ここで、serviceを利用することでControllerからはAjaxを利用していることなどを隠蔽し、普通のメソッド呼び出しの体になっています。
TypeScriptを利用していることもあって、わかめはこの形式でserviceを利用しています。
filter = android.widget.Filter or Adapter
filter
は、主にデータの加工に利用したりします。Androidではありませんが、例えば cat hoge.tx | grep android | wc -l
というコマンドを考えると、cat hoge.txt
部分が加工元のデータ、grep android
が1つ目のfilter、wc -l
が2つ目のfilter、といった具合です。
angular.module("myApp", [])
.filter("upper", function() {
return function(input, options) {
return input.map(function(data) {
return {
id: data.id,
name: data.name.toUpperCase()
};
});
};
});
function Controller($scope) {
$scope.list = [
{
id:1,
name:"vvakame"
},
{
id:2,
name:"grapswiz"
},
{
id:3,
name:"u1aryz"
},
{
id:4,
name:"eaglesakura"
}
];
};
<div ng-app="myApp" ng-controller="Controller">
<input type="text" ng-model="search" placeholder="絞込み">
<ul>
<li ng-repeat="data in list | filter:search | upper">
No.{{data.id}} {{data.name}}
</li>
</ul>
</div>
ここでは、AngularJSがデフォルトで持っているfilter filter
で絞込みを行った後、作ったupper filter
で名前を大文字に変換しています。
controller = Activity or Fragment or Adapter
controller
は、ActivityやFragmentにあたるもので、Viewに対して表示するデータをやりくりしてListFragment#setAdapter
にAdapterをセットしてやる、的な感じです。controller
ではAdapterなどの代わりに$scope
を利用します。$scopeに対して行った操作は、Viewとの双方向のデータのやり取りに利用します。
また、ネストさせる事もできるので、このように他のControllerの一部を再利用することもできます。ただ、あまり分かりやすいコードでもないですし、使うチャンスは少ないように思います。
function SelectController($scope, $window) {
$scope.selectedStateList = [];
$scope.selectedDataList = [];
$scope.changeSelection = function(index, data) {
if ($scope.selectedStateList[index]) {
$scope.selectedDataList[index] = data;
} else {
$scope.selectedDataList[index] = null;
}
};
$scope.show = function() {
var message = "selected " + $scope.selectedDataList.join(",");
$window.alert(message);
$scope.message = message
};
}
function UserController($scope) {
$scope.userList = [
"vvakame", "sys1yagi", "kojira", "neco"
];
}
<div ng-app>
<div ng-controller="UserController">
ユーザ一覧
<div ng-repeat="user in userList">
{{user}}
</div>
</div>
<div ng-controller="SelectController">
ユーザ選択
<div ng-controller="UserController">
<div ng-repeat="user in userList">
<input
type="checkbox"
ng-model="selectedStateList[$index]"
ng-change="changeSelection($index, user)">
{{user}}
</div>
{{message}}<br>
<button ng-click="show()">選択してるユーザを表示</button>
</div>
</div>
</div>
HTML = layout xml
HTMLはまんまHTMLの本来の役割通りの画面です。directive
と同じく、だいたいViewだと思ってもよいでしょう。HTMLはcontroller
からデータを与えてもらい、画面に反映する役割を持ちます。どういう構造で表示するかを書く、という面でlayout xmlと似ています。ですが、どのcontroller
が制御するかなどを指定する役割もあるので、細かく違うところももちろんあります。
HTMLについてもここまでに色々とサンプルで出てきたのでもはや割愛します。
$window や $location とか
unittestを行う際に実際のwindowやlocationなどを使ってしまうと、node.js環境で存在しなかったり、window.alertが呼ばれた時にポップアップが出てウザかったりするのでDIされた物を使うようにします。そのためにこれらが準備されています。
AngularJSのもう少し詳しい情報
他に知ってたほうがいいことを概説してみます。
-
Form
入力値のvalidationやエラーメッセージの表示なども手軽に行うことができます。 -
Expression
AngularJSでdirectiveなどに指定するExpressionについての解説です。JavaScriptっぽい構文ですが、完全に別物なのでうんうん唸って苦しんだりすることになります。なかなか覚えられなくて辛いです。 -
UnitTest
上でくどくどと言っていますが、AngularJSはDIで成り立っているので、テストもなかなか書きやすいです。ルールが飲み込めてくるまでは結構苦労するんですが…。Jasmineとかで適当にテストを書くことができます。ていうかこのドキュメントの最下部なんなの?マジぱないっす。-
testacular
そのうえで、よく使われているのがtestacularです。AngularJS自体もtestacularを利用してテストされています。PhantomJSやChrome、Firefox、Safari、IEなど複数のブラウザ上でテストを一括で走らせることができます。
-
testacular
-
E2E Test
End to End testを書くこともできます。結構こういうのって珍しい気がします。この辺りはまだちゃんと研究していないのでshogoggのブログとかを参照してみるといいかもしれない。
AngularJSとあまり関係のない情報
-
TypeScript
型があるぞー!!JavaScriptとの互換性もかなりあるぞー!!やったーー!!! -
tsd
TypeScript用の.d.tsファイルのダウンローダ的な感じ。 -
grunt
色々なタスクを行わせることができます。shell script書くより、色々な人が作ったタスクを組み合わせたほうが楽です。JavaScriptで設定ファイル書けますし。どっかのm○nとかいうXML編集させられるクソツールよりいいと思います。 -
bower
npmのクライアント版です。
参考情報 という名の愚痴
本家ドキュメント
ここ ぶっちゃけ、ドキュメントとしては量が少ないです。かなり大きなフレームワークですがそれに対して量が十分ではありません。なので、AngularJSを使い始めて、その世界観に慣れるまでに結構時間がかかります。わかめもなんやかやで2週間くらいかかった気がします。
また、本家ドキュメントとは直接関係はないのですが、Googleとかで検索した結果の資料として、1.0以前の内容はアテにならない場合が多いため、昔の情報はあまり信用しないほうがいいかもしれません。
テストの書き方
わかるまでが大変なのが、serviceやcontrollerのテストです。
そろそろサンプル作成するのが大変になってきたので、今書いているプロジェクトから抜粋します。言語はTypeScriptです。ご容赦ください…!!
普段はTypeScript+Jasmine+Testacularでテストを作って流して、ってしています。
describe("Serviceの", ()=> {
var $injector:ng.auto.IInjectorService;
beforeEach(()=> {
$injector = angular.injector(['ngMock']);
});
describe("Sample.Serviceの", ()=> {
var $httpBackend:ng.IHttpBackendService;
var service:Sample.Service;
beforeEach(()=> {
$httpBackend = $injector.get("$httpBackend");
service = $injector.instantiate(Sample.Service, {
$routeParams: {
hoge: "fuga"
}
});
});
it("getメソッドのテスト", ()=> {
$httpBackend.expect("POST", null).respond(200, {"hoge": "fuga"});
var promise = service.get();
var model;
promise.success((data)=> model = data);
$httpBackend.flush();
expect(model).toBeDefined();
});
});
});
describe("Controllerの", ()=> {
var $injector:ng.auto.IInjectorService;
beforeEach(()=> {
$injector = angular.injector(['ngMock']);
});
describe("Sample.Controllerの", ()=> {
var $scope:Sample.Scope;
var $controller:ng.IControllerService;
var $httpBackend:ng.IHttpBackendService;
var locals:any;
var makeController = (specificLocals:any = locals):Sample.Controller => {
return $controller(Sample.Controller, specificLocals);
};
beforeEach(()=> {
$httpBackend = $injector.get("$httpBackend");
$controller = $injector.get("$controller");
$scope = <any> $injector.get("$rootScope").$new();
var sampleService = $injector.instantiate(Sample.Service, {});
locals = {
$scope: $scope,
sampleService: sampleService
};
});
it("Controllerの作成", ()=> {
var controller = makeController();
expect(controller).not.toBeNull();
});
it("actionメソッドのテスト", ()=> {
var controller = makeController();
$httpBackend.expect("POST", null).respond(200, {"hoge": "fuga"});
$scope.action();
$httpBackend.flush();
expect($scope.result).toBeTruthy();
});
});
});
プロジェクト構成
日本語の情報
AngularJSドキュメント日本語訳プロジェクト
AngularJS メーリングリスト
後半の失速っぷりハンパナイネ!