关于样式隔离

我们在使用css来为网页设置样式时,一大痛苦的来源就是css会影响整个页面上的元素,因此不得不增加一大堆的.container .wraper等等的类名,并且配合上复杂的嵌套关系,来确保样式被准确设置到期望的元素上。特别是现在流行单页面应用,更是加剧了这个问题。
但是,在使用Angular, Vue等框架开发前端应用时,却可以只关注组件内部的样式问题,而不用担心是否影响到页面的其他地方,这就极大地简化了调整页面样式的工作(当然,前提是正确使用框架为我们提供的样式隔离机制)。最近正好在使用Angular框架,于是研究了一下它的样式隔离机制如何实现。

观前提醒:本文中所用案例基于Angular官网的英雄之旅,相关代码可以在下面的链接中找到:https://github.com/Difang233/tour-of-heroes,如果觉得不错欢迎帮忙点个star!

Angular是如何实现组件间样式隔离的?

在Angular中,组件间样式隔离是通过为元素设置属性并结合css属性选择器来实现的。
以app-messages组件为例,我们可以看一下生成的html文件:

<app-messages _ngcontent-uou-c53="" _nghost-uou-c52="">
	<div _ngcontent-uou-c52="">
		<h2 _ngcontent-uou-c52="">Messages</h2><button _ngcontent-uou-c52="" type="button" class="clear">Clear messages</button>
		<div _ngcontent-uou-c52=""> HeroService: fetched heroes </div>
		<div _ngcontent-uou-c52=""> HeroService: fetched heroes </div>
		<div _ngcontent-uou-c52=""> HeroService: fetched heroes </div>
	</div>
</app-messages>

可以看到,在最终生成的html中,app-messages其中的元素都被设置上了一个 _ngcontent-uou-c52 的属性,而app-messages元素本身被设置上了 _nghost-uou-c52 这个属性。(可以先忽略app-messages身上的 _ngcontent-uou-c53,这是因为它是其他组件的子组件导致的)
如果观察过整个页面,就可以发现每个组件都有唯一的 _nghost 属性,其内部的元素也对应有 _ngcontent 属性。
再来看生成的style标签中的样式:

		h2[_ngcontent-uou-c52] {
            color: #A80000;
            font-family: Arial, Helvetica, sans-serif;
            font-weight: lighter;
        }

        .clear[_ngcontent-uou-c52] {
            color: #333;
            background-color: #eee;
            margin-bottom: 12px;
            padding: 1rem;
            border-radius: 4px;
            font-size: 1rem;
        }

        .clear[_ngcontent-uou-c52]:hover {
            color: white;
            background-color: #42545C;
        }

而我们原本写在message.components.css中的样式是这样的:

h2 {
    color: #A80000;
    font-family: Arial, Helvetica, sans-serif;
    font-weight: lighter;
  }
  
  .clear {
    color: #333;
    background-color: #eee;
    margin-bottom: 12px;
    padding: 1rem;
    border-radius: 4px;
    font-size: 1rem;
  }
  .clear:hover {
    color: white;
    background-color: #42545C;
  }

这里我们可以发现,Angular自动帮我们增加了一个属性选择器,属性选择器的内容就是app-messages组件内部元素上增加的 _ngcontent-uou-c52属性。另外,因为每个组件内部的元素都有不同的 _ngcontent,这样就可以实现组件间的样式隔离。

三个特殊的选择器

了解了Angular如何实现组件间样式隔离之后,我们可以再了解一下Angular为我们提供的三个特殊的选择器以及它们如何配合样式隔离机制发挥作用的。

:host

此部分的代码可以在host分支下找到

:host最终也会被编译为一个属性选择器,其内容就是前面提到的组件身上的 _nghost 属性。因此,:host可以用来选择组件元素本身。
还是以app-messages为例,假设要为该组件添加一个红色背景。如果在不使用:host选择器的情况下,我们只能在该组件的父组件对应的样式文件中修改。这里我们要找到app.components.css,在其中增加:

app-messages{
  display: block;
  background-color: red;
}

其原理就是利用了前文提到的app-messages身上的 _ngcontent-uou-c53 属性,该属性会被附加到所有app-root组件内部的元素上。
如果使用了:host选择器,我们就可以在app-messages组件自身对应的样式文件中进行修改,代码如下:

