AngularJS : 控制器和 factories/services 应该如何构建一个丰富的分层对象模型?

AngularJS : How should controllers and factories/services be structured with a rich, hierarchical object model?

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

The state of angularjs controllers 乔纳森·克里默

Rethinking AngularJS Controllers 托德座右铭

在这些文章中,作者讨论了使用控制器的正确方法(使它们成为视图和模型之间的贫血桥梁)和factories/services(业务逻辑应该真正存在的地方)。

这是很好的信息,我真的很高兴开始重构我的一个项目中的控制器,但我很快发现如果你有一个丰富的对象模型,文章中显示的结构就会崩溃。

这里是对 "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;
    });

});

这是工厂:

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 的上下文中,它在一个 organization 的上下文中。数据应该如何存储以及存储在哪里?应该如何取回?

那么应该如何解决呢?控制器、factories/services 和富对象模型应该如何构建?

在我的想法中,我倾向于只保持精简而不是拥有丰富的对象模型。只需将对象存储在注入控制器的 $scope 上,如果导航到新视图,请从 API 重新加载。如果您需要 一些数据跨视图持久化,您可以使用服务或工厂构建该桥梁,但这不应该是您 most 事情。

其他人是如何解决这个问题的?有任何模式吗?

您可以使用丰富的对象模型,但对于非顶级对象,它们的工厂应该公开一个 api 用于创建新实例,而不是用作单例。这与您现在看到的许多应用程序的设计有点相反,它们比面向对象的功能更实用——我不评论这两种方法的优缺点,我不认为 Angular迫使您采用其中之一。

你的例子,重新设计,伪代码:

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

   var inbox = InboxFactory.createInbox();

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

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

});

如果您采用基于路由的方法(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 所建议的那样),在路由发生变化时提取必要的数据(例如,用户想要查看不同的收件箱)并将该数据直接注入适当的控制器。

这种形式的另一个好处是我们可以在实例化控制器时掌握数据。我们不必处理异步状态或担心在回调中放入大量代码。作为必然结果,我们可以在显示新的收件箱视图之前捕获数据加载错误,并且用户不会陷入半生不熟的状态。

除了您的问题之外,请注意了解您的丰富模型结构如何组合在一起的负担不再是控制器的问题。它只是获取一些数据并向视图公开一堆方法。

经过多次修改和尝试不同的方法后,我的最终决定是您不应该跨视图保留丰富的对象模型。

我保持对象模型超级精简,并只加载每个视图所需的内容。我保留了高级数据(用户信息,如姓名、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;
  });

});

到目前为止的好处是:

  • 简化应用程序逻辑
  • 精益求精,轻巧(我只加载当前状态所需的内容)
  • 更少的内存使用,这意味着更好的整体性能,尤其是在移动设备上