在AngularJS中编写指令时,我如何决定是否需要没有新作用域,新的子作用域或新的隔离作用域?

266
我正在寻找一些指南,可以帮助确定编写新指令时应使用哪种类型的作用域。理想情况下,我希望得到类似流程图的东西,通过一堆问题引导我,然后得出正确的答案——不需要新的作用域、新的子作用域或新的隔离作用域——但这可能要求过高了。这是我目前微不足道的一些指南: 我知道在元素上使用隔离作用域的指令会强制所有同一元素上的其他指令使用相同的(一个)隔离作用域,这是否严重限制了隔离作用域的使用范围?
我希望来自Angular-UI团队(或其他编写多个指令的开发者)能分享他们的经验。
请不要回答“为可重用组件使用隔离作用域”的简单答案。

你所说的“子作用域”,是指通过“scope.$new()”在链接函数中创建作用域吗?因为据我所知,指令可以有隔离作用域,也可以没有它(因此将使用它所在的作用域)。 - Valentyn Shybanov
3
设置 scope: true 将自动创建一个使用 $scope.new() 的子作用域。 - Josh David Miller
2
@Valentyn,就Josh说的而言:所以,三种可能性是 scope: false(默认值,没有新作用域),scope: true(新继承原型的作用域),以及 scope: { ... } (新的隔离作用域)。 - Mark Rajcok
1
是的,谢谢。我错过了“true”和“{}”之间的区别。知道了很好。 - Valentyn Shybanov
有第四种情况,人们通常倾向于忽略...那就是“指令控制器”..我认为问题应该扩展到包括它们...对这个问题的赞成。 - ganaraj
5个回答

292

多好的问题!我非常愿意听取他人的意见,但以下是我使用的指导方针。

高空作业前提:作用域被用作我们在父控制器、指令和指令模板之间进行通信的“粘合剂”。

父级作用域:scope:false,因此根本没有新的作用域。

我不经常使用这个选项,但正如@MarkRajcok所说,如果指令不访问任何作用域变量(并显然不设置任何作用域变量!),那么在我看来这完全没问题。这对于仅在父指令上下文中使用且没有模板的子指令非常有帮助(虽然总会有例外情况),基本上任何带有模板的内容都不应该共享作用域,因为你固有地将该作用域暴露给了访问和操作(但我相信也有例外情况)。

例如,我最近创建了一个使用我正在编写的SVG库绘制静态矢量图形的指令。它观测两个属性(widthheight)并将它们用于计算,但它既不设置也不读取任何作用域变量,也没有模板。这是一个不需要创建另一个作用域的好用例;我们不需要它,所以为什么要麻烦呢?

但在另一个SVG指令中,我需要使用一组数据并额外存储了一点状态。在这种情况下,使用父级作用域是不负责任的(通常情况下同样如此)。因此...

子级作用域:scope: true

带有子级作用域的指令具有上下文感知能力,并旨在与当前作用域进行交互。

