Kibana 4にみるAngularJSアプリの開発ノウハウ

[Kibana 4のプラグインをつくってみよう](http://qiita.com/zoetro/items/520f01e61833214d0a01) を書いたときに、Kibanaのソースコードを読んでいて発見したことについて紹介してみたいと思います。

# Kibanaの構成

 * https://github.com/elasticsearch/kibana

## 規模

* ソースコードはだいたい24,000行くらい。
* 各モジュールの数は次の通り。ディレクティブが多いのが目立ちます。
    * directive: 56
    * controller: 9
    * filter: 10
    * factory: 9
    * service: 20
    * provider: 0
    * value: 0
    * constant: 10

## ディレクトリ構成

ディレクトリ構成はこんな感じです。

![kibana_directory.png](https://qiita-image-store.s3.amazonaws.com/0/3489/4bbd2c7c-81b1-ff67-4e16-ce577da1b204.png)

[いま一番気に入っているAngularJSのプロジェクト構成](http://blog.mitsuruog.info/2014/12/angular-structure.html) で紹介されている、`folders-by-type`に近いでしょうか。
でも`components`の下は、機能毎にディレクトリが切られていてHTMLとlessとJavaScriptがまとめられているので、`folders-by-feature`とも言えそうです。

## test

* ユニットテストのみでE2Eテストはありません。
* Karmaは使わず、gruntでmochaを実行しています。istanbulでカバレッジ計測もしている模様。
* モックやスタブは基本sinonを利用しているようです。AngularJSが提供している`$provide.decorator`はあまり使っていません。

## minify

minifyはしてません。
Kibanaはインターネットに公開するような使い方はあまりせず、ローカルやイントラネット内で利用することが多いでしょうから、minifyも不要なんでしょうね。
なので、AngularJSでハマりがちな[minify対策](http://qiita.com/kawaz/items/363f430d21ec729f1b7d)は特に何もしていません。

## husky

AngularJSとは直接関係ないのですが、 [husky](https://github.com/typicode/husky) というモジュールがいいなと思いました。
gitリポジトリにpushしようとしたときにjshintを実行して、エラーが出たらpushできないようにしているようです。

# モジュール管理機能

AngularJSは、独自のモジュール管理機能を持っています。
しかし、AngularJSのモジュール管理機能はそれほど機能が豊富ではなかったり、既存のサードパーティライブラリをAngularJSのサービスとしてラップしたりするのが面倒だったりします。
また、コントローラの引数に大量のサービスを羅列する必要が出てきてしまうのが、AngularJSアプリ開発時の悩みの1つだったりします。

Kibanaでは、AngularJSのモジュール機能とRequireJSを組み合わせて、そのあたりの問題点を解消しているようです。

## modules

[angular.module の落とし穴](http://qiita.com/kazunorisasaki/items/bc5bc8a797f1aec12283) にあるとおり、angular.moduleは第2引数を指定した場合としなかった場合の挙動が異なるので注意が必要です。

```js
// モジュールを新規作成するとき
var module = angular.module('myModule', ['depModule']);

// 作成済みのモジュールを取得するとき
var module = angular.module('myModule');
```

Kibanaではmodulesというサービスを提供していて、モジュールがなかった場合はモジュールを生成し、存在した場合はモジュールが取得できるようになっています。

```js
var module = require('modules').get('myModule', ['depModule']);
```

## Privateサービス

Kibanaはrequirejsを使っているので、例えばpingサービスを使いたいときは次のように`require`して利用することができます。

```js
var ping = require('components/ping');
```

では、pingサービスからAngularJSの`$http`サービスを利用したい場合はどうすべきでしょうか。

まず、AngularJSのサービスと同じように、`$http`サービスを引数で受け取るようにします。

```js:components/ping.js
define(function (require) {
 return function PingProvider($http) {
    this.ping = function () {
      return $http.head('/health-check');
    };
  };
});
```

利用する側では、次のようにrequireの結果をPrivateに渡します。

```js
var ping = Private(require('components/ping'));
```

すると、PingProviderの引数に`$http`のインスタンスを渡して実行してくれます。

## requireとPrivateサービスとAngularJSサービスの使い分け

さてKibanaの実装では、他のサービスを利用する方法として、単純にrequireする方法と、上記で説明したPrivateサービスを使う方法と、AngularJSのDIを利用する方法が用意されています。

これらは、次のように使い分けているようです。

```js
module.controller('myController', function ($scope, $http, $location, Private) {
  var ping = Private(require('component/ping'));
  var _ = require('lodash');

  //・・・

});
```

 * `$http`や`$location`や自作のAngularJSサービスを利用する場合は、AngularJSのDIを利用する。
 * 他のAngularJSサービスに依存しているが、自身はAngularJSサービスになっていない場合はPrivateを使う。
 * lodashのようなサードパーティライブラリや、他のAngularJSサービスに依存していない自作サービスを使うときはrequireを利用する。


# モダンなスタイル

## ディレクティブを多用し、コントローラは使わない

[モダンAngularJS @ GDG中国2014.12.6](http://www.slideshare.net/armorik83/angularjs-141206) によると、モダンなAngularJSのスタイルでは、コントローラを使わず、ディレクティブを多用するようです。

Kibanaもそのようになっていて、画面の各要素はディレクティブとして作られており、コントローラは1画面にだいたい1つしかありません。

## コントローラ間連携

[Angular JS で複数のコントローラ間でモデル（状態や値）を共有する方法 3 種類](http://qiita.com/sunny4381/items/aeae1e154346b5cf6009) にあるように、AngularJSでコントローラ間の連携方法は悩ましい問題の一つです。
しかし、Kibanaではコントローラが1画面にだいたい1つなので、コントローラ間で連携することは少ないようです。
メモリリーク対策として`$destroy`イベントを利用していますが、それ以外にbroadcast, emit, onを使っている箇所は少ないです。
ただし、設定情報などを共有するために`$rootScope`に`globalState`という変数を用意したりしています。

## ディレクティブのタイプ

[AngularJS 2.0の実装](https://github.com/angular/angular/blob/master/modules/core/src/annotations/annotations.js) を眺めていると、AngularJS 2.0ではディレクティブが次の3つのパターンに分類されるようです。

* Component: Web Componentsのような部品化されたディレクティブ
* Decorator: 機能を付与するようなディレクティブ
* Template: テンプレートを扱うディレクティブ

Kibanaの場合、`components`ディレクトリ下にあるものがComponentタイプのディレクティブで、`directives`ディレクトリ下にあるものがDecoratorタイプのディレクティブと言えそうです。

AngularJS 2.0のリリースに備えて、AngularJS 1.xを使う場合でもこんな感じでディレクティブを作り分けておくと、移行がしやすくなるかもしれませんね。

# ルーティング

Kibana 4では、画面上で文字を入力したり選択したり何かしらの操作をするたびに、その情報がURLに反映されるようになっています。

例えばグラフの設定をしているときのURLは次のようになります。とても長いですが、インデックスやクエリの情報、指定した時間の範囲、グラフの軸の情報などが全部乗っかっているのが分かります。

~~~
http://localhost:8000/#/visualize/create?type=area&indexPattern=logstash-*&_g=(time:(from:'2014-12-06T16:25:22.054Z',mode:absolute,to:'2014-12-06T16:27:35.040Z'))&_a=(filters:!(),query:(query_string:(query:'*')),vis:(aggs:!((id:1,params:(),schema:metric,type:count),(id:2,params:(field:code),schema:metric,type:avg),(id:3,params:(extended_bounds:(),field:'@timestamp',interval:auto,min_doc_count:1),schema:segment,type:date_histogram)),listeners:(),params:(addLegend:!t,addTooltip:!t,mode:stacked,shareYAxis:!t),type:area))
~~~

Kibana 3のときには、画面を再読み込みしたら、頑張って設定していたグラフの情報が消えたり悲しい思いをしましたが、Kibana 4ではリロードしても設定が消えることはありません。
またブラウザの戻るボタンを押すと、1つ前に操作した内容が戻る動きをするので、Undoとして利用することができます。

なお、ui-routerは使わず、標準のngRouterを使っています。

# おわり

今回は紹介しきれませんでしたが、Elasticsearchと通信をするところなど、他にも興味深い仕組みがたくさんあります。
AngularJSで大きなアプリケーションを作りたいと思っている時に、勉強がてら読んでみてはいかがでしょうか。