AngularJS: Как структурировать контроллеры и фабрики/службы с помощью богатой иерархической объектной модели?
Я прочитал эти две великие статьи:
Состояние контроллеров angularjs Джонатана Кримера
и
Переосмысление контроллеров AngularJS от Тодда Мотто
В этих статьях авторы говорят о правильном способе использования контроллеров (что делает их анемичными мостами между представлением и моделью) и фабриками/службами (где бизнес-логика должна действительно жить).
Это отличная информация, и я был очень рад начать реорганизацию контроллеров в одном из моих проектов, но я быстро обнаружил, что структура, показанная в статьях, ломается, если у вас есть богатая объектная модель.
Здесь приведен пример настройки из "Rethinking Angularjs Controllers":
Здесь контроллер:
app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {
var vm = this;
vm.messages = InboxFactory.messages;
vm.openMessage = function (message) {
InboxFactory.openMessage(message);
};
vm.deleteMessage = function (message) {
InboxFactory.deleteMessage(message);
};
InboxFactory
.getMessages()
.then(function () {
vm.messages = InboxFactory.messages;
});
});
и здесь factory:
app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) {
factory.messages = [];
factory.openMessage = function (message) {
$location.search('id', message.id).path('/message');
};
factory.deleteMessage = function (message) {
$http.post('/message/delete', message)
.success(function (data) {
factory.messages.splice(index, 1);
NotificationFactory.showSuccess();
})
.error(function () {
NotificationFactory.showError();
});
};
factory.getMessages = function () {
return $http.get('/messages')
.success(function (data) {
factory.messages = data;
})
.error(function () {
NotificationFactory.showError();
});
};
return factory;
});
Это замечательно, и поскольку providers
(factory) являются одноточечными, данные поддерживаются в разных представлениях и могут быть доступны без необходимости перезагрузки из API.
Это работает просто отлично , если messages
- объект верхнего уровня. Но что происходит, если это не так? Что делать, если это приложение для просмотра входящих почтовых ящиков других пользователей? Возможно, вы являетесь администратором, и вы хотите иметь возможность управлять и просматривать почтовые ящики любого пользователя. Возможно, вам нужно одновременно загружать почтовые ящики нескольких пользователей. Как это работает? Проблема заключается в том, что сообщения inbox хранятся в службе, т.е. InboxFactory.messages
.
Что делать, если иерархия выглядит так:
Organization
|
__________________|____________________
| | |
Accounting Human Resources IT
| | |
________|_______ _____|______ ______|________
| | | | | | | | |
John Mike Sue Tom Joe Brad May Judy Jill
| | | | | | | | |
Inbox Inbox Inbox Inbox Inbox Inbox Inbox Inbox Inbox
Теперь messages
находятся на нескольких уровнях в иерархии и не имеют никакого значения сами по себе. Вы не можете сохранять сообщения в factory, InboxFactory.messages
, потому что вам нужно получать сообщения для нескольких пользователей за раз.
Теперь у вас будут OrganizationFactory, DepartmentFactory, UserFactory и InboxFactory. Получение "сообщений" должно быть в контексте user
, который находится в контексте department
, который находится в контексте organization
. Как и где должны храниться данные? Как его можно получить?
Итак, как это должно быть разрешено? Как структурировать контроллеры, фабрики/службы и богатые объектные модели?
В этот момент, по моему мнению, я склоняюсь к тому, чтобы держать его в курсе и не иметь богатую объектную модель. Просто сохраните объекты в области $, введенной в контроллер, и если вы перейдете к новому представлению, перезагрузите его из API. Если вы нуждаетесь, некоторые данные сохраняются в представлениях, вы можете построить этот мост с помощью службы или factory, но это не должно быть так, как вы делаете большинство.
Как другие решили это? Существуют ли какие-либо шаблоны для этого?
Ответы
Ответ 1
После того, как MUCH переделывает и пытается использовать разные подходы, мое окончательное решение заключается в том, что вы не должны сохранять свою богатую объектную модель по представлениям.
Я держу объектную модель супер-тощей и загружаю только то, что мне нужно для каждого вида. Есть данные высокого уровня, которые я поддерживаю (данные пользователя, такие как имя, идентификатор, электронная почта и т.д., А также данные организации, например, в какой организации они вошли), но все остальное загружается для текущего представления.
С помощью этого тощего подхода, как бы выглядел мой factory:
app.factory('InboxFactory', function InboxFactory ($location, NotificationFactory) {
factory.messages = [];
factory.openMessage = function (message) {
$location.search('id', message.id).path('/message');
};
factory.deleteMessage = function (message) {
$http.post('/message/delete', message)
.success(function (data) {
NotificationFactory.showSuccess();
return data;
})
.error(function () {
NotificationFactory.showError();
return null;
});
};
factory.getMessages = function (userId) {
return $http.get('/messages/user/id/'+userId)
.success(function (data) {
return data;
})
.error(function () {
NotificationFactory.showError();
return null;
});
};
return factory;
});
И контроллер:
app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {
var vm = this;
vm.messages = {};
vm.openMessage = function (message) {
InboxFactory.openMessage(message);
};
vm.deleteMessage = function (message) {
InboxFactory.deleteMessage(message);
};
InboxFactory
.getMessages(userId) //you can get the userId from anywhere you want.
.then(function (data) {
vm.messages = data;
});
});
Преимущества:
- упрощенная прикладная логика
- худой и средний, легкий (я загружаю только то, что мне нужно для текущего состояния)
- меньше использования памяти, что обеспечивает общую производительность, особенно на мобильных устройствах.
Ответ 2
Вы можете использовать богатую объектную модель, но для объектов, которые не являются топ-уровнями, их фабрики должны выставлять api для создания новых экземпляров, а не для использования в качестве одиночных. Это несколько противоречит дизайну многих приложений, которые вы видите в наши дни, которые более функциональны, чем объектно-ориентированные. Я не комментирую плюсы и минусы любого подхода, и я не думаю, что Angular заставляет вас принять тот или иной.
Ваш пример, переработанный, в псевдокоде:
app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {
var inbox = InboxFactory.createInbox();
$scope.getMessages = function(){
inbox.getMessages()
.then(...)
$scope.deleteMessages = function(){
inbox.deleteMessages()
.then(...)
});
Ответ 3
Ваша ситуация становится намного проще, если вы примете подход на основе маршрута (a la ngRoute
или что-то подобное). Рассмотрим эту альтернативу - предупреждение непроверенного кода:
app.config(function($routeProvider) {
$routeProvider
.when('/inbox/:inboxId',
templateUrl: 'views/inbox.html',
controller: 'InboxCtrl',
controllerAs: 'inbox',
resolve: {
inboxMessages: function(InboxFactory) {
// Use use :inboxId route param if you need to work with multiple
// inboxes. Taking some libery here, we'll assuming
// `InboxFactory.getMessages()` returns a promise that will resolve to
// an array of messages;
return InboxFactory.getMessages();
}
}
// ... other routes
.otherwise: {
// ...
};
});
app.controller('InboxCtrl', function InboxCtrl (InboxFactory, inboxMessages) {
var vm = this;
vm.messages = inboxMessages;
vm.openMessage = InboxFactory.openMessage;
vm.deleteMessage = InboxFactory.deleteMessage;
});
Посмотрите, насколько тонкий контроллер сейчас! Конечно, я использовал несколько более компактный синтаксис в нескольких местах, но это подчеркивает, как наш контроллер действительно просто склеивает вещи.
Мы можем еще больше упростить процесс, избавившись от InboxFactory.messages
, когда мы его фактически будем использовать? Нам гарантируется, что он будет заполнен после того, как InboxFactory.getMessages
будет разрешен, так что пусть это обещание будет разрешено самим сообщениям.
Сохранение данных в одиночных точках таким образом может быть самым простым решением в некоторых случаях, но это затрудняет жизнь, когда эти данные должны быть получены на лету. Вам будет лучше отказаться от API и фабрик (как предлагает AlexMA), вытаскивая необходимые данные всякий раз, когда маршрут изменяется (например, пользователь хочет посмотреть на другой почтовый ящик) и вводить эти данные непосредственно в соответствующий контроллер.
Другим преимуществом этой формы является получение наших данных в момент создания экземпляра контроллера. Нам не нужно жонглировать асинхронными состояниями или беспокоиться о том, чтобы вводить много кода в обратные вызовы. В качестве следствия мы обнаруживаем ошибки загрузки данных перед отображением нового представления "Входящие", и пользователь не застревает в полуиспеченном состоянии.
В дополнение к точке вашего вопроса, хотя обратите внимание, что бремя знания того, как ваша богатая модельная модель сочетается, уже не проблема контроллера. Он просто получает некоторые данные и предоставляет кучу методов для представления.