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