AngularとBrowserifyの微妙すぎる関係

この記事の内容はいささか、古くなってしまっているようです。投稿からしばらくして、AngularもCommonJS対応を強化して、以前よりは`require`との相性が改善しています。ただ、筆者自身はもうAngularから離れてしまったのでキャッチアップしきれていません。

うっかりこの記事にたどり着いてしまった方は、@armorik83さんによる詳説「AngularJSモダンプラクティス」にある[CommonJS + Browserifyスタイルを好む方は](http://qiita.com/armorik83/items/5542daed0c408cb9f605#commonjs--browserify%E3%82%B9%E3%82%BF%E3%82%A4%E3%83%AB%E3%82%92%E5%A5%BD%E3%82%80%E6%96%B9%E3%81%AF)の項を参照することをお勧めします。

------

AngularJSとBrowserify、この二人の関係、かなり微妙です。もうどうしてくれようかってくらい。例えるなら宗教の違う恋人のようなものです(適当)。CommonJSに慣れた人には、AngularのDIが正直お邪魔。かといって、Angularの根幹に関わる部分なので使わないわけにもいかないし、と距離感に悩む二人です。もちろん、[React](http://facebook.github.io/react/)や[Ampersand.js](http://ampersandjs.com/)と幸せになる手もありますが、それはそれ。どんな折り合いのつけ方があるのか、別のパートナーを探すべきなのか、少し考えてみたいと思います。

## 時代背景的な

AngularJSの初版は案外古くて2009年に遡ります。Backbone.jsが2010年、Ember.jsが2011年という順番です。なので、当初は確かにモジュールローダを独自実装する必要が強くありました。ただ、2015年の今、AMDかCommonJSにしておいてくれれば...!と思わずにはいられません。実際、将来のAngularの2.0では[DIコンテナが分離](https://github.com/angular/di.js/)され、他のフレームワークとも統合しやすいものになると見られています。(2.0でのweb componentsの扱いも気になりますね)

年 | 関係するできごと | 備考
:--- | :--- | :---
2009 | AngularJS初版 | Googleが支援
2010 | Knockout.js, Backbone.js初版 | 
2011 | Ember.js初版 | SproutCore 2 から
2013 | React初版 | Facebookが支援
2014 | HTML5 正式勧告 | 
2015 | ES6 勧告予定 | 
???? | [Web Components](http://www.w3.org/wiki/WebComponents/) | 

## モジュールローダの宗派

Node自体や最近のNode製ツールを触っていると、CommonJSがデフォルトのような気がしてきますが、クライアントサイドへの浸透は案外進んでいません。Browserifyがあらためて普及し始めて、クライアントサイドへの道が開けましたが、それまではほぼAMD一択の状況だったので、まあしょうがないかなと。2014年は、webpackやduoなど、Browserify以外でもCommonJS対応するものが話題になりました。

- [**AMD**](https://github.com/amdjs/amdjs-api): [RequireJS](http://requirejs.org/), [webpack](http://webpack.github.io/), [Dojo](https://dojotoolkit.org/), ...
- [**CommonJS**](http://wiki.commonjs.org/wiki/CommonJS): [Node.js](http://nodejs.org/), [Browserify](http://browserify.org/), [webpack](http://webpack.github.io/), [duo](http://duojs.org/), ...
- [**UMD**](https://github.com/umdjs/umd): AMDとCommonJSの両方に対応する書き方
- [**ECMAScript 6**](https://people.mozilla.org/~jorendorff/es6-draft.html): 次期バージョンの JavaScript (2015年6月勧告予定)
- 独自系: [AngularJS](https://angularjs.org/) ※Angular用語的には「依存性の注入(DI)」

種類 | 呼び出し | 定義
:-- | :-- | :--
AMD | `requirejs.config({ backbone: 'path/to/file' })` | `define(['backbone'], function (Backbone) {/* */})`
CommonJS | `var Backbone = require('backbone')` | `module.export = function() {/* */}`
ES6 (参考) | `import { hello } from 'somemodule'` | `export function hello {/* */}`


## 「AngularのDIは`require`である」

Angularの初学者を悩ませるものの一つが「DI = Dependency Injection」です。

- 「他言語でいうところのDIとなんか違うような...?」
- 「やっていることは、モジュール連携の作法に見える」
- 「独特でよくわからん」

いろんな声が聞こえてきそうです。でも「DI」という言葉にとらわれ過ぎると、よくわからなくなります。むしろ、CommonJS慣れしているなら、単に「`require`の代わりになる何か」と思った方がわかりやすいはず。

## Angularとモジュールローダ

さて、そろそろ具体論に。HTMLに`script`タグが氾濫することと、グローバル汚染だけは避けたいのが人情です。[TodoMVCのHTML](https://github.com/tastejs/todomvc/blob/gh-pages/examples/angularjs/index.html)でこれ(↓)なので、実際の運用は推して知るべし。

```html
<script src="bower_components/todomvc-common/base.js"></script>
<script src="bower_components/angular/angular.js"></script>
<script src="bower_components/angular-route/angular-route.js"></script>
<script src="js/app.js"></script>
<script src="js/controllers/todoCtrl.js"></script>
<script src="js/services/todoStorage.js"></script>
<script src="js/directives/todoFocus.js"></script>
<script src="js/directives/todoEscape.js"></script>
```

これ(↑)をこんな風(↓)にスッキリしたい...!

```html
<script src="app.js"></script>
```

それでは、いくつかのパターンを考えていきましょう。

### Angular + gulp-concat

トップバッターは、しょっぱな浮気して`gulp-concat`です。
シンプルかつ頑張りすぎてなくてバランスのとれた好人物もとい解決策です。でも、なんか納得がいかない... ><

一応説明を加えると、コントローラとサービスをそれぞれファイルを分けて書き、単純にファイルを結合しています。下記は簡略化した例です。(以下、ややこしくなるので、angularはすでにグローバルに定義済みとして話を進めます)

```coffeescript
# ./app.coffee
app = angular.module 'app', []
```

残念ながら、`app`はグローバルです。続いて、コントローラと

```coffeescript
# ./controller/todoCtrl.coffee
app.controller 'todoCtrl', ($scope, todoStorage) ->
  $scope.todos = todoStorage.get()
  # 処理が続く
```

サービスです。

```coffeescript
# src/service/todoStorage.coffee
app.factory 'todoStorage', ->
  STORAGE_ID = 'todos-angularjs-perf'
  ret = 
    get: -> JSON.parse localStorage.getItem STORAGE_ID
    put: (todos) -> localStorage.setItem STORAGE_ID, JSON.stringify todos
```

結合(concat)するためのgulpfileです。ソースマップもつけた方が良いですね。

```coffeescript
# gulpfile.coffee
gulp     = require 'gulp'
coffee   = require 'gulp-coffee'
concat   = require 'gulp-concat'
uglify   = require 'gulp-uglify'
annotate = require 'gulp-ng-annotate'
 
gulp.task 'angular', ->
  gulp.src [
  	'src/app.coffee'
  	'src/*/*.coffee'
  ]
  .pipe coffee bare: true # CoffeeScriptのコンパイル
  .pipe concat 'app.js' # 結合
  .pipe annotate() # アノテーションをつけて`uglify`に備える
  .pipe uglify() # ミニファイ
  .pipe gulp.dest 'dist/'
```

### Angular + Browserify (弱)

次。本命らしきBrowserifyに弱気に近づきます。

ほとんど、結合しているのと変わりませんが、`require`を使って`app.coffee`から他のファイルを読み込むようにしています。なお、AngularJSは非CommonJSなため、`angular = require 'angular'`とすることは[難しいです](http://stackoverflow.com/questions/25088406/cant-get-external-library-with-browserify-and-debowerify/25111348)。

```coffeescript
# src/app.coffee
angular.module 'app', []

require './controller/todoCtrl'
require './service/todoStorage'
```

先ほどと違うのは、読み込む先のファイルから`app`オブジェクトを直接参照できない点。`angular`はグローバル変数なので`module`関数経由で参照を取得します。ちなみに、`module`関数は引数2つでモジュール生成、引数1つでモジュール取得になります。

```coffeescript
# src/controller/todoCtrl.coffee
app = angular.module 'app'
app.controller 'todoCtrl', ($scope, todoStorage) ->
  $scope.todos = todoStorage.get()
  # 処理が続く
```

これだけ見ると、不要な複雑さを持ち込んでいるようにも見えますが、`require`で必要なnpm/bowerモジュールを読み込めるのは魅力です。あと、`app.factory`の外側に変数を書いてもグローバルを汚染しないのは気楽ですね。

```coffeescript
# src/service/todoStorage.coffee
app = angular.module 'app'
STORAGE_ID = 'todos-angularjs-perf'

app.factory 'todoStorage', ->
  get: -> JSON.parse localStorage.getItem STORAGE_ID
  put: (todos) -> localStorage.setItem STORAGE_ID, JSON.stringify todos
```

Browserify用のgulpfileはこんな感じに。ここではBrowserifyのTransformを使いましたが、前の例と同じに`gulp-ng-annotate`でも構いません。ちなみに、bower経由のモジュールを使う場合は[debowerify](https://github.com/eugeneware/debowerify)をTransformに加える必要があります。

```coffeescript
gulp       = require 'gulp'
browserify = require 'browserify'
coffeeify  = require 'coffeeify'
annotatify = require 'browserify-ngannotate'
source     = require 'vinyl-source-stream'
streamify  = require 'gulp-streamify'
uglify     = require 'gulp-uglify'

gulp.task 'angular', ->
  browserify
    entries: ['src/app.coffee']
    extensions: ['.coffee']
  .transform coffeeify # CoffeeScriptのコンパイル
  .transform annotatify # アノテーションをつけて`uglify`に備える
  .bundle()
  .pipe source 'app.js'
  .pipe streamify uglify() # ミニファイ
  .pipe gulp.dest 'dist/'
```


### Angular + Browserify (中)

さらに、もう少し歩み寄ってみます。要は、CommonJSっぽい運用にします。

```coffeescript
# src/app.coffee
angular
.module     'app', []
.controller 'todoCtrl',    require './controller/todoCtrl'
.factory    'todoStorage', require './service/todoStorage'
```

(弱)の例では`module.export`をあえて使っていませんでした。

```coffeescript
# src/controller/todoCtrl.coffee
module.exports = [
  '$scope', 'todoStorage'
  ($scope, todoStorage) ->
    $scope.todos = todoStorage.get()
    # 処理が続く
]
```

Angularのモジュールとしての体裁に揃えるため、関数でラップして返しているのもポイントです。Angularのマジックで`$scope`と`todoStorage`が使えるのが気になりますが、そこはぐっとこらえます。

```coffeescript
# src/service/todoStorage.coffee
STORAGE_ID = 'todos-angularjs-perf'

module.exports = ->
  get: -> JSON.parse localStorage.getItem STORAGE_ID
  put: (todos) -> localStorage.setItem STORAGE_ID, JSON.stringify todos
```

gulpfileは先ほどとあまり変わりません。~~ただ、上述のように`.controller`や`.factory`関数などから`require`する形だと、自動アノテーションができません。そのため、圧縮率で不利ですがmangleをしない設定にしています。~~ (修正: 配列で渡せば可能でした)

```coffeescript
gulp       = require 'gulp'
browserify = require 'browserify'
coffeeify  = require 'coffeeify'
source     = require 'vinyl-source-stream'
streamify  = require 'gulp-streamify'
uglify     = require 'gulp-uglify'

gulp.task 'angular', ->
  browserify
    entries: ['src/app.coffee']
    extensions: ['.coffee']
  .transform coffeeify # CoffeeScriptのコンパイル
  .bundle()
  .pipe source 'app.js'
  .pipe streamify uglify mangle: false # ミニファイ (mangleなし)
  .pipe gulp.dest 'dist/'
```


### Angular + Browserify (強)

ふたりも慣れてきたので、本音をぶつけ合ってみます。

先の例では、Angularのサービスを一旦Angularのモジュールとして読み込んでから使っていましたが、なんだか不毛です。HTMLテンプレート内から呼ぶ可能性があるのはディレクティブとフィルターが主なので、サービスに関してはばっさりAngularから切り離します。(ただし、コアのサービスやプラグインとして提供されているものは、Angularの文法に従うほかないので、ダブルスタンダード感は否めません)

```coffeescript
# src/app.coffee
angular
.module     'app', []
.controller 'todoCtrl', require './controller/todoCtrl'
```

ふう...、やっと普通のJavaScriptっぽくなってきました。

```coffeescript
# src/controller/todoCtrl.coffee
todoStorage = require '../service/todoStorage'

module.exports = [
  '$scope'
  ($scope) ->
    $scope.todos = todoStorage.get()
    # 処理が続く
]
```

Angularの作法に従う必要はないので、この例ではオブジェクトを返しています。(つまり、関数のラッピングを外しました)

```coffeescript
# src/service/todoStorage.coffee
STORAGE_ID = 'todos-angularjs-perf'

module.exports =
  get: -> JSON.parse localStorage.getItem STORAGE_ID
  put: (todos) -> localStorage.setItem STORAGE_ID, JSON.stringify todos
```

※gulpfileは(中)の例と同じなので省略。


### Angular + RequireJS

別キャラ登場ですが、すみません、力尽きました。[angularAMD](http://marcoslin.github.io/angularAMD/)がわかりやすいので、説明を譲ります。

![angularAMD: The Simple Way to Integrate AngularJS and RequireJS.png](https://qiita-image-store.s3.amazonaws.com/0/1858/c81128ea-ee0f-dbdd-0123-18908f8dd284.png "angularAMD: The Simple Way to Integrate AngularJS and RequireJS.png")


## まとめ

ここまで、5つのパターンを見ました。

- Angular + gulp-concat
- Angular + Browserify (弱)
- Angular + Browserify (中)
- Angular + Browserify (強)
- Angular + RequireJS

個人的には、どれもしっくりきていませんが、(強)でやるか、Browserifyを諦めてgulp-concatのみで行くかどちらかという気がしています。

実は、[Angularのソース](https://github.com/angular/angular.js/tree/master/src)から個別に`require`するという荒技の「Angular + Browserify (烈)」というレシピもあるはずなんですが、茨の道すぎて尻込みしてした結果が、この記事になります。あしからず。

## 参考

- [Best Practices for Building Angular.js Apps](https://medium.com/@dickeyxxx/best-practices-for-building-angular-js-apps-266c1a4a6917)
- [Requiring vs Browserifying Angular](http://developer.telerik.com/featured/requiring-vs-browerifying-angular/)