AngularJSのDIの仕組み、minify対策は覚えておこう!

  • 427
    Like
  • 4
    Comment
More than 1 year has passed since last update.

DI (Dependency Injection)ってのは日本語では依存性注入とも呼ばれ、大雑把に言うとAngularJSがコントローラなどに必要とされているコンポーネント(オブジェクト)をいい感じに渡してやる機能です。

ここでは特にAngularJSのDIがどのような仕組で動いてるか、そしてその独特なDIの実装にまつわるトラブルケースを説明します。

AngularJSのコントローラの書き方

まずはAngularJSの中心的な機能であるコントローラの書き方には、簡単版と面倒版の複数の書き方があることを抑えておきましょう。

パターン1(グローバル関数パターン)

サンプルとかでよく見るのは↓こういうグローバル関数の形のコントローラです。

<div ng-app>
  <div ng-controller="HogeCtrl">(略)</div>
</div>
function HogeCtrl($scope) {
  //略
}

シンプルでとっつきやすいです。初心者にも優しくデモ向きですね。
AngularJSの素敵さ(不思議さ)がよく分かる形です。

パターン2(モジュール作って.controllerメソッドパターン)

そしてもう一つは↓このようなキチンとしたモジュール定義の形です。

<div ng-app="myApp">
  <div ng-controller="HogeCtrl">(略)</div>
</div>
var myApp = angular.module('myApp', []);
myApp.controller('HogeCtrl', ['$scope', function($scope) {
  //略
}]);

なんか急に複雑で、初心者お断りな雰囲気になりましたね…。

これからAngularJSやってみようかって人が最初にこれ見たら、その時点で半分は逃げ出しちゃいそうです。AngularJSの素敵さを説明するには本質以外の部分が多いのでデモには向かない形かもです。

でもAngularJSでちゃんとしたアプリを作ろうと思ったら覚えなきゃいけない形です。理由は続きを読めば分かります。

パターン3($inject Annotationパターン)

1にかなり近い形だけど2ほど面倒じゃなくて、でもAngularJS的には一応キチンとしてて1よりはベターな↓こんな形もあります。

<div ng-app>
  <div ng-controller="HogeCtrl">(略)</div>
</div>
function HogeCtrl($scope) {
  //略
}
HogeCtrl.$inject = ['$scope'];

2つ目の形を覚えたくないならこの形も一応アリ、但しコントローラの機能しか使わないのであれば、だけど。フィルター定義とかしたくなったら結局は2つ目の形を覚える必要があるので、むしろこの形は一番覚える必要が無いとも言えます。

これの意味するところも最後まで読めば分かります。

Angular公式の推奨は2つ目の書き方(多分)

上記の3種類の書き方はどれも同じように動くけど、公式ドキュメントのUnderstanding Controllersのページでは、以下に引用するように上記2つ目の .controllerメソッドを利用した形を推奨 する文章があります。

NOTE: Many of the examples in the documentation show the creation of functions in the global scope. This is only for demonstration purposes - in a real application you should use the .controller method of your Angular module for your application as follows: ...

↓超訳

ドキュメント上のサンプルではグローバル関数でコントローラ定義してるのが多いけど、それはあくまでデモの分かりやすさを優先してるからだよ。ちゃんとしたアプリを作るときは、アプリ用のモジュールを作ってその.controllerメソッドを使ってコンポーネント名を列挙した書き方をする べき(should) だ。

AngularJSのDIの仕組み

AngularJSの不思議

AngularJSのコントローラーの引数って$scope$route$windowって名前が当たり前のように使われてますが、これってただの慣習的なものじゃないんですよね。試しに引数の変数名をscopeとかrとかに変えると動かなくなってしまいます。

それに、コントローラ関数の$scopeとか$routeという引数の順番を変えても動くことにも不思議を感じませんか?これ関数の引数になってるからグローバル変数でもありえないしどうなってんの、と。

その答えは実は、AngularJSのちょっと特殊(変態的)なDIの仕組みにあります。

実装の秘密(変数名でDI)

AngularJSの不思議の鍵はその実装方法にあります。

実はAngularJSはコントローラに指定された関数定義を 文字列としてパース して、引数の 変数名 からコントローラ関数に渡すべきオブジェクトを決定するというかなり特殊(ぶっちゃけ変態的)なアプローチを取っているんです。だからこんなJavascript的には一見不思議なDIが可能なんですね。

このあたりのは$injector内のannotate(fn)という関数の実装を見ると分かります。関数オブジェクトに対してHogeCtrl.toString()すると関数定義全体が文字列として取得できることを利用しているんですね。

minifyにまつわるトラブル

勘の良い人ならもう気づいてると思いますが、AngularJSのDIが上述のような仕組みで動いている為、AngularJS用のソースコードはminify(文字数節約による圧縮)やobfuscate(難読化)等に弱いんです。

例えば、function($scope){...}function(s){...}とかに変換されてしまうと、引数の変数名を見ても$scopeのオブジェクトを渡すべきってことが分からなくなってしまうのですね。

これを知らないとminifyしたとたんに動かなくなって、Angularがバグってやがる、とかminifierのバグだ、とか見当違いのデバッグ作業で時間を無駄にすることになったります。

公式ドキュメントのコメントとかにも「サンプル動かねーんだけど!」って声がよくあるのは、大抵このパターンだと思います。フレームワークで最初から勝手にminifyされるのに慣れちゃってるような人がよくハマる罠です。

解決策(パターン2の書き方を推奨する理由)

この問題の解決策として用意されているのが、コントローラの書き方パターン2の「.controllerメソッドを使うパターン」です。

minifyやobfuscateプログラムは文字列の値は弄りませんから、渡してほしいオブジェクト名を文字列として明示的に指示してやることで、AngularJSはわざわざfunctionオブジェクトをパースせずとも「1番目の引数として$scopeを渡してやればいいんだな」ということが知ることが出来、結果としてアノテート対象の関数の引数名が何であろうと第1引数に$scopeという名前のオブジェクトを受け取れるようになるわけです。

そんなわけで、導入としては便利だしとっつきやすいから1番目の書き方もできるけど、本格的に開発したりライブラリとして第3者に利用されることも考えたコードを書くなら、minifyが導入される可能性も考慮してパターン2の「モジュール作って.controllerメソッドを利用する」書き方を推奨してるのです。

蛇足(パターン3の書き方でもよい理由)

ちなみに、パターン1で関数を文字列パースして分かった引数名は最終的には関数オブジェクトの$injectというプロパティにキャッシュとして保存されて、以降Angularは$injectプロパティから要求されているコンポーネント名を取得するようになってます。

最初に紹介したパターン3の形はこの内部実装を利用した抜け道なわけですね。これは一応公式ドキュメントでも$inject Annotationとして紹介されている方法ですが、実装に依存してる感は拭いきれず気持ち悪いので個人的にはあんまし使わないです。

コントローラに限った話ではない

ここではコントローラを中心に説明しましたが、このDIの仕組みとminify問題は、全てのコンポーネント(フィルター、サービス、ディレクティブ、コンフィグ、etc)でも基本みんな同じです。それぞれ.filterとか.serviceとか.factoryとか.directiveとか.configとか…、要はangular.Moduleで定義されてるメソッドで関数を渡すべき場所ではみんな、関数単体を渡す代わりにコンポーネント名と関数を纏めた配列で渡すことができるし、そうすべきだよってことです。

ちなみにコントローラもコンポーネントのうちです。何かの折に名前で関数オブジェクトとして取得されて使われることもあります、テストとかね。

例えば$routeProvider使った場合のコントローラの指定とかも

↓こんな感じに.controllerで指定したコントローラ名を文字列で渡してやればminify対策が出来るようになってます。

var myApp = angular.module('myApp', [''])
  .controller('ListCtrl', ['$scope', function($scope) {/* 略 */}])
  .controller('HogeCtrl', ['$scope', function($scope) {/* 略 */}])
  .config(["$routeProvider", function($routeProvider) {
    $routeProvider.
      when('/', {controller:"ListCtrl", templateUrl:'list.html'}).
      when('/hoge', {controller:"HogeCtrl", templateUrl:'hoge.html'});
  }]);

$routeProvider.when(path, route)ドキュメントにも文字列or関数で指定すると書いてあります。

route - {Object}
・controller – {(string|function()=} – Controller fn that should be associated with newly created scope or the name of a registered controller if passed as a string.

参考URL