AngularJS:如何在具有丰富的层级对象模型的情况下组织控制器和工厂/服务?

30

我读了这两篇很棒的文章:

《AngularJS控制器的状态》,作者是Jonathan Creamer。

《重新思考AngularJS控制器》,作者是Todd Motto。

在这些文章中,作者们谈到了正确使用控制器(将它们作为连接视图和模型的贫血桥梁)和工厂/服务(业务逻辑应该真正存在的地方)的方法。

这是很好的信息,我很兴奋地开始重构我的一个项目上的控制器,但我很快发现,如果你有一个丰富的对象模型,那么这些文章中展示的结构会出现问题。

以下是从“重新思考AngularJS控制器”中的设置摘要:

这是控制器:

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;
    });

});

这是工厂:

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(工厂)是单例模式,因此数据在视图之间保持,并且可以访问而无需从API重新加载。

如果messages不是顶级对象会怎么样呢?如果这是一个用于浏览其他用户收件箱的应用程序呢?也许您是管理员,希望能够管理和浏览任何用户的收件箱。也许您需要同时加载多个用户的收件箱。这该怎么办?问题在于收件箱消息存储在服务中,即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 在层次结构中有几个级别,本身没有意义。您无法将消息存储在工厂中,InboxFactory.messages,因为您必须一次为多个用户检索消息。

现在,您将拥有一个OrganizationFactory、DepartmentFactory、UserFactory和InboxFactory。检索“messages”必须处于user的上下文中,而该上下文又处于department的上下文中,department又处于organization的上下文中。数据应该存储在何处?如何检索它?

那么这应该如何解决?控制器、工厂/服务和丰富的对象模型应该如何构建?

在我的思考中,我倾向于保持简洁,不使用丰富的对象模型。只需将对象存储在注入到控制器中的$scope上,如果要导航到新视图,则从API重新加载。如果您需要跨视图保留某些数据,可以使用服务或工厂构建该桥梁,但这不应该是您完成大部分事情的方式。

其他人如何解决这个问题?是否有什么模式可用?


这不应该是后端端点的任务吗?因此,如果您以Jonh的身份登录,则GET /messages仅返回John的消息,并且界面与组织老板在登录时看到的界面不同。 - terafor
3个回答

3

您可以使用丰富的对象模型,但对于不是顶层的对象,它们的工厂应该提供API来创建新的实例,而不应该作为单例使用。这与许多现代应用程序的设计有点矛盾,这些应用程序更加功能化而不是面向对象 - 我没有评论任何方法的优缺点,我也不认为Angular会强制您采用其中之一。

您的示例代码经过重新设计,伪代码如下:

app.controller('InboxCtrl', function InboxCtrl (InboxFactory) {

   var inbox = InboxFactory.createInbox();

   $scope.getMessages = function(){
      inbox.getMessages()
           .then(...)

   $scope.deleteMessages = function(){
      inbox.deleteMessages()
           .then(...)

});

谢谢AlexMa。这就是我倾向于的做法。如果这样做,你是否主张将对象图在视图之间持久存储在某个地方?或者你更愿意主张根据需要为每个适用的视图重新创建图形?我倾向于后者,以保持应用程序轻巧和轻量级。 - richard
1
我同意;我尽量避免让应用程序状态比必要的更复杂,因为这会产生副作用,并且难以在后期进行测试和更改。 - AlexMA

2
如果你采用基于路由的方式(例如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解决后保证它被填充,所以让这个Promise直接解决为消息本身。
以这种方式将数据存储在单例中可能是某些情况下最简便的解决方案,但当需要即时获取数据时,这会使生活变得困难。最好依靠API和工厂(如AlexMA建议的那样),每当路由改变时(例如用户想查看不同的收件箱)拉取必要的数据,并将该数据直接注入适当的控制器中。
这种形式的另一个好处是我们可以在控制器实例化时拥有我们的数据。我们不必处理异步状态或担心将大量代码放在回调中。作为一个推论,我们在显示新的收件箱视图之前就可以捕捉到数据加载错误,并且用户不会陷入半成品状态。
进一步说明你的问题,注意你的丰富模型结构如何适配在一起的负担不再是控制器的问题。它只获取一些数据并向视图公开一堆方法。

Angular UI Router提供了嵌套路由,并且非常方便用于那些使用案例。 - Eloims

0

经过大量的调试和尝试不同的方法,我的最终决定是你不应该在视图之间持久化你的丰富对象模型。

我保持对象模型超级精简,并仅加载每个视图所需的内容。有一些高级数据我会保留(例如用户信息,如姓名、ID、电子邮件等,以及组织数据,例如他们登录的组织),但其他所有内容都会为当前视图加载。

采用这种精简的方法,我的工厂看起来就像这样:

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;
  });

});

到目前为止,它带来的好处如下:

  • 简化了应用逻辑
  • 轻巧且精简(我仅加载当前状态所需的内容)
  • 更少的内存使用,这导致整体性能更佳,特别是在移动设备上

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接