We read every piece of feedback, and take your input very seriously.
To see all available qualifiers, see our documentation.
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
原文 | A Guide To Transclusion in AngularJS 作者 | Tero Parviainen
在transclusion(注:本文将transclusion翻译为嵌入,而且从实际的运用来看也比较贴切)方面我一直有困惑。虽然我时不时会用到,但是用起来总觉得不顺手。我从来没有明确的明白这个API做了什么事,或者更确切地说是我应该用它做些什么。
据我所知,并不止我一个人对此感到困惑。当人们谈论在使用Angular的困难的时候,嵌入总是经常被大家提及。
是什么原因造成了这样一个困难的话题呢?从概念上看,我们讨论的并不是什么太复杂的东西——本质上只是移动了一些DOM元素。我认为更大的问题是显而易见的:
直到在我的书(注:本文作者Tero Parviainen写了一本书,叫build-your-own-angularjs,也就是文中作者提到的这一本,书中对angular的各种特性逐一讲解)中写嵌入章节的时候我才感觉完全掌握了这个专题。虽然书中涵盖了API的诸多细节,但这篇文章是为了完成我之前就想写的用户使用指南。我想要阐明可以使用嵌入做什么以及如何去做。
这篇文章不包括API中已经被弃用的部分,而是专注于官方支持的内容。
“嵌入”这个词让许多人看起来不舒服,并且还经常被人诟病这是一个拼装出的词汇。
虽然它是一个拼装的词汇,但并不是由Angular团队创造的,实际上在计算机应用中它还有一段有趣的历史。作为trans(穿过)和inclusion(包含)组合出来的词,我认为这实际上很恰当地描述了其所具备的功能。
transclusion的基本使用场景很简单:当你需要把一个模板中的内容放置到另一个(模板)中的时候。
例如,假如你有一个叫myCard的指令用来呈现一块内容作为“卡片”效果的组件:有一个标题,一个图像,和一些内容。下面展示了不使用嵌入的情况下这样的指令会是什么样子:
myCard
JS Bin
对于这样一个组件来说这是一个很好的开始,但是它有一个缺点:我们必须在my-content属性中填写一个很长的字符串作为卡片的主体内容。而且这样不能使用HTML格式的文本,并且通常会让人感觉很笨拙。
my-content
当<my-card>是一个普通的HTML标签的时候,我们想做的事就非常显而易见了:像HTML一样把内容嵌套其中。
<my-card>
这就是嵌入所做的事情。如果我们在myCard指令中设置transclude:true来开启嵌入特性,Angular就会获取到<my-card>节点中原始的内容,并且这些内容可以被我们放置在模板中的任何位置。
transclude:true
具体点来说,获取到的内容就是在指令的link函数中的第五个参数。(前四个参数分别是节点、作用域、属性和依赖的controller。)
link
第五个参数是一个嵌入方法,大家一般把它命名为transclude或$transclude。
transclude
$transclude
当调用这个函数,会返回经过转化的内容,我们可以将其追加到模板中:
所以这就是transclusion最基础的用法:节点中的内容被置入一个方法中,之后你就可以调用它并把内容填充到其他地方。
transclusion
你可以把其视为使用HTML参数化指令:myCard的“输入参数”之一就是其包含的内容。
如果你不想使用link函数,而是想要使用指令controller,嵌入仍然完全没问题。transclusion函数可以使用$transclude注入到controller中,就可以像link函数的第五个参数一样注入一个额外的函数。
嵌入经常会在含有模板的指令中使用,当然这也不是必须的,你也可以使用嵌入而不是模板。比如这个只有一些简单DOM操作的示例。
在我们之前的例子中,我们使用jQuery选择器找到需要嵌入内容的元素。这样有些笨拙而且在Angular代码中并不常用的。在HTML模板中对要嵌入的元素进行特殊声明是一种更好的方法,就好像说“请把内容放到这里来”。
这实际上是可行的的,因为当你把一个指令设置了transclude: true,所有这个指令的子元素都可以获得它的transclusion函数。所以我们引入一个指令,比如myContentTransclusionPoint,它就可以使用从父级指令中获取的transclusion函数。
transclude: true
myContentTransclusionPoint
不在需要jQuery选择器了,因为HTML的属性就可以指明嵌入的内容需要放置的位置。
获取到的transclusion函数将是遍历DOM树之后找到的“最近的transclusion”。它可能是如第一个例子演示的当前的元素,也可能是像第二个例子展示的父级元素。
ngTransclude
用myContentTransclusionPoint实现的方法太过于普通,也太一般了,Angular实际上有一个指令专门做这样一件事。不再是使用myContentTransclusionPoint,你完全可以使用ngTransclude作为替代:
值得注意的是,这几乎是ngTransclude所能做的全部事情。这没有什么神奇的地方,如果你阅读这部分源代码的话就很明白了。
嵌入对于静态的HTML来说是非常好用的,但是如果你使用的是动态的内容呢?比如,内容本身包含指令或{{数据绑定}}呢?
{{数据绑定}}
幸运的是,正如我们所料,如果我们将外部作用域中的动态数据放置到<my-card>的内容中,也是可以执行的:
虽然大部分情况下事情确实“仅仅工作”,但是最好理解到底发生了什么事情:内容段落现在通过使用嵌入包含到了myCard的模板中,而且myCard有一个独立的作用域,除了data.content属性别的一无所知,内容在那里是如何变得可用的呢?
data.content
答案是Angular如何为transclusion的作用域进行分层的,当你执行transclusion函数的时候,嵌入的内容已经被链接。它们被链接到一个特殊的“内嵌作用域”,这是一个继承自外层<my-card>的作用域的子作用域:
作用域的原型链控制从父作用域传递到子作用域的数据。myCard使用了一个独立的作用域所以它没有原型的父级。然而,在被嵌入(元素)之前,嵌入作用域将其原型指向了原始的内容作用域。
然而,内嵌作用域并不只是一个普通的外部作用域的子作用域,因为这样的话我们会有一个摆脱它的问题:当myCard最终被删除的时候,我们想要同时把内嵌作用域销毁掉。因为我们不希望泄露作用域。Angular作用域是当其父作用域销毁的时候销毁,但是如果我们的内嵌作用域的父作用域是外部作用域,这就不能保证它(父作用域)能保留多久了。
为了处理这个问题,Angular设置内嵌作用域的$parent属性指向了和数据来源作用域不同的一个作用域。$parent是基于这个transclusion函数调用的位置的,在我们的例子中,它就是myCard的局部作用域:
$parent
作用域的$parents决定了它何时被销毁。对于内嵌作用域来说,原型上的父级以及结构上的父级可能甚至经常是不一样的。内嵌作用域的$parent是myCard的独立作用域,因为那是它被嵌入的区域的作用域。
$parents
这种将原型上的父级和结构上的父级分离正是内嵌作用域特殊的地方。然而,正如前文提到的,大多数情况下事情仅仅是工作而不必在作用域层次上思考过于深入。
我们已经知道了transclusion函数如何使用:当你需要被嵌入的DOM的时候,只需要调用这个函数并取得函数的返回值。
同时也有另一个可供选择的方式调用transclusion函数,这种方式不需要方法的返回值,而是传递一个函数作为回调参数。它会执行这个函数并传递嵌入的DOM:
你提供的回调函数被称为克隆附加函数。
既然可以通过返回值得到需要的内容,那这样做的意义在哪里?在上面的实例中,真的没有意义,但是本质上还是有所不同的。
当你使用克隆附加函数的时候会有一个副作用,即被嵌入内容在其被链接以及给予给你之前就会被克隆。给予到克隆附加函数中元素将不是最初被嵌入的完全相同的元素,他们将是原始内容的克隆。
这很重要,尤其是当你想要多次嵌入内容,使用克隆你可以做到这一点。
如果没有克隆附加函数这是无法做到的,如果你多次调用transclusion函数,那每次都会得到相同的DOM元素,如果把它们附加到多个地方,它们只会保留在最后一个地方。继续尝试吧!
ngTransclude指令内部也是使用了克隆附加函数。这意味着你可以在你的模板中多次使用ng-transclude,每一个(使用ng-transclude的)地方都会插入一个被嵌入内容的克隆。
ng-transclude
尽管并不总是需要,但大多数人在嵌入内容的时候都会使用克隆附加函数。这也是Angular文档中所推荐的做法。
克隆附加函数并不仅仅只是一个嵌入功能,因为你也可以在公共的链接函数使用。如果你在手动链接而且想要链接同一个DOM的多个克隆,则只需要传递克隆附加函数作为第二个参数即可。
让我们给我们的myCard指令增加一个新特性——让用户能够收起和展开内容区。当卡片展开的时候,调用transclusion函数来创建并链接一个的副本。当卡片收起的时候,被嵌入的元素会被移除。
这样实现有一个问题:作用域泄露。每次调用transclusion函数,内嵌作用域都会被重新创建。但是当我们清空被嵌入的内容时,Angular不确定是否要销毁(内嵌)作用域,所以作用域会一直保留。
我们应该负责销毁作用域,我们可以做到这个因为transclusion函数会将作用域作为第二个参数传递出来。如果我们能够获得它,那就可以随后调用$destroy()销毁它。
$destroy()
如上述例子所示,当被嵌入的内容先于指令本身被销毁之前,调用内嵌作用域的销毁方法是你唯一需要考虑的事情。如果被嵌入的内容和指令本身的生命周期一致,则前面讨论过的父子作用域关系将保证事情自动被处理。
上面的例子有点勉强,因为我们可以很容易地通过使用ngIf和ngTransclude来完成同样的行为。原本有一个相关的bug,但是在Angular1.3版本中被修复了。
ngIf
还有一个原因可能要使用克隆附加函数:让它在特定的时间点被调用——内嵌作用域创建以及被链接之间。这意味着你可以在作用域中附加内容,并且它们在被嵌入的内容中可以使用。
例如,myCard指令可以提供一个“折叠”功能作为内嵌作用域上的方法。如果你接下来想在被嵌入的内容中增加一个折叠按钮,你可以很容易做到这一点:
克隆附加函数为什么是一个函数而仅仅是一个克隆标志或类似的东西已经开始变得更加说得通了。即当transclusion函数返回的时候,链接已经完成了,但是你想要在作用域链接之前在其中添加内容。
Angular提供了一个API调用,而不是两个不同的API,通过克隆附加函数作为一个回调方法,在正确的时间调用:在嵌入作用域被创建以及被链接之间。
正如前面讨论的,Angular将会为你管理内嵌作用域的创建,所以大多数时候你不必做任何事情就OK了。
但是你可以选择不让Angular创建内嵌作用域而是提供自定义的。transclusion函数可以使用scope对象作为第一个参数(在克隆附加函数之前)。如果你提供一个自定义作用域,就会跳过设置嵌入作用域的过程。
什么时候需要这样做呢?一方面,如果你遇到了无法处理的作用域管理的刁钻问题,这将是一个解决方案。过去人们已经做到了这一点,例如,将transclusion和ngRepeat结合起来使用。
ngRepeat
对此,嵌入还有一个有趣的使用方法:你可以将嵌入内容链接到你独立的作用域而不是范围作用域。因此,指令的部分模板的由嵌入内容提供,但是数据却是通过指令内部支持的。
数据
例如,你可以实现一个类似下面townView的指令,这个指令可以完全控制其显示的数据,但是允许传递一个动态的模板以决定数据如何显示:
townView
如何
请注意以这样的方式使用嵌入会有一些出乎意料的情况:嵌入内容中的表达式无法访问他们看似应该能够使用的数据。因此,我认为这种模式应该被谨慎使用。
现在让我们把注意力放到另一种可以使用嵌入的方式上:使用transclude: 'element'进行元素嵌入。
transclude: 'element'
这种方式和常规方式的区别在于被嵌入的内容是什么:常规的方式是把当前元素的子节点进行嵌入。而元素嵌入是元素本身被嵌入(包括其所有的子节点)。
元素本身
从表面上看不出太大的区别:这种方式只是比普通方式多一个层级。但是,如果我们深入研究就会发现二者有着本质的差异。如果元素本身成为嵌入的一部分,那么DOM中被嵌入内容的容器究竟是什么呢?
有趣的是元素会被嵌入到哪里:嵌入的“元素”实际上消失了。如果你查看DOM,你会看到它被一段HTML注释所替代。该注释节点也是链接函数中接受到的元素。
元素
那么原始的元素发生了什么?答案是它现在由嵌入函数提供。你可以通过调用嵌入函数并在注释之后附加上结果。
如果元素上存在其他指令,各自不同的优先级决定了会发生什么:在正常链接的过程中链接优先级高的指令,但是优先级低的指令只有当你调用嵌入函数的时候才会被链接。
虽然这个练习很有趣,但是我们并没有掌握这一点。元素嵌入到底是用来做什么的呢?
元素嵌入的一个应用场景是当你想要延迟链接部分UI或者附属的内容,直到特定的事情触发。
由于元素嵌入本质上是删除了DOM的一部分,并且使其可以通过使用嵌入函数再放回,所以你可以控制触发的时间。例如,你可能希望仅在父作用域的某些条件变为true的时候才链接DOM。
true
这里本质上是实现了ngIf的简化版,如果你阅读ngIf的源代码,你应该就能理解它做了什么:只有当条件表达式为真时,它才会通过元素嵌入的方式将这部分DOM进行链接。
这样也就解释了为什么当你使用ngIf的时候会有HTML注释,他们实际上是通过元素嵌入的方式(将内容)插入DOM的。
元素嵌入的另一个应用场景是如果你想要多次链接并附加一部分DOM。例如,你可以使用一个遍历数组的指令并渲染数组中每一个项目为DOM子树,同时使每一个子项在当前作用域都可用。
当我们把元素嵌入和克隆附加函数结合一起使用时,这一切都可以实现,因为我们可以为每一个子项的DOM克隆一个新的副本。
这本质上是ngRepeat的(超级)简化版本。原生的ngRepeat指令也是使用了元素嵌入,尽管研究其源代码远不如ngIf简单,因为ngRepeat用法很多而且包含大量优化内容。不过它的核心也只是对元素嵌入以及克隆附加函数的应用罢了。
一个可能使用到元素嵌入的场景是和模板一起使用,并且使用模板来直接替换原本的元素。然而,这并不是最初设计的功能而且这样使用你可能会遇到一些麻烦。对于这些情况,用replace来使用模板是正确的选择,尽管replace已经被弃用了。
replace
元素嵌入被设计使用初衷是为了这样的场景:当你想要延迟、跳过、重复或者其他控制部分UI的链接及渲染。有关这方便更多的信息,请参见@caitp在相关GitHub issue下的精彩评论。
有点不幸的是,用于不同目的的两个特性(常规嵌入和元素嵌入)在API层面看起来是那么的相似。此外,在元素嵌入中,并没有任何东西被真正的嵌入,这使其看起来那么不恰当。
在Angular2里这不再是个问题,因为组件内的内容标签涵盖了常规的嵌入,而核心API涵盖了元素嵌入。(查看当前Angular2中ng-if的实现,它看起来可以控制何时从原型视图创建新的视图。)
译在最后:翻译的很挫,边学边翻~主要还是看用例来理解,推荐看一些用transclusion比较多的组件,比如ui-select
其他参考链接: 彻底弄懂AngularJS中的transclusion - 用Angular开发web应用 - 前端乱炖
The text was updated successfully, but these errors were encountered:
No branches or pull requests
在transclusion(注:本文将transclusion翻译为嵌入,而且从实际的运用来看也比较贴切)方面我一直有困惑。虽然我时不时会用到,但是用起来总觉得不顺手。我从来没有明确的明白这个API做了什么事,或者更确切地说是我应该用它做些什么。
据我所知,并不止我一个人对此感到困惑。当人们谈论在使用Angular的困难的时候,嵌入总是经常被大家提及。
是什么原因造成了这样一个困难的话题呢?从概念上看,我们讨论的并不是什么太复杂的东西——本质上只是移动了一些DOM元素。我认为更大的问题是显而易见的:
直到在我的书(注:本文作者Tero Parviainen写了一本书,叫build-your-own-angularjs,也就是文中作者提到的这一本,书中对angular的各种特性逐一讲解)中写嵌入章节的时候我才感觉完全掌握了这个专题。虽然书中涵盖了API的诸多细节,但这篇文章是为了完成我之前就想写的用户使用指南。我想要阐明可以使用嵌入做什么以及如何去做。
这篇文章不包括API中已经被弃用的部分,而是专注于官方支持的内容。
“嵌入”这个词让许多人看起来不舒服,并且还经常被人诟病这是一个拼装出的词汇。
虽然它是一个拼装的词汇,但并不是由Angular团队创造的,实际上在计算机应用中它还有一段有趣的历史。作为trans(穿过)和inclusion(包含)组合出来的词,我认为这实际上很恰当地描述了其所具备的功能。
基本用法:包含其他模板中的内容
transclusion的基本使用场景很简单:当你需要把一个模板中的内容放置到另一个(模板)中的时候。
例如,假如你有一个叫
myCard
的指令用来呈现一块内容作为“卡片”效果的组件:有一个标题,一个图像,和一些内容。下面展示了不使用嵌入的情况下这样的指令会是什么样子:JS Bin
对于这样一个组件来说这是一个很好的开始,但是它有一个缺点:我们必须在
my-content
属性中填写一个很长的字符串作为卡片的主体内容。而且这样不能使用HTML格式的文本,并且通常会让人感觉很笨拙。当
<my-card>
是一个普通的HTML标签的时候,我们想做的事就非常显而易见了:像HTML一样把内容嵌套其中。这就是嵌入所做的事情。如果我们在
myCard
指令中设置transclude:true
来开启嵌入特性,Angular就会获取到<my-card>
节点中原始的内容,并且这些内容可以被我们放置在模板中的任何位置。具体点来说,获取到的内容就是在指令的
link
函数中的第五个参数。(前四个参数分别是节点、作用域、属性和依赖的controller。)第五个参数是一个嵌入方法,大家一般把它命名为
transclude
或$transclude
。当调用这个函数,会返回经过转化的内容,我们可以将其追加到模板中:
JS Bin
所以这就是
transclusion
最基础的用法:节点中的内容被置入一个方法中,之后你就可以调用它并把内容填充到其他地方。你可以把其视为使用HTML参数化指令:
myCard
的“输入参数”之一就是其包含的内容。如果你不想使用link函数,而是想要使用指令controller,嵌入仍然完全没问题。transclusion函数可以使用
$transclude
注入到controller中,就可以像link函数的第五个参数一样注入一个额外的函数。JS Bin
嵌入经常会在含有模板的指令中使用,当然这也不是必须的,你也可以使用嵌入而不是模板。比如这个只有一些简单DOM操作的示例。
从父指令中嵌入
在我们之前的例子中,我们使用jQuery选择器找到需要嵌入内容的元素。这样有些笨拙而且在Angular代码中并不常用的。在HTML模板中对要嵌入的元素进行特殊声明是一种更好的方法,就好像说“请把内容放到这里来”。
这实际上是可行的的,因为当你把一个指令设置了
transclude: true
,所有这个指令的子元素都可以获得它的transclusion
函数。所以我们引入一个指令,比如myContentTransclusionPoint
,它就可以使用从父级指令中获取的transclusion
函数。JS Bin
不在需要jQuery选择器了,因为HTML的属性就可以指明嵌入的内容需要放置的位置。
获取到的transclusion函数将是遍历DOM树之后找到的“最近的transclusion”。它可能是如第一个例子演示的当前的元素,也可能是像第二个例子展示的父级元素。
使用
ngTransclude
用
myContentTransclusionPoint
实现的方法太过于普通,也太一般了,Angular实际上有一个指令专门做这样一件事。不再是使用myContentTransclusionPoint
,你完全可以使用ngTransclude
作为替代:JS Bin
值得注意的是,这几乎是
ngTransclude
所能做的全部事情。这没有什么神奇的地方,如果你阅读这部分源代码的话就很明白了。理解嵌入作用域
嵌入对于静态的HTML来说是非常好用的,但是如果你使用的是动态的内容呢?比如,内容本身包含指令或
{{数据绑定}}
呢?幸运的是,正如我们所料,如果我们将外部作用域中的动态数据放置到
<my-card>
的内容中,也是可以执行的:JS Bin
虽然大部分情况下事情确实“仅仅工作”,但是最好理解到底发生了什么事情:内容段落现在通过使用嵌入包含到了
myCard
的模板中,而且myCard
有一个独立的作用域,除了data.content
属性别的一无所知,内容在那里是如何变得可用的呢?答案是Angular如何为transclusion的作用域进行分层的,当你执行transclusion函数的时候,嵌入的内容已经被链接。它们被链接到一个特殊的“内嵌作用域”,这是一个继承自外层
<my-card>
的作用域的子作用域:作用域的原型链控制从父作用域传递到子作用域的数据。
myCard
使用了一个独立的作用域所以它没有原型的父级。然而,在被嵌入(元素)之前,嵌入作用域将其原型指向了原始的内容作用域。然而,内嵌作用域并不只是一个普通的外部作用域的子作用域,因为这样的话我们会有一个摆脱它的问题:当
myCard
最终被删除的时候,我们想要同时把内嵌作用域销毁掉。因为我们不希望泄露作用域。Angular作用域是当其父作用域销毁的时候销毁,但是如果我们的内嵌作用域的父作用域是外部作用域,这就不能保证它(父作用域)能保留多久了。为了处理这个问题,Angular设置内嵌作用域的
$parent
属性指向了和数据来源作用域不同的一个作用域。$parent
是基于这个transclusion函数调用的位置的,在我们的例子中,它就是myCard
的局部作用域:作用域的
$parents
决定了它何时被销毁。对于内嵌作用域来说,原型上的父级以及结构上的父级可能甚至经常是不一样的。内嵌作用域的$parent
是myCard
的独立作用域,因为那是它被嵌入的区域的作用域。这种将原型上的父级和结构上的父级分离正是内嵌作用域特殊的地方。然而,正如前文提到的,大多数情况下事情仅仅是工作而不必在作用域层次上思考过于深入。
嵌入多个节点
我们已经知道了transclusion函数如何使用:当你需要被嵌入的DOM的时候,只需要调用这个函数并取得函数的返回值。
同时也有另一个可供选择的方式调用transclusion函数,这种方式不需要方法的返回值,而是传递一个函数作为回调参数。它会执行这个函数并传递嵌入的DOM:
JS Bin
你提供的回调函数被称为克隆附加函数。
既然可以通过返回值得到需要的内容,那这样做的意义在哪里?在上面的实例中,真的没有意义,但是本质上还是有所不同的。
当你使用克隆附加函数的时候会有一个副作用,即被嵌入内容在其被链接以及给予给你之前就会被克隆。给予到克隆附加函数中元素将不是最初被嵌入的完全相同的元素,他们将是原始内容的克隆。
这很重要,尤其是当你想要多次嵌入内容,使用克隆你可以做到这一点。
JS Bin
如果没有克隆附加函数这是无法做到的,如果你多次调用transclusion函数,那每次都会得到相同的DOM元素,如果把它们附加到多个地方,它们只会保留在最后一个地方。继续尝试吧!
ngTransclude
指令内部也是使用了克隆附加函数。这意味着你可以在你的模板中多次使用ng-transclude
,每一个(使用ng-transclude
的)地方都会插入一个被嵌入内容的克隆。尽管并不总是需要,但大多数人在嵌入内容的时候都会使用克隆附加函数。这也是Angular文档中所推荐的做法。
克隆附加函数并不仅仅只是一个嵌入功能,因为你也可以在公共的链接函数使用。如果你在手动链接而且想要链接同一个DOM的多个克隆,则只需要传递克隆附加函数作为第二个参数即可。
管理内嵌作用域生命周期
让我们给我们的
myCard
指令增加一个新特性——让用户能够收起和展开内容区。当卡片展开的时候,调用transclusion函数来创建并链接一个的副本。当卡片收起的时候,被嵌入的元素会被移除。JS Bin
这样实现有一个问题:作用域泄露。每次调用transclusion函数,内嵌作用域都会被重新创建。但是当我们清空被嵌入的内容时,Angular不确定是否要销毁(内嵌)作用域,所以作用域会一直保留。
我们应该负责销毁作用域,我们可以做到这个因为transclusion函数会将作用域作为第二个参数传递出来。如果我们能够获得它,那就可以随后调用
$destroy()
销毁它。JS Bin
如上述例子所示,当被嵌入的内容先于指令本身被销毁之前,调用内嵌作用域的销毁方法是你唯一需要考虑的事情。如果被嵌入的内容和指令本身的生命周期一致,则前面讨论过的父子作用域关系将保证事情自动被处理。
上面的例子有点勉强,因为我们可以很容易地通过使用
ngIf
和ngTransclude
来完成同样的行为。原本有一个相关的bug,但是在Angular1.3版本中被修复了。在内嵌作用域上附加自定义数据
还有一个原因可能要使用克隆附加函数:让它在特定的时间点被调用——内嵌作用域创建以及被链接之间。这意味着你可以在作用域中附加内容,并且它们在被嵌入的内容中可以使用。
例如,
myCard
指令可以提供一个“折叠”功能作为内嵌作用域上的方法。如果你接下来想在被嵌入的内容中增加一个折叠按钮,你可以很容易做到这一点:JS Bin
克隆附加函数为什么是一个函数而仅仅是一个克隆标志或类似的东西已经开始变得更加说得通了。即当
transclusion
函数返回的时候,链接已经完成了,但是你想要在作用域链接之前在其中添加内容。Angular提供了一个API调用,而不是两个不同的API,通过克隆附加函数作为一个回调方法,在正确的时间调用:在嵌入作用域被创建以及被链接之间。
使用自定义的内嵌作用域
正如前面讨论的,Angular将会为你管理内嵌作用域的创建,所以大多数时候你不必做任何事情就OK了。
但是你可以选择不让Angular创建内嵌作用域而是提供自定义的。
transclusion
函数可以使用scope对象作为第一个参数(在克隆附加函数之前)。如果你提供一个自定义作用域,就会跳过设置嵌入作用域的过程。什么时候需要这样做呢?一方面,如果你遇到了无法处理的作用域管理的刁钻问题,这将是一个解决方案。过去人们已经做到了这一点,例如,将
transclusion
和ngRepeat
结合起来使用。对此,嵌入还有一个有趣的使用方法:你可以将嵌入内容链接到你独立的作用域而不是范围作用域。因此,指令的部分模板的由嵌入内容提供,但是
数据
却是通过指令内部支持的。例如,你可以实现一个类似下面
townView
的指令,这个指令可以完全控制其显示的数据,但是允许传递一个动态的模板以决定数据如何
显示:JS Bin
请注意以这样的方式使用嵌入会有一些出乎意料的情况:嵌入内容中的表达式无法访问他们看似应该能够使用的数据。因此,我认为这种模式应该被谨慎使用。
理解元素嵌入
现在让我们把注意力放到另一种可以使用嵌入的方式上:使用
transclude: 'element'
进行元素嵌入。这种方式和常规方式的区别在于被嵌入的内容是什么:常规的方式是把当前元素的子节点进行嵌入。而元素嵌入是
元素本身
被嵌入(包括其所有的子节点)。从表面上看不出太大的区别:这种方式只是比普通方式多一个层级。但是,如果我们深入研究就会发现二者有着本质的差异。如果元素本身成为嵌入的一部分,那么DOM中被嵌入内容的容器究竟是什么呢?
有趣的是元素会被嵌入到哪里:嵌入的“元素”实际上消失了。如果你查看DOM,你会看到它被一段HTML注释所替代。该注释节点也是链接函数中接受到的
元素
。JS Bin
那么原始的元素发生了什么?答案是它现在由嵌入函数提供。你可以通过调用嵌入函数并在注释之后附加上结果。
JS Bin
如果元素上存在其他指令,各自不同的优先级决定了会发生什么:在正常链接的过程中链接优先级高的指令,但是优先级低的指令只有当你调用嵌入函数的时候才会被链接。
虽然这个练习很有趣,但是我们并没有掌握这一点。元素嵌入到底是用来做什么的呢?
元素嵌入进行延迟渲染
元素嵌入的一个应用场景是当你想要延迟链接部分UI或者附属的内容,直到特定的事情触发。
由于元素嵌入本质上是删除了DOM的一部分,并且使其可以通过使用嵌入函数再放回,所以你可以控制触发的时间。例如,你可能希望仅在父作用域的某些条件变为
true
的时候才链接DOM。JS Bin
这里本质上是实现了
ngIf
的简化版,如果你阅读ngIf
的源代码,你应该就能理解它做了什么:只有当条件表达式为真时,它才会通过元素嵌入的方式将这部分DOM进行链接。这样也就解释了为什么当你使用
ngIf
的时候会有HTML注释,他们实际上是通过元素嵌入的方式(将内容)插入DOM的。元素嵌入进行重复渲染
元素嵌入的另一个应用场景是如果你想要多次链接并附加一部分DOM。例如,你可以使用一个遍历数组的指令并渲染数组中每一个项目为DOM子树,同时使每一个子项在当前作用域都可用。
当我们把元素嵌入和克隆附加函数结合一起使用时,这一切都可以实现,因为我们可以为每一个子项的DOM克隆一个新的副本。
JS Bin
这本质上是
ngRepeat
的(超级)简化版本。原生的ngRepeat
指令也是使用了元素嵌入,尽管研究其源代码远不如ngIf
简单,因为ngRepeat
用法很多而且包含大量优化内容。不过它的核心也只是对元素嵌入以及克隆附加函数的应用罢了。一个可能使用到元素嵌入的场景是和模板一起使用,并且使用模板来直接替换原本的元素。然而,这并不是最初设计的功能而且这样使用你可能会遇到一些麻烦。对于这些情况,用
replace
来使用模板是正确的选择,尽管replace
已经被弃用了。元素嵌入被设计使用初衷是为了这样的场景:当你想要延迟、跳过、重复或者其他控制部分UI的链接及渲染。有关这方便更多的信息,请参见@caitp在相关GitHub issue下的精彩评论。
有点不幸的是,用于不同目的的两个特性(常规嵌入和元素嵌入)在API层面看起来是那么的相似。此外,在元素嵌入中,并没有任何东西被真正的嵌入,这使其看起来那么不恰当。
在Angular2里这不再是个问题,因为组件内的内容标签涵盖了常规的嵌入,而核心API涵盖了元素嵌入。(查看当前Angular2中ng-if的实现,它看起来可以控制何时从原型视图创建新的视图。)
译在最后:翻译的很挫,边学边翻~主要还是看用例来理解,推荐看一些用
transclusion
比较多的组件,比如ui-select其他参考链接:
彻底弄懂AngularJS中的transclusion - 用Angular开发web应用 - 前端乱炖
The text was updated successfully, but these errors were encountered: