Angular视图封装模式——Emulated模式

680 阅读4分钟

“多余”的属性

Angular项目中如果在组件中通过 styleUrls 引入样式文件且使用Emulated模式,打开控制台调试DOM时,就会发现这些组件上多出来一些属性。这些属性看似无用,其实是Angular用作样式隔离的,那么这个属性是怎么加上去的呢?为什么能隔离样式呢?

Angular的三种视图封装模式

  • ShadowDom模式,使用浏览器原生的Shadow DOM实现,组件样式的表现为不进不出,全局样式进不来,组件样式出不去

  • Emulated模式,Angular默认的封装模式,模拟Shadow DOM的行为,组件样式表现为只进不出,全局样式能进来,组件样式出不去

  • None模式,不用视图封装,直接把css添加到全局样式中,组件样式表现为能进能出

可见把css样式局限在组件视图中,和Shadow DOM有很大的关系。

shadow Dom 是什么?

一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。

Web components 的一个重要属性是封装——可以将标记结构、样式和行为隐藏起来,并与页面上的其他代码相隔离,保证不同的部分不会混在一起,可使代码更加干净、整洁。其中,Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。

这是MDN上的解释,表示Shadow Dom是隐藏的元素。

写个例子看一看:

app-listapp-list-item两个组件,引入各自的css文件,且全部使用ShadowDom模式。同时在全局引入一个样式文件global.scss:

app-list

 // html
<div class="app-list">
  <p class="list-title bg-color">今日代办:</p>
  <app-list-item *ngFor="let list of lists" [list]="list"></app-list-item>
</div>


// ts
@Component({
  selector: 'app-list',
  templateUrl: './list.component.html',
  styleUrls: ['./list.component.scss'],
  encapsulation: ViewEncapsulation.ShadowDom
})


//scss
.app-list{
  display: block;
  width: 300px;
  .list-title{
    padding: 10px 20px;
    color: #fff;
  }
}
.bg-color{
  background-color: #66c060;
}

app-list-item

 //html
 <div class="list-item bg-color">
   <input type="checkbox" class="list-item-checkbox" [checked]="list.checked">
   <span class="list-item-title">{{list.title}}</span>
 </div>
 
 // ts
@Component({
  selector: 'app-list-item',
  templateUrl: './list-item.component.html',
  styleUrls: ['./list-item.component.scss'],
  encapsulation: ViewEncapsulation.ShadowDom
})


//scss
.list-item{
  overflow: hidden;
  padding: 15px  20px;
  border-radius: 4px;
  margin-bottom: 5px;
  .list-item-checkbox{
    margin-right: 20px;
  }
}
.bg-color{
  background-color: #ccc;
}

global.scss

.bg-color{
  background-color: red;
}

展示效果如下:

通过渲染出来的DOM结构可以看到**,组件的宿主app-list内部附加了一个Shadow Dom,以 shadow-root 为根,包裹所有子元素和css,全局css无效,确实做到了内外隔离,不进不出的效果(全局的样式进不来,内部的样式出不去)。**

背景点表示全局样式作用区域

我试着打印了一下:

    console.log('开始', document.getElementsByClassName('app-list'))

确实是隐藏的,所以全局样式作用不到。

Emulated模式

Angular通过预处理css代码,且为每个Dom增加了一些额外的属性,来模拟Shadow DOM的行为,与之不同的是,允许全局的样式进入(只进不出)。

看看例子:

代码如上,将 ViewEncapsulation.ShadowDom 换成 ViewEncapsulation.Emulated

  • app-list 上增加了属性_nghost-mgx-c17,以及子元素都增加了属性 _ngcontent-mgx-c17
  • app-list-item同时增加了属性_ngcontent-mgx-c17_nghost-mgx-c16, 内部子元素增加了属性_ngcontent-mgx-c16

也就是说Angular的模拟方式为:如果是ShadowDom模式下的宿主, 增加_nghost-xxx属性,如果是ShadowDom模式下的被Shadow root 包裹的子元素则增加_ngcontent-xxx 属性。同时预处理css,更改对应的选择器。由于并没有创建Shadow Dom,所以全局样式生效。

背景点表示全局样式作用区域

Angular官网也明确指出:

@Component 的元数据中指定的样式只会对该组件的模板生效。

它们既不会被模板中嵌入的组件继承,也不会被通过内容投影(如 ng-content)嵌进来的组件继承。

app-list-item同时增加了属性_ngcontent-mgx-c17_nghost-mgx-c16,表示宿主不是组件模版内容的一部分,而是父组件模板的一部分。 但是在宿主内部可以通过:host 伪类选择器来选择宿主元素。 如上图所示,如果想给在 app-list 的scss文件中想给 .my-app-list 类上写样式,应该这样写:

:host.my-app-list{
  ......
}

收获知识点

  • Angular中为样式隔离提供了两种模式:ShadowDom 和 Emulated
  • ShadowDom模式是使用浏览器原生的Shadow DOM实现,ShadowDom对外部的元素是隐藏的,因此,样式的表现为不进不出,全局样式进不来,组件样式出不去
  • Emulated是Angular通过模拟Shadow DOM的行为实现的一种模式,并没有创建ShadowDom,只是给组件增加了额外的属性来标识 host 和 content 并预处理css选择器。组件样式表现为只进不出,全局样式能进来,组件样式出不去
  • Emulated模式下宿主不是组件模版内容的一部分,而是父组件模板的一部分,但是在宿主内部可以通过:host 伪类选择器来选择宿主元素