/* 在messages.component.css中进行修改 */
  :host{
    display: block;
    background-color: red;
  }

下面是生成的html中style标签内的样式:

[_nghost-uou-c52]{
    display: block;
    background-color: red;
  }

可见:host选择器在编译之后变成了一个属性选择器,对应于app-messages元素的 _nghost 属性。

另外需要注意的是,实际上_nghost和_ngcontent后面的值在每次编译之后都会发生变化,但是元素上的属性与style标签中css属性选择器的对应关系是不会变的。因此,本文为了方便阅读,我会对这些值进行调整,保持其和第一次编译后的结果一致。

::ng-deep

此部分的代码可以在ngDeep分支下找到

使用::ng-deep选择器可以帮助我们进行样式穿透。为了方便说明,下面使用app-dashboard组件进行演示,因为它内部包含有一个子组件app-hero-search,可以帮助我们更好地理解::ng-deep的作用;同时,在app-hero-search内部添加一个h2元素。代码如下:

<div id="search-component">
	<!--这是为了演示新增加的元素-->
    <h2>这是hero-search组件</h2>
    <label for="search-box">Hero Search</label>
    <input #searchBox id="search-box" (input)="search(searchBox.value)" />
  
    <ul class="search-result">
      <li *ngFor="let hero of heroes$ | async" >
        <a routerLink="/detail/{{hero.id}}">
          {{hero.name}}
        </a>
      </li>
    </ul>
  </div>

下面是app-dashboard组件对应的样式文件中关于h2标签的样式设置:

h2 {
    text-align: center;
  }

但是如果我们看页面的话,会发现这个样式并没有对app-hero-search内的样式生效

原因是hero-search组件内的标签和app-dashboard组件内的标签有不同的 _ngcontent 属性,因此在app-dashboard组件的样式文件中设置的样式并不会影响到其子组件。
如果我们想要在app-dashboard中设置的样式也在app-hero-search中起作用,就可以使用::ng-deep选择器,代码如下:

::ng-deep h2{
    text-align: center;
  }

此时生成的html上对应的代码是这样的:

h2{
    text-align: center;
  }

可见,::ng-deep去掉了后面选择器上附加属性选择器,但是这也带来了一个问题,现在h2的样式是在全局生效了,即使和app-dashboard并列的组件app-messages内部的h2元素也收到了影响:
在这里插入图片描述
可以看到Messages现在也居中了,这是::ng-deep带来的一个副作用,即它破坏了Angular的样式隔离机制。因此,一般在使用::ng-deep时要结合:host使用,可以保证影响的范围只是本组件及其内部的子组件。

:host ::ng-deep h2{
    text-align: center;
  }

此时生成的html上的样式如下:

[_nghost-uou-c18]     h2{
    text-align: center;
  }

在这里插入图片描述
通过:host增加一个属性选择器来选择组件自身,保证了影响范围只在本组件内部。可见此时app-dashboard和app-hero-search的h2元素内的文字居中了,但app-messages并没有受到影响。

因为::ng-deep破坏样式隔离机制的特性,Angular宣布将在未来弃用该选择器,不过目前还是可以使用的

:host-context

此部分的代码可以在hostContext分支下找到

:host-context可以为根据组件的外部元素来决定组件内的样式,还是以app-dashboard和app-hero-search为例,在app-dashboard对应的html文件中为app-hero-search的外部添加一个div标签,代码如下:

<div class="redColor">
  <app-hero-search></app-hero-search>
</div>

假设要根据app-hero-search外部的div决定内部h2元素的颜色,这时就可以使用:host-context选择器,在app-hero-search对应的样式文件中增加一个样式:

:host-context(.redColor) h2{
    color: red;
  }

最终生成的html中样式如下:

.redColor[_nghost-uou-c17]   h2[_ngcontent-uou-c17], .redColor   [_nghost-uou-c17]   h2[_ngcontent-uou-c17]{
    color: red;
  }

可见,:host-context选择器本质上是通过后代选择器来实现的,可以让我们根据该组件的外部元素或者该组件的组件标签上的class来修改组件内部的样式。

Logo

前往低代码交流专区

更多推荐