AngularJS: 为什么不在controller中写逻辑?

AngularJS: Why not write logic in controller?

如果这听起来很愚蠢,请原谅我,但我已经使用 AngularJS 一段时间了,到处都看到人们告诉我 将我的逻辑包装在一个指令(或服务?)而不是我的控制器,只保留我控制器中的绑定。除了指令的可重用性方面还有其他原因吗?

直到现在我还没有真正理解为什么会这样。编写指令不会带来很多开销吗?我在我的控制器中编写逻辑时没有遇到任何问题,而且很容易。 这种方法的缺点是什么?

我将逻辑排除在控制器之外有两个很好的理由:

可重用性

如果您的应用程序有多个控制器,并且每个控制器都做几乎相同的事情,但有一些不同,那么在控制器中保留逻辑意味着您将重复编写的代码。如果您 Don't Repeat Yourself. By putting that logic into a service you can inject the same code into multiple controllers. Each service (really a Factory) is created as a new instance of itself each time it is injected into a controller. By pushing logic into a service you can modularise 您的代码会更好,这样更容易维护和测试(见下文)

测试

好的代码已经过测试。不仅仅是人,还有你写的单元测试。作为开发人员,单元测试向您保证您的代码也按照您的预期进行。它们还可以帮助您很好地设计代码。

如果您的控制器有 20 个不同的方法,每个方法都有自己的逻辑,那么测试(和您的代码)就会变成意大利面条。

编写狭窄的单元测试更容易,即它们一次测试一件事。幸运的是,将您的代码分解成封装的部分也很好(出于上述原因),即它们只做一件事并且可以孤立地完成。所以单元测试(特别是如果你先写你的测试)迫使你考虑如何将你的代码分解成可维护的部分,如果你想在未来进行更改,这会使你的应用程序处于良好状态(你 运行 单元测试,可以看到哪里出了问题)。

例子

表格申请:

您有一个提供多个表单的表单应用程序。每个表单都有一个控制器。当用户提交表单时,数据通过代理发送到 CRM,后者将信息存储在数据库中。

如果客户已经存在于 CRM 中,您不想创建重复项(是的,CRM 应该处理数据清理,但您希望尽可能避免这种情况)。因此,一旦用户提交了他们的表单数据,就需要实现类似这样的逻辑:

  • 通过 API 端点在 CRM 中搜索用户
  • 如果用户存在,获取用户 ID 并将其与表单数据一起传递到另一个端点
  • 如果它们不存在,请点击另一个端点并创建一个新用户,获取用户 ID 并发送它和表单数据以将其与用户相关联

注意:可以说以上所有内容都应该由后端服务完成,但为了举例,让我们继续吧。

您的申请有多种形式。如果您在每个表单的每个控制器中硬编码相同的逻辑(是的,您应该每个表单都有一个控制器,即每个视图一个),那么您将重复自己多次。编写需要检查控制器的测试可以完成基础工作(post 数据,管理对视图的更改),但也可以测试每个控制器的所有逻辑。

或者只编写一次该逻辑,将其放入服务中,为其编写一个测试并将其注入到任何你喜欢的地方。

参考资料

看看 Angular documentation, and look at the patterns that Angular implements and why these are good to follow (Design Patterns - 最大的是模块化、依赖注入、工厂和单例)。

控制器是执行所有与范围相关的所有事情的正确位置。这是你写所有

的地方
$scope.$watch(...)

并定义您需要从视图访问的所有 $scope 函数(如事件处理程序)。通常,事件处理程序是一个计划函数,它将依次调用函数服务。

$scope.onLoginButtonClick = function(){
    AuthenticationService.login($scope.username,
        $scope.password);
};

在极少数情况下,您可以在其中添加承诺成功处理程序。

不要:在控制器中编写业务逻辑

前面的例子之所以如此,有一个非常具体的原因。它向您展示了一个 $scope 函数,该函数依次调用服务中的函数。控制器不负责登录机制或登录如何发生。如果你在服务中编写这段代码,你就是在将服务与控制器解耦,这意味着你想在其他任何地方使用相同的服务,你需要做的就是注入并触发该功能。

控制器未来的规则:

  • 控制器应保持零逻辑 控制器应仅将引用绑定到模型(并调用从 promises 返回的方法)
  • 控制器只将逻辑整合在一起
  • Controller 驱动 Model 变化,View 变化。关键词;驱动器,而不是 creates/persists,它会触发它们!
  • 委托更新工厂内部的逻辑,不解析控制器内部的数据,仅使用更新的工厂逻辑更新控制器的值,这避免了跨控制器重复代码以及工厂测试更容易
  • 保持简单,我更喜欢 XXXXCtrl 和 XXXXFactory,我很清楚这两者的作用,我们不需要给事物起花哨的名字
  • 保持 method/prop 名称在共享方法之间保持一致,例如 this.something = MyFactory.something;否则会变得混乱
  • 工厂保存模型,更改、获取、更新和保存模型更改
  • 将工厂视为需要持久化的对象,而不是在控制器中持久化
  • 与工厂内的其他工厂交谈,让他们远离 Controller(诸如 success/error 处理之类的事情)
  • 尽量避免将 $scope 注入到控制器中,通常有更好的方法来做你需要的,比如避免 $scope.$watch()

控制器最大的问题是你没有定义它工作的html。

当您使用...

<div ng-controller="myController"></div>

...然后你必须在你的控制器中注入你的 html,这基本上是老式的 jQuery 想法。

当您使用...

<div ng-controller="myController">... some html ...</div>

...您的指令和它所作用的 html 在不同的地方定义。也不是你想要的。

使用指令强制你把你的 html 和它需要的代码放在同一个地方。因为该指令也有它自己的作用域,所以不会对代码中其他地方的其他变量产生任何干扰。如果你需要来自其他地方的变量,你也必须明确地注入它们,这也是一件好事。

我用来解释为什么这是一件好事的词是 'atomic',但我不确定这个词是否正确。意思是:所有应该协同工作的东西都在一个文件中。使用 templateUrl 这不再完全正确,模板仍然在指令中定义。

所以在我的控制器中没有代码可以对 dom 做任何事情。只是最低限度的,比如一些 page/view 计数代码,或者将 API 数据连接到范围,或者用 $routeParam 数据做一些事情。所有其他代码都放在 Services/Factories(业务逻辑)或指令(dom 逻辑)中。

顺便说一句:可以为您的指令定义一个控制器,但通常只用于 'inter-directive communication'(因此它们可以共享状态),但您只能将其与始终协同工作的指令一起使用(例如在 tabs 指令中重复的 tab 指令。

您不在控制器中编写逻辑的主要原因是 $scopescontroller 中收集垃圾 $destroy() 路由更改。在接收到 $routeChangeSuccess 广播时的 ngView 指令中,有一个函数只保留当前活动视图的 $scope,所有其他 $scope 都被销毁。

因此,例如,如果您有一个购物车应用程序并且您的业务逻辑是使用 $scopes 的控制器,如果用户使用后退按钮,他们将丢失产品和已经在订单页面上输入的所有表单数据等