Dependency Injection – одна из парадигм на которых строиться AngularJS.
Давайте подумаем как вот этот синтаксис
можно превратить в DI, и выполнить внедрение $scope и $http (в данном случае).
Итак, поставим задачу в духе TTD: у нас есть основная функция DemoController:
function DemoController($scope, $log){ $log($scope.someVariable); }
и вероятно еще 2 функции-конструкторы для объектов, которые будем внедрять:
function scopeProvider(){ return { someVariable: 'Some text'} } function logProvider(){ return function(text){ console.log.apply(console,arguments); } }
Теперь мы должны написать такую функцию invoke, используя которую можно будет выполнить
invoke('DemoController');
Первое что приходит в голову – получить функцию ввиде строки(toString), а потом достать из нее параметры регуляркой.
Получить метод по имени можно через объект, в котором он опеределен. В нашем случае это глобальный объект window:
var fn = window[functionName];
получаем описание функции в виде строки:
var fnText = fn.toString();
вырезаем из нее аргументы и записываем в массив:
var args = fnText.slice(fnText.indexOf('(') + 1, fnText.indexOf(')')).match(/([^\s,]+)/g);
итого имеем в args:
["$scope", "$log"]
пройдемся по ним циклом, создадим экземпляр класса провайдера для каждого и запишем в массив:
args.forEach(function(arg){ var providerName = arg.replace('$', '') + 'Provider'; injectors.push(new window[providerName]); })
ну и теперь осталось только выполнить наш метод с этими параметрами:
fn.apply(window, injectors);
Весь метод:
function invoke(functionName){ var fn = window[functionName], fnText = fn.toString(), args, injectors = []; args = fnText.slice(fnText.indexOf('(') + 1, fnText.indexOf(')')).match(/([^\s,]+)/g); args.forEach(function(arg){ var providerName = arg.replace('$', '') + 'Provider'; injectors.push(new window[providerName]); }) fn.apply(window, injectors); } invoke('DemoController');
Для одного уровня зависимостей этого достаточно, но что если мы захотим также добавить зависимости для scopeProvider и logProvider? Получается мы рекурсивно должны пройтись по всем вложенным конструкторам.
Итак добавим еще одну зависимость для scopeProvider:
function scopeProvider($dbAdapter){ this.someVariable = $dbAdapter.getText(); }
function dbAdapterProvider(){ this.getText = function(){ return 'Some text' } }
Попробуем пока без рекурсии добавить обработку еще одного уровня вложенности расширив метод invoke:
function invoke(functionName){ var fn = window[functionName], args, injectors = []; args = parseArguments(fn.toString()) || []; args.forEach(function(arg){ var providerName = arg.replace('$', '') + 'Provider', subfn = window[providerName], subargs = parseArguments(subfn.toString()) || [], subinjectors = []; subargs.forEach(function(subarg){ var subproviderName = subarg.replace('$', '') + 'Provider'; subinjectors.push(ObjectFactory(window[subproviderName])); }); injectors.push(ObjectFactory(window[providerName], subinjectors)); }) fn.apply(window, injectors); }
Прошу обратить внимание на ключевые моменты:
- регулярные выражения мы вынесли в отдельный метод parseArguments
- для динамического создания экземпляра класса мы использовали вспомогательный метод ObjectFactory
Содержание метода ObjectFactory:
function ObjectFactory(Constructor, args) { var Temp = function(){}, inst, ret; Temp.prototype = Constructor.prototype; inst = new Temp; ret = Constructor.apply(inst, args); return Object(ret) === ret ? ret : inst; }
Мы решили задачу, но опять у нас есть ограничение по уровню вложенности. Перед тем как переходить к рекурсивному решению еще немного упростим код:
function invoke(fn){ var args= parseArguments(fn.toString()) || [], injectors = []; args.forEach(function(arg){ var provider = getProviderMethod(arg); subargs = parseArguments(provider.toString()) || []; provider.injectors = []; subargs.forEach(function(subarg){ provider.injectors.push(ObjectFactory(getProviderMethod(subarg))); }); injectors.push(ObjectFactory(provider, provider.injectors)); }) fn.apply(window, injectors); } invoke(window['DemoController']);
что изменилось:
- теперь передаем параметром ссылку на саму функцию(а не название)
- получение метода провайдера вынесено в отдельную функцию getProviderMethod
Возьмемся за рекурсию:
function getInjectors(fn){ if(!fn.length){ //no injectors return []; } var args = parseArguments(fn.toString()) || [], injectors = []; args.forEach(function(arg){ var provider = getProviderMethod(arg); injectors.push(ObjectFactory(provider, getInjectors(provider))); }) return injectors; } function invoke(fn){ fn.apply(window, getInjectors(fn)); }
Вот так мы пришли к упрощенному варианту того, что внутри angular.$injector.
Методы инжектора Angular и их аналог в нашем примере:
- invoke – invoke
- instantiate – ObjectFactory
- get – getProviderMethod
- annotate – getInjectors