Привет!
Сегодня мы поговорим о наследовании в AngularJS. Поскольку в нами любимом фрэймворке нет встроенного механизма наследования, в этой статье мы с вами посмотрим, как при помощи паттернов наследования JavaScript организовать наследование в компонентах AngularJS.
Наследование контроллеров
Для начала давайте рассмотрим, как это работает в контроллерах. На самом деле, наследование одного контроллера от другого (за исключением использования «controller as» синтаксиса) — достаточно маловероятно. Всё потому, что scope дочерних контроллеров наследуется через прототип от родительского scope. Поэтому, если вам нужно переиспользовать функциональность родительского контроллера, всё что вам нужно — добавить требуемые методы в родительский scope. Сделав это, дочерний контроллер будет иметь доступ ко всем методам через прототип его scope. Вот вам пример:
myApp.controller('MyParentController', function ($scope) {
$scope.parentMethod = function () {
console.log('function body...');
};
});
myApp.controller('MyChildController', function ($scope) {
$scope.parentMethod();
});
Конечно, наследование создает сильную связанность, но только в одном направлении (т.е. дочерний контроллер зависит от родительского), поэтому вы не можете напрямую вызвать дочерний метод из вашего родительского контроллера. Если у вас всё-таки появилась такая необходимость, вы можете использовать механизм событий:
myApp.controller('MyParentController', function ($scope) {
$scope.$broadcast('event', args);
$scope.$on('event-response', function (result) {
console.log('function body...');
});
});
myApp.controller('MyChildController', function ($scope) {
$scope.$on('parent-event', function (args) {
var result;
console.log('result calculation...');
$scope.$emit('parent-event-response', result);
});
});
Этот пример приведён исключительно с информативной целью, т.к. если вам требуется вызывать методы дочернего контроллера из родительского, задумайтесь — скорее всего, вы что-то делаете не так.
А теперь давайте представим, что у вас есть две страницы с практически одинаковым функционалом. Предположим, что у них пересечение в общей части более 50%. Несмотря на то, что у них может быть абсолютно разные представления (view), логика, стоящая за ними, может быть похожа, даже очень. В таком случае вы можете создать базовый контроллер, который будет содержать в себе общую логику, и два дочерних контроллера, расширяющих базовый. Базовый контроллер необязательно должен быть реализован как AngularJS-контроллер. Вы можете использовать обычную функцию-конструктор. Вот пример:
function MyBaseController($scope, $location, ...) {
$scope.commonMethodInScope = function () {
console.log('function body...');
};
$scope.commonVar = 12;
}
MyBaseController.prototype.commonMethod1 = function () {
console.log('function body...');
};
MyBaseController.prototype.commonMethod2 = function () {
console.log('function body...');
};
После этого дочерний контроллер может легко наследоваться от базового вот таким вот образом:
function MyChildController1($scope, $location, ...) {
MyBaseController.call(this, $scope, $location, ...);
$scope.childMethodInScope = function () {
this.commonMethod2();
};
}
MyChildController1.prototype = Object.create(MyBaseController.prototype);
MyChildController1.prototype.childMethod1 = function () {
this.commonMethod1();
};
myApp.controller('MyChildController1', MyChildController1);
В коде выше видно, что применяется JS-паттерн наследования на классах. Так же нужно сделать и со вторым контроллером.
Синтаксис «controller as»
Начиная с AngularJS 1.2 в фрэймворк был добавлен «controller as» синтаксис. Что он даёт? Он позволяет нам создавать псевдонимы для наших контроллеров. К примеру, используя директиву ng-controller мы можем сделать так:
<div ng-controller="BossController as boss">
<button ng-click="boss.onClick()">Click</button>
</div>
со следующим контроллером:
function BossController() {
this.name = 'myName';
}
BossController.prototype.onClick = function () {
alert('Oooooh! You\'ve clicked me!');
};
Ещё один пример, показывающий преимущества данного подхода:
function MyBaseController() {
this.name = 'myName';
}
MyBaseController.prototype.parentMethod = function () {
console.log('function body...');
};
function MyChildController() {
MyBaseController.call(this);
this.name = 'myChildName';
}
MyChildController.prototype = Object.create(MyBaseController.prototype);
MyChildController.prototype.childMethod = function () {
this.parentMethod();
console.log('function body...');
};
app.controller('MyBaseController', MyBaseController);
app.controller('MyChildController', MyChildController);
Разметка для кода выше:
<div ng-controller="MyBaseController as base">
<button ng-click="base.parentMethod()">Предок</button>
<div ng-controller="MyChildController as child">
<button ng-click="child.childMethod()">Потомок</button>
</div>
</div>
Когда пользователь жмёт кнопку с подписью «Предок», вызовется parentMethod, определённый в MyBaseController. Если же пользователь нажмёт «Потомок», будет вызван childMethod. Заметим, что в своём теле он также вызывает parentMethod.
Наследование сервисов
Как вы знаете, существует два способа создания внедряемого через Dependency Injection (DI) AngularJS-сервиса:
module.factory(name, factoryFunc);
module.service(name, factoryFunc);
module.factory
В module.factory функция factoryFunc возвращает литерал объекта, который и является сервисом. У себя под капотом AngularJS вызывает функцию фабрики внутри определения injector’а. Если вам нужно наследование в сервисах, которые созданы через module.factory, вам отлично подойдёт паттерн прототипного наследование путём вызова Object.create.
Давайте создадим базовый сервис:
var BaseService = (function () {
var privateVar = 0;
return {
anyDoing: function () {
if (privateVar === 42) {
alert('It is answer!!');
}
privateVar++;
};
};
}());
А теперь дочерний сервис:
var MyChildService = Object.create(BaseService);
MyChildService.anyDoingYet = function () {
console.log('function body...');
};
module.factory('MyChildService', function () {
return MyChildService;
});
Теперь вы можете внедрять MyChildService в AngularJS-компоненты и переиспользовать наследованные от BaseService возможности.
function MyController(MyChildService) {
MyChildService.anyDoing();
}
Внедрение родителя
В сервисах можно применять ещё один паттерн наследования — внедрение родительского сервиса через DI (Dependency Injection) и создание нового объекта, наследующего от родительского через прототип:
module.factory('ParentService', function ($http) {
return {
// API, доступное снаружи
};
});
module.factory('MyChildService', function (ParentService, $sce) {
var child = Object.create(ParentService);
child.childMethod = function () {
// расширение родителя
};
return child;
});
Всю прелесть этого подхода мы увидим, когда нам потребуется внедрять какие-нибудь зависимости в родительский сервис перед наследованием от него.
module.service
Обычно я работаю с переменными, находящимися в scope, и специальными сервисами, которые инкапсулируют в себе бизнес-логику — Модели.
Эти сервисы по своей реализации чем-то похожи на паттерн ActiveRecord. Мои модели обычно отвечают за общение с сервером напрямую или через некий шлюз. Для создания таких моделей я предпочитаю использовать метод module.service. Внутри эти сервисы создаются через инстанцирующий метод.
Почему я предпочитаю использовать метод service вместо factory? Ну… возможно, я заблуждающийся разработчик, который не понимает истинной силы прототипного наследование, используемого с литералом объекта, но я предпочитаю использовать для моделей наследование на классах. Используя его, я могу создать набор функций-конструкторов, которые достаточно хорошо представляют мою доменную модель. В один прекрасный день я могу решить использовать MDD (Model Driven Development) и генерировать все мои модели из каких-нибудь UML-диаграммы.
Вот пример того, как вы можете получить преимущество от паттерна наследования на классах для AngularJS-сервисов, созданных через module.service:
function Man(name) {
this.name = name;
}
Man.prototype.talk = function () {
return 'Меня зовут ' + this.name;
};
Man.$inject = ['name'];
function Programmer(name, abilities) {
Man.call(this, name);
this.abilities = abilities;
}
Programmer.prototype = Object.create(Man.prototype);
Programmer.prototype.writeCode = function () {
return 'I\'m writing code...';
};
Programmer.$inject = ['name', 'Abilities'];
angular.module('app').service('Man', Man);
angular.module('app').service('Programmer', Programmer);
angular.module('app').value('name', 'Mega Developer');
angular.module('app').value('Abilities', ['Java', 'PHP', 'JavaScript']);
Вот и всё! Надеюсь, тебе моя статья помогла, и ты обязательно организуешь свой JS код с максимальным переиспользованием кода! Наследуйся правильно и будет тебе счастье
Делись ссылкой с друзьями и читай другие статьи моего блога.
До новых встреч!