显然,与隔离作用域相比的一个关键优势是用户可以自由地在任何想要使用插值的属性上使用它;例如,在具有隔离作用域的指令上使用“class =”item-type- {{item.type}}“将不起作用(默认情况下),但在具有子作用域的指令上可以正常工作,因为无论插值的内容是什么,默认情况下都可以在父作用域中找到。此外,指令本身可以安全地在其自己的作用域中评估属性和表达式,而不必担心污染或损坏父作用域。
例如,工具提示只是添加的东西;隔离作用域不起作用(默认情况下,请参见下文),因为我们预计会在这里使用其他指令或插入的属性。工具提示只是一个增强功能。但是,工具提示还需要在作用域上设置一些内容,以便与子指令和/或模板一起使用,并显然管理自己的状态,因此使用父作用域会非常糟糕。我们要么污染它,要么破坏它,这两者都不好。
我发现自己更经常使用子作用域而不是隔离或父作用域。
隔离作用域: scope: {} 这是为可重用组件设计的。 :-)
但说真的,我认为“可重用组件”指的是“自包含组件”。它们的意图是要用于特定的目的,因此将其与其他指令组合或添加其他插值属性到DOM节点中根本就没有意义。
更具体地说,为此独立功能提供的任何内容都是通过在父作用域中评估指定的属性提供的;它们可以是单向字符串('@'),单向表达式('&'),或双向变量绑定('=')。
对于独立组件来说,需要应用其他指令或属性在其上是没有意义的,因为它本身存在。它的样式由自己的模板(如果需要)控制,并且可以传递相应的内容(如果需要)。它是独立的,所以我们将它放在隔离作用域中,也可以说:“不要搞乱它。我通过这些少数属性为您提供了定义的API。”一个好的最佳实践是尽可能地从指令链接和控制器函数中排除模板相关的内容。这提供了另一个“类似API”的配置点:指令的用户可以简单地替换模板!功能保持不变,其内部API也从未被触及,但我们可以根据需要更改样式和DOM实现。ui/bootstrap是一个很好的例子,因为Peter和Pawel非常出色。
隔离作用域也非常适合与插入使用。以标签页为例;它们不仅具有整个功能,而且可以自由地评估其内部的任何内容,同时使标签页(和窗格)可以执行他们想要的任何操作。标签页显然有自己的状态,属于作用域(与模板交互),但该状态与其所在的上下文无关-它完全是使标签指令成为标签指令的内部状态。此外,使用其他指令与标签页没有太多意义。它们就是标签-我们已经获得了那个功能!
包装更多功能或插入更多功能,但指令已经是它所是了。
所有这些说法,我应该指出,有一些绕过隔离作用域的限制(即功能)的方法,正如@ProLoser在他的答案中暗示的那样。例如,在子作用域部分,我提到在使用隔离作用域时(默认情况下),对非指令属性进行插值会破坏它们。但是用户可以简单地使用class="item-type-{{$parent.item.type}}",它会再次起作用。因此,如果有充分的理由使用隔离作用域而又担心这些限制中的一些,知道如果需要,您可以绕过几乎所有这些限制。
总结:

没有新作用域的指令是只读的;它们完全可以信任 (即内部应用程序),并且它们不会影响到其他东西。有子作用域的指令添加功能,但它们并不是唯一的功能。最后,孤立作用域是用于整个目标是指令的情况;它们是独立的,所以让它们变得不受约束是可以的(也是最“正确”的)。

我想先把我最初的想法说出来,但是随着我的思考,我会更新这个想法。不过天啊 - 对于一个SO答案来说,这个问题太长了...


PS:完全离题,但既然我们谈论到作用域,我更喜欢说“原型”而其他人更喜欢“原型式”,尽管它似乎更准确,但发音却不太好听。 :-)


@MarkRajcok 对于(1),我进行了一些修改,使其更加清晰明了 - 如果我失败了,请告诉我。对于(2),那是一个打字错误和措辞不当的组合;我重新写了那一段,使其更加清晰。我还添加了一个或两个额外的例子,澄清了更多细节,并纠正了一些打字错误。 - Josh David Miller
如答案中所提到的,Angular 的 Bootstrap 是将它们结合起来的一个很好的例子。我发现手风琴示例特别有用 - GitHub - 手风琴 - CalM
你提到你最常使用子作用域,我认为可重用指令模式是最常见的,因此我避免编写只被使用一次的指令。这样做是不必要的吗?有时当我的HTML变得太大时,我想将该部分移到指令中,但它只会被使用一次,所以我就把它留在HTML中了。 - user2483724
2
一个非常普遍的误解是,“可重用”的指令是使用隔离作用域的指令;事实并非如此。如果您查看预打包的指令,几乎没有一个使用隔离作用域 - 有些甚至没有子作用域 - 但我向您保证它们是可重用的!规则应该在指令内部使用的作用域上。如果只是为了节省文件空间,我不确定指令是否是最佳方法。这会增加开发人员的处理时间。但是,如果必须这样做,请继续。或者使用 ngInclude。或者将其作为构建的一部分来完成。有很多选择! - Josh David Miller
@JoshDavidMiller 非常好知道那个常见的误解。我需要更好地掌握指令的基础知识!谢谢 :) - user2483724
显示剩余3条评论

53

我的个人策略和经验:

隔离:一个私有沙盒

我想要创建很多作用域方法和变量,这些方法和变量只能被我指令使用,并且从未被用户看到或直接访问。我想要白名单控制哪些作用域数据对我可用。我可以使用传输(transclusion)允许用户跳回到父级作用域(不受影响)。不希望在传输的子元素中访问到我的变量和方法。

子级:内容的一个子部分

我想要创建作用域方法和变量,可以被用户访问,但与指令上下文之外的包容作用域(兄弟和父级)无关。同时我也想要所有的父级作用域数据自动向下传递。

无:简单的只读指令

我不需要处理作用域方法或变量。我可能正在做一些与作用域无关的事情(比如显示简单的jQuery插件,验证等等)。

注意事项

  • 你不应该让ngModel或其他东西直接影响你的决策。你可以通过做一些像是ng-model=$parent.myVal(子级)或ngModel: '='(隔离)的事情来规避奇怪的行为。
  • 隔离 + 传输将恢复兄弟指令的所有正常行为,并返回到父级作用域,所以不要让这个影响你的判断。
  • 不要在上搞乱作用域,因为那就像是给DOM下半部分而不是上半部分放置数据,这毫无意义。
  • 要注意指令的优先级(没有具体的例子说明它如何影响事情)
  • 注入服务或使用控制器来跨指令通信,任何作用域类型都可以。您还可以执行require:'^ngModel'以查找父元素。

