Angular 18+ 高级教程 – Component 组件 の Angular Component vs Shadow DOM (CSS Isolation & slot)
前言
要掌握 Angular,最好先掌握原生。
全局 CSS 的问题,还有如何用原生 CSS 来管理全局 CSS,看这篇。
利用 Shadow Dom 来隔离 CSS 看这篇。
CSS Global Effect
CSS style 是全局影响的。
假设我们有 2 个组件,AppComponent 和 TestComponent。
app html
<div class="container"> <h1>Outside Hello World</h1> <app-test></app-test> </div>
app css
h1 { background-color: pink; }
test html
<h1>Inside Hello World</h1>
test css
h1 { font-size: 48px; color: red; }
两个组件 html 都有 h1 element,同时 2 个组件的 css 都给予 h1 不同的 style(1 个给 background-color, 1 个给 color)
最终效果
两个 h1 element 都渲染了 2 个 styles(background-color & color)这就是 CSS 默认的行为。
但总所周知,全局是魔鬼。所以 CSSer 有许许多多方法来解决这个问题,比如 BEM。 而 Angular 也用了其中的二种方案。
Angular CSS Isolation
如果你按照我上面的例子写,你会发现出来的效果 style 并没有互相渲染。这是因为 Angular 默认就开启了 CSS 隔离。(我是为了演示特意关闭的)
Angular 有 2 种隔离方案,它们都可以达到隔离效果,但有一点点微小的区别。
Shadow DOM 隔离方案
第一种方案是 Shadow DOM,Shadow DOM 的主要功能本来就是隔离。所以 Angular 会选择这种方案是显而易见的。
我们在组件的 metadata 里声明
encapsulation: ViewEncapsulation.ShadowDom
这样就开启了 Shadow DOM 隔离
和
效果
element 结构
Shadow DOM 隔离方案的概念是 “不能进,不能出",外面 (其它组件或者全局) CSS 不能进入到组件里,同时组件定义的 CSS 也不会跑出去影响其组件。
Emulated Shadow DOM 隔离方案
encapsulation: ViewEncapsulation.Emulated
有 Shadow DOM 方案不就够了吗?为什么还要搞多一个 Emulated (模拟) 方案呢?而且这个模拟方案竟然还是 default(首选)方案!?
Shadow DOM 会隔离 "所有" 外部的 style。这也意味着我们不可以写全局的 reset.css、base.css 等等。
虽然我们想隔离,但是我们想隔离的是各个组件之间的 style,而不是像 reset.css 这种通用的全局 style。
为此 Angular 就搞了一个 Emulated Shadow DOM,它的概念是 "能进,不能出",和 Shadow DOM 一样不允许组件的 style 跑出去,但它允许外面的 style 跑进来。
Angular 项目中全部都是组件,所有的 style 都跑不出去,唯一能跑进来的就只剩下全局 style 了。
这样各个组件既不会互相影响,同时有能拿到全局 style,完美👍
这个 styles.scss 就可以让我们写全局 reset 和 base style。
Emulated 的具体实现手法类似 BEM,通过给 class selector 长命名来起到相互不被影响,它整个过程是在 compile 阶段完成的,所以我们完全感受不到。
下面是最终生成出来的 HTML 和 style:
element 和 CSS selector 都多了许多 attribute, _ngcontent-ng-c123456789
这样就实现了隔离效果。
关闭所有隔离方案
上面有提到,Angular 默认就替组件开启了 Emulated 隔离方案,所以哪怕我们什么也没声明,它就已经是隔离的了。
但如果我们想关关闭隔离方案也是可以的,只要声明
encapsulation: ViewEncapsulation.None
这样就关闭了。
它的概念是 "能进,能出",全局 style 可以进入到此组件,同时此组件定义的 CSS 也会跑出去 (相等于定义了全局 style) 影响其它组件。
Shadow DOM Slot vs Angular Content Projection (a.k.a slot / transclude / ng-content)
Shadow DOM 可以通过 slot 从外部 transclude element 到 Shadow DOM 里面。
Angular 也有这个能力,但 Angular 并不是用原生 slot 来实现的。不管是 Emulated mode 还是 ShadowDom mode,Angular 都不是用原生 slot。
ng-content
app.component.html
<app-test> <h1>Hello World</h1> <p>Lorem, ipsum dolor sit amet consectetur adipisicing elit.</p> </app-test>
有一个 test component,我们要从外部 translude h1 和 p 进去。
test.component.html
<div class="container"> <h1>This is inside content</h1> <h1>Below is outside content:</h1> <div class="outside-content-area"> <ng-content></ng-content> </div> </div>
如果是原生 Shadow DOM,那么里面应该用 <slot> element。
而 Angular 则使用 <ng-content> element。
功能都是一样的 transclude outside element to inside。
效果
注:关于 transclude element 的 style,Angular 和 Shadow DOM 是一样,style 由外部设置。
你可以这样去理解. transclude element 在外部渲染好了以后,被 cut and paste 到内部。
default ng-content (a.k.a fallback content)
Angular 直到 v18.0 才支持 default ng-content 功能...
这个功能的 Github feature request:ng-content default content 是在 2016-10-26 提的...一个功能需要 8 年时间做...👍
用法很简单
<ng-content> <h1>default content</h1> </ng-content>
在 ng-content 内放入 element 就可以了,如果外部没有传 element 进来,那就会用 default 的 element。
Check if empty ng-content?
default ng-content 只能让你在 empty content 的时候放入一个 default content,但它无法让你判断是否是 empty 做任意的事情。
比如:hide parent when empty content
<span class="left-part"> <span class="icon-wrapper"><ng-content select="mat-icon" /></span> <span class="text"> <span class="line1"><ng-content /></span> <span class="line2">{{ textLine2() }}</span> </span> </span>
假如没有传入 mat-icon,我希望把 icon-wrapper 给 display none。
参考:Stack Overflow – How to check whether <ng-content> is empty? (in Angular 2+ till now)
使用 CSS seelctor 是一个不错的办法,但这招也只能针对这个需求,其它需求还得想其它办法。
总之 Angular 没有提供一个简单通用的方案就是了😞
multiple ng-content
Shadow DOM 的 slot 支持 multiple,Angular 的 ng-content 也支持。
我们把 translude element 分成 2 个部分。first and second
<app-test> <h1 slot="first">Hello World</h1> <p slot="second">Lorem, ipsum dolor sit amet consectetur adipisicing elit.</p> </app-test>
然后 component 里面也分成 2 个 display areas
<div class="container"> <h1 part="inside-h1">This is inside content</h1> <h1>Below is outside content:</h1> <div class="outside-content-area"> <ng-content select="[slot='first']"></ng-content> </div> <div class="outside-content-area"> <ng-content select="[slot='second']"></ng-content> </div> </div>
对比原生 slot 和 Angular ng-content 的语法
outside component:
语法一致.
inside component:
原生 slot 是通过 name attribute = "outside slot name"。
Angular 则是通过 select attribute = "any css selector"。
所以,对比这个环节。Angular 其实是更加方面的,因为它支持任何 selector 方式。我们不一定要用 slot="first" 也可以用 class 或 element tag 去 select。比如:
这样也是 ok 的.
Only first layer can be select
App Template
<app-test> <h1>Hello World</h1> </app-test>
Test Template
<ng-content select="h1"></ng-content>
上面这样 ok。
但如果我们把 h1 wrap 起来就不行了
<app-test> <div> <h1>Hello World</h1> </div> </app-test>
总之,只有 first layer child 才可以被 select,这是 Angular 的 limitation。
ngProjectAs
有一个 HelloWorld 组件,里面 select h1 作为 ng-content。
可是呢,外部因为某种说不出的原因,只能使用 h2
<app-hello-world> <h2>content 1</h2> </app-hello-world>
这样就 match 不到了丫,怎么办呢?
这时,我们可以使用 ngProjectAs="what-ever-selector"
<h2 ngProjectAs="h1">content 1</h2>
我们可以把 h2 变成任何 selector,这样 HelloWorld 组件内部就可以 select 到了。
<ng-container> wrapper
假设内部 select by class "content-1" 和 "content-2"
<ng-content select=".content-1"></ng-content> <ng-content select=".content-2"></ng-content>
外部长这样
<app-hello-world> <h1 class="content-1">content 1</h1> <h2 class="content-2">Content 2</h2> <h3 class="content-2">Content 3</h3> <h4 class="content-2">Content 4</h4> </app-hello-world>
h2, h3, h4 都是 content-2,每一个都需要写上 class="content-2"。
那有没有一种方式可以不必重复写 class 呢?
有,那就是用 <ng-container> wrap 它
<ng-container class="content-2"> <h2>Content 2</h2> <h3>Content 3</h3> <h4>Content 4</h4> </ng-container>
<ng-container> 的用途很广,以后会详细讲解,这里我们只要知道它是 Angular 的一个特别的 syntax,
它不是组件,也不是 Element,它被用于 compile 阶段,compile 结束后它会变成一个 Comment 节点 (<!--ng-container-->)。
上面例子中,<ng-container> 起到了一个 wrapper 作用,Angular 在 compile 阶段可以知道 h2, h3, h4 是一组的,它们都属于 content-2。
而在 compile 后,它会变成这样
可以看到 ng-container 并没有再 wrap 着 h2, h3, h4,只是在结尾多了一个 Comment 节点。
它这样就做到了在不严重破坏 DOM 结构的前提下,提供 wrapper 概念给 compiler。
在 <ng-container> 上写 DOM 属性 (比如 class="content2") 是不顺风水的,毕竟人家不是真的 Element。更好的做法是配上 ngProjectAs。
<ng-container ngProjectAs=".content-2"> <h2>Content 2</h2> <h3>Content 3</h3> <h4>Content 4</h4> </ng-container>
提醒:wrap 了 <ng-container> 一定要给 class 或者 ngProjectAs 哦,否则 <ng-content> 会 select 不到。
原因上面我们说过了 -- only first layer child can be select。一旦 wrap 了 <ng-container> h2, h3, h4 就不算是 first layer child 了 (即使在 compile render 后 <ng-container> 会变成 comment 并且不再 wrap 着 h2, h3, h4)
:host, :host-context
Angular 支持 :host,也支持 :host-context()。它和 Native Shadow DOM 用法、效果都是一致的。
而且,Angular 还解决了 :host-context 在 Firefox 和 Safari 下不支持的问题哦。
提醒::host-context 不只是看 host element 有没有 matched CSS selector,它也会看所有 ancestor (祖先) elements,只要其中一个 match 就算有。
比如说 :host-context(.my-class),<body class="my-class"> 这样也算 matched。
::slotted()、::part()、::ng-deep
Angular 完全不支持 ::slotted(),因为 Angular 没有 slot element 所以 select 不到。
Angular 在 Emulated mode 下,不支持 ::part(),但是在 ShadowDom mode 下支持。
在 Angular 圈,没有人愿意用那么乱的东西,一些支持,一些又不支持的。
大部分 Angular 人会统一使用 Emulated mode + ::ng-deep 来替代 ::slotted() 和 ::part()。
注意:
::ng-deep 一定要搭配 Emulated mode 才有效哦。另外,它是一个废弃了很多年的语法,但时至今日它依然可以用。因为 Angular 一直没有方法可以实现 ::slotted() 和 ::part(),所以只能暂时让 ::ng-deep 负责。
更新:::ng-deep 在被废弃多年以后,既然在 v18 死灰复燃,Angular 团队把 ::ng-deep 从 deprecated 改为 un-deprecated...想不到吧,尽然还能这样玩...😑。
::ng-deep 的功能是 by pass 所有 CSS 隔离,selector 可以渗透到 component 内(不管多少层 component 都渗透哦)
::ng-deep 替代 ::part()
app-test::part(inside-h1) { background-color: green; } app-test ::ng-deep [part="inside-h1"] { background-color: gray; }
::ng-deep 可以配任何 selector, 不一定要搭配 attribute part 来用.
::ng-deep 替代 ::slotted()
::slotted(h1) { background-color: green; } ::ng-deep h1 { background-color: green; }
目录
上一篇 Angular 18+ 高级教程 – Component 组件 の Angular Component vs Custom Elements
下一篇 Angular 18+ 高级教程 – Component 组件 の Template Binding Syntax
想查看目录,请移步 Angular 18+ 高级教程 – 目录
喜欢请点推荐👍,若发现教程内容以新版脱节请评论通知我。happy coding 😊💻