AngularJSのDIの仕組みを追ってみた
AngularJS黒魔術のうちの1つ。DI。
コントローラーの引数に$httpなどを指定すると、なぜ何もしなくてもHttpProviderの返り値が入ってくるのか。
var userControllers = angular.module('userControllers', []); userControllers.controller('UsersCtrl', function($scope, $http) { $http.get('users/index.json').success(function(data) { $scope.users = data; }); });
これは、定義したコントローラーをインスタンス化する際にannotate関数でDI対象となる引数を取得して、該当するサービスオブジェクトに差し替えているから。
function invoke(fn, self, locals){ var args = [], $inject = annotate(fn), length, i, key; for(i = 0, length = $inject.length; i < length; i++) { key = $inject[i]; if (typeof key !== 'string') { throw $injectorMinErr('itkn', 'Incorrect injection token! Expected service name as string, got {0}', key); } args.push( locals && locals.hasOwnProperty(key) ? locals[key] : getService(key) ); } if (!fn.$inject) { fn = fn[length]; } return fn.apply(self, args); }
では、annotate関数では何をしているのか。
FunctionオブジェクトをtoStringで文字列化して、引数に当たる文字列を抜き出し、返している。
fn.lengthはその関数が受け取る引数の数を表す。
function annotate(fn) { var $inject, fnText, argDecl, last; if (typeof fn == 'function') { if (!($inject = fn.$inject)) { $inject = []; if (fn.length) { fnText = fn.toString().replace(STRIP_COMMENTS, ''); argDecl = fnText.match(FN_ARGS); forEach(argDecl[1].split(FN_ARG_SPLIT), function(arg){ arg.replace(FN_ARG, function(all, underscore, name){ $inject.push(name); }); }); } fn.$inject = $inject; } } return $inject; }
上の例の場合、argDecl[1]には"$scope, $http"という文字列が入る。
これをsplitして、getService関数でファクトリ関数の返り値をキャッシュしたプールからオブジェクトを取り出し、コントローラーのコンストラクタに渡して実行する、というわけか。
このキャッシュプールから取り出すキーになるのが"$http"だったり"$routeParams"だったりするので、引数の名前が違うとDIされない。
Angularが用意しているProviderを使いたければ、Angularが中で持っている名前にしないといけないし、module.factoryなどでユーザーが定義したサービスだったら、その名前で指定しないといけない。
また、よく言われるminifyのための注意として、ユーザーが定義するコントローラーやサービスには文字列でも引数を指定しておくというのも納得。
そうしないとminify時に引数名が短縮されてしまうので、DIすべき対象が見つからなくなってしまう。
userControllers.controller('UsersCtrl', ['$scope', '$http', function($scope, $http) { $http.get('users/index.json').success(function(data) { $scope.users = data; }); }]);