82
81

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

AngularとBrowserifyの微妙すぎる関係

Last updated at Posted at 2015-01-06

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

うっかりこの記事にたどり着いてしまった方は、@armorik83さんによる詳説「AngularJSモダンプラクティス」にあるCommonJS + Browserifyスタイルを好む方はの項を参照することをお勧めします。


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

時代背景的な

AngularJSの初版は案外古くて2009年に遡ります。Backbone.jsが2010年、Ember.jsが2011年という順番です。なので、当初は確かにモジュールローダを独自実装する必要が強くありました。ただ、2015年の今、AMDかCommonJSにしておいてくれれば...!と思わずにはいられません。実際、将来のAngularの2.0ではDIコンテナが分離され、他のフレームワークとも統合しやすいものになると見られています。(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

モジュールローダの宗派

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

種類 呼び出し 定義
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でこれ(↓)なので、実際の運用は推して知るべし。

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

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

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

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

Angular + gulp-concat

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

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

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

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

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

サービスです。

# 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です。ソースマップもつけた方が良いですね。

# 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'とすることは難しいです

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

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

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

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

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

# 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をTransformに加える必要があります。

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っぽい運用にします。

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

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

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

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

# 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をしない設定にしています。 (修正: 配列で渡せば可能でした)

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の文法に従うほかないので、ダブルスタンダード感は否めません)

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

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

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

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

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

# 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がわかりやすいので、説明を譲ります。

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のソースから個別にrequireするという荒技の「Angular + Browserify (烈)」というレシピもあるはずなんですが、茨の道すぎて尻込みしてした結果が、この記事になります。あしからず。

参考

82
81
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
82
81

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?