这篇文章,我们将会学习默认的 angular 样式机制(Emulated Encapsulation)是如何在引擎中工作的,什么时候使用每个功能以及为何要使用该功能,组件样式隔离的好处
文章主题:
- angular 样式隔离 --如何生效的?
-
:host
修饰符,何时使用?为何使用? -
:host-context
修饰符,常见的主题使用案例; -
/deep/ ::ng-deep >>>
修饰符(即将淘汰); - 如何给使用
ng-content
的元素添加样式;
为什么要样式隔离?
事不宜迟,我们开始深入 angular 的样式隔离吧。
第一个要思考的问题就是:我们为什么要隔离组件间的样式?
原因有很多,最主要的一个原因就是 CSS 的可维护性。
当我们为应用程序开发了一个组件样式时,通常会遇到这种情况,即某个特性样式会干扰另一个特性样式。这是因为浏览器至今还未广泛支持样式隔离,我们可以限制一种样式仅应用于页面指定的部分。
如果不够谨慎没有系统地组织样式来阻止这个问题,我们就会很快就会面临 css 维护问题。
能够用简洁易读的选择器来为组件写样式不是很好嘛,不用担心在所有的场景里面这些样式会被覆盖。
样式隔离的另一个好处
这有个场景:我们多次尝试在应用中添加第三方组件的时候却发现组件因为样式问题不能使用?
样式隔离使得我们可以在知道组件样式不会被应用中的其他样式覆盖的前提下发布组件,这让组件更有效地重复利用,因为这样的组件在多数情况下都能正常工作,包括样式。
Angular View Encapsulation 给我们带来了诸多好处,现在来学习一下它是如何工作的
Angular 样式隔离是如何工作的?Emulated View Encapsulation
为了更好地理解默认的 View Encapsulation 如何工作,来看下自定义的html标签 app-root
在运行时长啥样:
<app-root _nghost-c0="">
<h2 _ngcontent-c0="">Component Style Isolation example</h2>
<button _ngcontent-c0="" class="red-button">Button</button>
</app-root>
在运行时 HTML 级别上发生了几件事:
1.自定义的 app-root
标签上加上了一个奇怪的_nghost-c0
属性;
2.应用中根组件里面的html标签都加上另一个不同的属性:_ngcontent-c0
这些属性是什么
那这些属性是如何工作的呢?为了更好地理解这些属性且它们是如何启用样式隔离的,我们要创建第二个只包含一个蓝色按钮的独立组件
为了更简洁,我们将在模板旁边内联定义此组件样式:
@Component({
selector: 'blue-button',
template: `
<h2>Blue button component</h2>
<button class="blue-button">Button</button>
`,
styles: [`
.blue-button {
background:blue;
}
`]
})
export class BlueButtonComponent {
}
为了使用这个新定义的组件,我们将把它添加到应用程序根组件的模板中
@Component({
selector: 'app-root',
styleUrls:['./app.component.css'],
template: `
<button class="red-button">Button</button>
<blue-button></blue-button>
`})
export class AppComponent {
...
}
在这个阶段尝试猜测运行时的 HTML 会是什么样子,以及那些奇怪命名的属性发生了什么
宿主元素和模板元素样式隔离属性
有了第二个组件,让我们再看看 HTML。这两个属性的工作方式现在将变得更加明显
<app-root _nghost-c0="">
<h2 _ngcontent-c0="">Component Style Isolation example</h2>
<button _ngcontent-c0="" class="red-button">Button</button>
<blue-button _nghost-c1="" _ngcontent-c0="">
<h2 _ngcontent-c1="">Blue button component</h2>
<button _ngcontent-c1="" class="blue-button">Button</button>
</blue-button>
</app-root>
注意 blue-button
元素,我们现在有一个名为_nghost-c1
的新宿主属性。
blue-button
元素仍然使用 _ngcontent-c0
属性标记,该属性应用于应用程序根组件上的所有模板元素,但是现在,蓝色按钮模板中的元素现在改为应用 _ngcontent-c1
属性。
小结
让我们总结一下这些特殊的 HTML 属性是如何工作的,然后看看它们是如何启用样式隔离的
- 在应用程序启动时(或在使用 AOT 构建时),每个组件都会有一个附加到宿主元素的唯一属性,具体取决于组件的处理顺序:
_nghost-c0
、_nghost-c1
等 - 每个组件模板中的每个元素也将应用该特定组件独有的属性:
_ngcontent-c0
、_ngcontent-c1
等。
这一切都是透明的,并且为我们在后台完成。
这些属性如何启用视图封装
这些属性的存在可以让我们手动编写比模板上的简单样式更有针对性的 CSS 样式,
例如,如果我们只想将蓝色范围限定为蓝色按钮组件,我们可以手动编写以下样式:
/* Style 1 - a simple CSS style,
with low specificy and easilly overridable */
.blue-button {
background: blue;
}
/* Style 2 - a similar style, with a much higher
specificity and much harder to override */
.blue-button[_ngcontent-c1] {
background: blue;
}
虽然样式 1 适用于页面上任何位置具有blue-button
类的任何元素,但样式 2 仅适用于具有特殊命名属性的元素!所以这意味着样式 2 的有效范围仅限于蓝色按钮组件模板的元素,不会影响页面的任何其他元素。
我们现在可以看到这两个特殊属性是如何实现某种样式隔离的,但是在我们的 CSS 中手动使用这些属性会很麻烦(事实上,我们不应该这样做)
幸运的是,我们不必这样做,angular 会自动帮我们实现
Angular 如何封装样式
在启动时(或在构建时,如果使用 AOT),Angular 将通过样式或styleUrls
组件属性查看哪些样式与哪些组件相关联,然后 Angular 将采用这些样式并将它们透明地应用到相应的隔离属性中,然后将这些样式作为样式标签直接注入到页眉中
<style>
.blue-button[_ngcontent-c1] {
background:blue;
}
</style>
_ngcontent-c1
属性对于 blue-button
模板的元素是唯一的,因此样式将仅限于这些元素
这就是 Angular 默认视图封装机制的工作原理!这个机制不是 100% 不可穿透的,因为它不能保证完美隔离,但实际上,它总是有效。
这个机制不是基于 shadow DOM 而是基于这些特殊的 HTML 属性,所以如果真需要的话仍然可以覆盖这些样式。但鉴于原生 Shadow Dom 隔离机制目前仅在 Chrome 和 Opera 中可用,我们还不能依赖这种做法。
这种机制非常有用,因为它使我们能够编写不容易破坏的简单样式,但我们有时可能希望能选择性地打破这种隔离,让我们学习几种这样做的方法,以及我们为什么要这样做。
伪类选择器 :host
有时我们想设置组件自定义HTML元素本身的样式,而不是模板内部的样式。举个例子我们想设置app-root
组件的样式,给它额外设置一个border
样式,我们直接在其关联的app.component.css
文件中添加样式是没用的。
这是因为该文件里面的所有样式都是模板范围内的,并不包括app-root
元素本身。
如果我们想给组件元素本身设置样式,我们需要使用:host伪类选择器。
/* other styles on app.component.css */
...
/* styles applied directly to the ap-root element only */
:host {
border: 2px solid dimgray;
display: block;
padding: 20px;
}
这个选择器会确保这些样式只对 app-root
元素生效,记得我们之前说过的 _nghost-c0
属性吗?这就是在运行时用于实现:host
的方式。
<style>
[_nghost-c0] {
border: 2px solid dimgray;
display: block;
padding: 20px;
}
</style>
使用特殊的_nghost-c0
将确保这些样式仅适用于 app-root
元素,因为 app-root
在运行时添加了该属性
<app-root _nghost-c0="">
...
</app-root>
:host 和其他选择器联合使用
注意: 可以把这个选择器和其他选择器结合起来,这是我们还没有谈到的
这不是特定于这个选择器,但是看看这个选择器的例子,我们在宿主元素中设置 h2 元素的样式
:host h2{
color: red;
}
可以看到上面这个样式只会对app-root
里面的h2
生效,不会对blue-button
里面的h2
生效。
这是为什么呢?来看看样式编译后的代码:
<style>
....
[_nghost-c0] h2[_ngcontent-c0] {
color: red;
}
</style>
所以我们可以看到特殊的作用域属性也被应用于嵌套的选择器,以确保样式始终作用于该特定模板,但是如果我们确实想覆盖所有 h2
元素的样式,仍然有办法
::ng-deep 伪类选择器
如果我们想让组件样式穿透到子组件中,但不对页面中其他的元素生效,我们可以结合:host
和::ng-deep
一起使用
:host ::ng-deep h2{
color: red;
}
运行后生成的样式
[_nghost-c0] h2 {
color: red;
}
所以这个样式会应用于app-root
里面的所有h2
元素,包括子组件里面的。
这种选择器的组合非常有用,例如将样式应用于使用 ng-content
传递给模板的元素,查看这篇文章了解更多细节
::ng-deep ,/deep/ >>> 弃用
::ng-deep
伪类选择器包括它的别名>>>
和 /deep/
很快将被移除
主要的原因就是这种在组件周围刺穿样式隔离沙箱的机制可能会鼓励不良的样式实践
这种情况仍旧存在,但现在,如果案例中需要的话,可以使用 ::ng-deep
:host-context 伪类选择器
有时,我们还想让组件将样式应用于其外部的元素, 这种情况不常见,但很可能常见的一种情况就是主题启用类。
比如,我们想发布有多个不同主题的组件,每个主题都能通过添加向父元素添加css类来启用
我们可以通过使用 :host-context
选择器来实现这个案例
@Component({
selector: 'themeable-button',
template: `
<button class="btn btn-theme">Themeable Button</button>
`,
styles: [`
:host-context(.red-theme) .btn-theme {
background: red;
}
:host-context(.blue-theme) .btn-theme {
background: blue;
}
`]
})
export class ThemeableButtonComponent {
}
这些主题默认是未启用的,为了激活一个主题,我们需要把一个主题激活的样式添加到组件任意父元素上,
例如,我们可以这样激活一个蓝色主题样式。
<div class="blue-theme">
<themeable-button></themeable-button>
</div>
总结
有很多选项可以为我们的组件设置样式,因此了解何时以及为什么使用哪个很重要。看下这个简短的总结:
- 有时我们想要全局样式,应用于页面的所有元素 - 我们可以将它们添加到我们的 angular-cli.json 配置中;
- Angular View 封装机制允许我们编写更简单的样式,更易于阅读并且不会干扰其他样式;
- 默认的视图封装机制会长期有益于减少 css 相关的 bug,使用它,我们就不会陷入添加一些 css 代码修复问题但不小心破坏其他页面样式的情况 - 这是一个巨大的优势;
网友评论