理解Angular2的样式和view封装

Angular2是一个组件化的前端框架,通过编写一个个组件组合成一个整体的应用。他的组件是一种Web Component的实现方式。每一个组件的定义都包括模板和样式,Angular2在生成和渲染这个组件的时候,需要将解析后的模板的内容插入到html中,也就是将这个模板的视图view封装以后插入页面中。并且将样式的定义也插入到页面中,使它应用到组件上。在这篇文章中,我们就看看组件的样式是怎么起作用的,并且视图view是如何封装的。我们使用之前的文章Angular2入门教程-2 实现TodoList App 的例子,来看看样式和视图。
先来看看我们的组件TodoItemComponent的定义:

1
2
3
4
5
6
7
@Component({
selector: 'todo-item',
templateUrl: 'app/todo/item/item.component.html',
styleUrls: ['app/todo/item/item.component.css'],
styles: ['.completed { background: lightblue; }']
})
export class TodoItemComponent { ... }

这个定义中,为了演示,我同时使用styleUrlsstyles来定义样式。在styles中,加了一个class .completed { background: lightblue; }。同时,在styleUrls里面的css文件里,也加了一个class:

1
2
3
.completed {
background: lightyellow;
}

然后,打开页面,看到的效果如下:
screen1.png
完成的任务,它的颜色是浅黄色,也就是说,是css文件里面定义的样式在起作用。即使我们替换styleUrlsstyles的顺序,也是css文件里的样式起作用。所以,当使用2种方式同时定义样式的时候,css文件的样式会覆盖styles里面定义的。

下面我们使用chrome的开发工具,看看生成html是什么样。
screen2.png
我们可以看到在这个页面的html的header里面,有几个style元素,都是各个组件中定义的样式文件,被加到了index.html里面。根据之前的实例,我们的这个应用的组件的树形结构是这样的:
angular2-todo-list-component.png
根组件是AppComponentTodoListComponent是它的子组件,列表组件又有一个子组件TodoItemComponent。再对应到上面的图中,有4个style,其中3个就是对应的TodoItemComponent组件的css文件样式,和它的上面2级的组件的样式,另外一个,就是上面我们为了演示加的

1
styles: ['.completed { background: lightblue; }']

从这个生成的页面,我们可以看出,我们的组件在显示到页面上的时候,组件定义的样式就会被添加到页面头部里面。而且,同一个组件,如果即定义了styleUrls的样式,也定义了styles的样式,通过styleUrls定义的样式会放在后面。如果有定义的一样的class,后面的会覆盖前面的。

在上面的开发工具看到的页面的DOM里,我们看到样式是这样的:

1
2
3
.completed[_ngcontent-qka-4] {
background: lightyellow;
}

然后再看看DOM,找到完成的那个任务对应的元素:
screen3.png
当前完成的这个任务,用红框标出来,看到里面的DOM都包含一些属性:

1
<div _ngcontent-qka-4 class="todo-item completed">

实际上,Angular2就是通过这些属性来实现各个组件的样式之间的隔离。因为每个组件都会根据一定的规则分配一个属性,然后在样式上,也是通过属性加class的方式来设置这个组件的某一个class的样式。从而达到的样式隔离的效果。这就是Angular2的视图封装。

实际上,Angular2提供了3种视图封装的方式,我们上面看到的效果,也就是默认的方式,我们可以在组件上添加encapsulation属性来设置。默认的封装方式是ViewEncapsulation.Emulated,完整的定义如下:

1
2
3
4
5
6
7
8
@Component({
selector: 'todo-item',
templateUrl: 'app/todo/item/item.component.html',
styleUrls: ['app/todo/item/item.component.css'],
styles: ['.completed { background: lightblue; }'],
encapsulation: ViewEncapsulation.Emulated
})
export class TodoItemComponent { ... }

Emulated顾名思义就是’模拟的’,是用模拟的方式实现组件之间的隔离。在实现上,他通过给组件添加一个属性,通过属性+class的方式定义样式,不同的组件会有不同的属性值,这样就能实现样式的隔离。这显然是一个很聪明的做法。

Angular2还提供了另外两种封装方式。分别是:ViewEncapsulation.None 和ViewEncapsulation.Native。
先来看看None:

1
2
3
4
5
6
7
8
@Component({
selector: 'todo-item',
templateUrl: 'app/todo/item/item.component.html',
styleUrls: ['app/todo/item/item.component.css'],
styles: ['.completed { background: lightblue; }'],
encapsulation: ViewEncapsulation.None
})
export class TodoItemComponent { ... }

然后看看页面:
screen4.png
我们看到DOM里面这个完成的任务上面没有属性了,在头部的style里面,定义的class上也没有属性。这就是None的实现方式,也就是不实现什么隔离,如果你的两个组件中,有同一个class的定义,那个这两个定义就会冲突,后面的那个就会覆盖前面的。这显然不是一个好的实现方式。
最后,我们再来看看Native,修改组件的封装方式:

1
encapsulation: ViewEncapsulation.Native

再看看页面是什么样的:
screen5.png
完成的那个任务的DOM,也就是一个<todo-item>的元素里面,出现了一个shadow-root,所有的样式和模板都在这个shadow-root里面。在头部,只有2个style元素,没有这个TodoItemComponent组件的样式了。
那么这个shadow-root是什么东西呢?它是一个Shadow DOM。可以阅读这篇文章做一个简单的了解。简单来说,它就是把html、样式,甚至脚本都封装在一个Shadow DOM,插入到这个组件所在的位置,然后,它里面的样式、甚至脚本都只能够在这个Shadow DOM里面起作用。
Shadow DOM有些地方被翻译为影子DOM,但是还是英文的称呼更能体现它的精髓,一切只在’shadow’中进行。

有些人可能会觉得,Shadow DOMiframe很像。iframe是把整个页面插入到一个页面中,包括完整的document和上下文,所以从实现上是非常重的,性能也不好。但是Shadow DOM的实现很轻量,只是添加需要的DOM元素,应用样式,同时又能实现隔离。

在Angular2中使用Shadow DOM还有一个性能上的优势,是在数据绑定的修改检测上。因为Shadow DOM把它的逻辑都封装到一个DOM里,这个DOM里面的数据修改的检测,也只是在这个DOM中。如果我们使用不可变的数据,再结合合理的数据绑定和事件,就能够大大减少数据的修改检测。

但是,这么好的东西,Angular2为什么不默认就使用呢,原因是兼容性,我们可以在这里看到Shadow DOM在各个浏览器中的兼容性。IE不支持也就算了,连Firefox都不支持,Safari9的版本和iOS上的Safari也都不支持。而且Shadow DOM还不是W3C的正式标准,还只是处于草案阶段,所以,目前我们也只能在有限的平台去使用它,比如你用Angular2开发一个内置网页的应用,对于Android版本,就可以使用Shadow DOM封装视图去发布。

坚持原创技术分享,您的支持将鼓励我继续创作!