1
我可能误解了这部分内容:“隔离+转移将恢复所有兄弟指令的正常行为”。请参见[此plunker](http://plnkr.co/edit/bpopdGaYcQE3OtnHWJcK?p=preview)。您需要查看控制台。 - Josh David Miller
1
感谢ProLoser提供的见解/答案。如果我添加了angularjs-ui标签,你就是我希望看到这篇文章的人之一。 - Mark Rajcok
@JoshDavidMiller 当在同一DOM元素上讨论指令时,事情变得更加复杂,您应该开始查看优先级属性。插入更与子内容相关。 - ProLoser
1
@ProLoser 没错,但我不确定您的声明的意思是什么。显然它们会影响子级指令,但指令范围如何影响它们的兄弟指令呢? - Josh David Miller

19

写了很多指令后,我决定使用较少的 隔离 作用域。虽然它很酷,可以封装数据并确保不会泄漏到父作用域中,但严重限制了您可以一起使用的指令数量。

如果您要编写的指令完全独立运行,不与其他指令共享,请使用 隔离作用域。(像一个组件,您只需将其插入,对最终开发人员没有太多自定义)。 (这在尝试编写其中包含指令的子元素时会变得非常棘手)

如果您要编写的指令只是进行 DOM 操作,不需要作用域内部状态或明确的作用域更改(主要是非常简单的事情);请使用无新作用域。(例如 ngShowngMouseHoverngClickngRepeat

如果您要编写的指令需要更改父作用域中的某些元素,但也需要处理一些内部状态,请使用 新子作用域。(例如 ngController

务必查看指令的源代码:https://github.com/angular/angular.js/tree/master/src/ng/directive,它极大地帮助了解如何思考它们。


如果多个组件需要相互通信,它们可以具有隔离的作用域并使用“require”,从而使您的指令保持解耦。那么它如何限制可能性?它甚至使指令更加具体(因此声明您所依赖的内容)。因此,我只会留下一个规则:如果您的指令具有状态或需要从其使用的作用域中获取某些数据-请使用隔离作用域。否则不要使用作用域。至于“子作用域”-我也编写了很多指令,从未需要过此功能。如果“需要更改父作用域中的某些元素”-请使用绑定。 - Valentyn Shybanov
1
在同一元素中,您不能请求多个隔离作用域,除非您明确地绑定它们,否则无法访问父作用域中的函数(祝您使用ngClick等好运)。我同意要求创建了一种解耦,但您仍然需要注意父指令。除非它像组件一样,否则我反对采用隔离。指令(至少大多数指令)旨在高度可重用,而隔离会破坏这一点。 - Umur Kontacı
只是一个简短的问题:当我想要重用某些内容时,为什么隔离是不好的?哦,还有我从 Angular 文档中了解到 ngRepeat 会创建新的作用域,我错了吗? - F Lekschas
@Flek - 我不认为隔离作用域在想要重用指令时是坏的。但并不总是需要使用它们。请参阅我的答案,了解何时应该使用它们。是的,ngRepeat会创建一个新的(子)作用域。 - Josh David Miller
5
首先,我认为你对隔离作用域的批评有些过于苛刻了。我认为它们具有比你所认为的更广泛的适用性,并且有许多方法可以避免你(正确地)指出的使用它们时面临的许多挑战。我也不同意“最终开发人员没有太多定制”的说法-有关详细信息,请参见我的回答。话虽如此,你的回答既不差也不错误,并且回答了问题,所以我不确定为什么会被投下负分。因此,+1。 - Josh David Miller
显示剩余5条评论

10

我想分享一下我的理解,并将其与其他JS概念联系起来。

默认情况(例如未声明或范围:false)

这在哲学上等同于使用全局变量。指令可以访问父控制器中的所有内容,但同时也会受到影响。

范围:{}

这就像一个模块,它需要显式传递任何要使用的内容。如果您使用的每个指令都是隔离作用域,那么它就相当于使您编写的每个JS文件都成为自己的模块,需要注入所有依赖项。

范围:child

这是全局变量和显式传递之间的折中方案。它类似于JavaScript的原型链,只是扩展了父级作用域的副本。如果创建了隔离作用域并传递了父作用域的每个属性和函数,则在功能上等效于此。


关键是任何指令都可以以任何方式编写。不同的范围声明只是为了帮助您组织代码。您可以使所有内容都成为模块,也可以只使用所有全局变量并非常小心。但出于易于维护的考虑,最好将逻辑模块化为逻辑上相关的部分。在开放式草地和封闭式监狱之间存在平衡。 我认为这很棘手的原因是,当人们了解这一点时,他们认为他们正在学习指令如何工作,但实际上他们正在学习代码/逻辑组织。

另一个帮助我理解指令如何工作的事情是学习ngInclude。 ngInclude可帮助您包含HTML部分。当我开始使用指令时,我发现可以使用其模板选项来减少代码量,但我并没有真正附加任何逻辑。

当然,在Angular的指令和angular-ui团队的工作之间,我还没有必要创建自己的具有实质性功能的指令,因此我对此的看法可能完全错误。


3
我同意Umur的观点。理论上,隔离作用域听起来很棒,也很“便携”,但在构建我的应用程序以涉及非平凡功能时,我需要合并多个指令(有些嵌套在其他指令内或向它们添加属性),以完全编写自己的HTML,这是特定领域语言的目的。
最终,必须通过每个DOM调用指令上的多个属性将每个全局或共享值传递到链中(如隔离范围所需)。在DOM中反复编写所有这些内容看起来很愚蠢,感觉效率低下,即使这些是共享对象。这也不必要地使指令声明复杂化。使用$parent来“向上”抓取指令HTML中的值的解决方法似乎很糟糕。
我也改变了我的应用程序,大部分子范围指令只有很少的隔离 - 除了那些不需要访问父级中除了可以通过简单、非重复属性传递的任何东西之外的隔离。
在出现这种情况之前,我曾经梦想过特定领域语言几十年,我很高兴AngularJS提供了这个选项,并且我知道,随着更多的开发人员在这个领域工作,我们将看到一些非常酷的应用程序,这些应用程序也容易为它们的架构师编写、扩展和调试。
-- D

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