Angular样式隔离(style isolation)及选择器(:host, :host-context, ::ng-deep)的使用
<h3>1.Angular样式隔离</h3><p>Angular样式隔离的好处最最要的一条就是CSS的可维护性。当没有样式隔离时,我们创建一个组件并添加样式后,可能会影响到其他的组件样式,而且很有可能查找不出问题所在。虽然我们可以想出办法来避免样式被覆盖,但是可能会引发CSS的可维护性问题。</p>
<p>Angular的视图封装(View Encapsulation)</p>
<p>在Angular中,组件的样式可以封装在组件的宿主元素中(host),这样它们就不会影响应用程序的其他部分。</p>
<p>视图封装模式:</p>
<p>1.<strong>ViewEncapsulation.ShadowDom</strong>: Angualr使用浏览器内置的Shadow Dom API将组件的视图封装在ShadowRoot中,用作组件的宿主元素,并以隔离的方式应用提供的样式(只对浏览器内置Shadow Dom支持时才起作用)。组件的样式只添加到Shadow Dom宿主中,确保它们只影响各自组件视图中的元素。</p>
<p>2.<strong>ViewEncapsulation.Emulated</strong>:使样式仅应用于组件的视图,不会影响应用程序中的其他元素,模拟Shadow Dom行为(默认的视图封装模式)。组件的样式被添加到文档的<head>中,使它们在整个应用程序中可用,但只影响它们各自组件模板中的元素。</p>
<p>3.<strong>ViewEncapsulation.None</strong>:不使用任何类型的视图封装,为组件指定的任何样式都是全局应用的,并且影响应用程序中的任何HTML元素。组建的样式被添加到文档的<head>中,使它们在整个应用程序中可用,所以时完全全局的,并影响文档中的任务匹配元素。</p>
<p>要想设置组件的视图封装模式,可以在组件装饰器中设置 encapsulation 选项。</p>
<p>为了更好的理解默认的视图封装(Emulated View Encapsulation)是如何起作用的,先贴上一段代码:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 0, 0, 1)">@Component({
</span><span style="color: rgba(0, 128, 128, 1)"> 2</span> <span style="color: rgba(0, 0, 0, 1)"> selector: 'app-root',
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span> <span style="color: rgba(0, 0, 0, 1)"> template: `
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">h2</span><span style="color: rgba(0, 0, 255, 1)">></span>Parent Component<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">h2</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 128, 128, 1)"> 5</span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">app-child</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">app-child</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 128, 128, 1)"> 6</span> <span style="color: rgba(0, 0, 0, 1)"> `,
</span><span style="color: rgba(0, 128, 128, 1)"> 7</span> <span style="color: rgba(0, 0, 0, 1)"> styles: [
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span> <span style="color: rgba(0, 0, 0, 1)"> `
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span> <span style="color: rgba(0, 0, 0, 1)"> h2 {
</span><span style="color: rgba(0, 128, 128, 1)">10</span> <span style="color: rgba(0, 0, 0, 1)"> background-color: lightskyblue;
</span><span style="color: rgba(0, 128, 128, 1)">11</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">12</span> <span style="color: rgba(0, 0, 0, 1)"> `
</span><span style="color: rgba(0, 128, 128, 1)">13</span> <span style="color: rgba(0, 0, 0, 1)"> ],
</span><span style="color: rgba(0, 128, 128, 1)">14</span> <span style="color: rgba(0, 0, 0, 1)"> encapsulation: ViewEncapsulation.Emulated
</span><span style="color: rgba(0, 128, 128, 1)">15</span> <span style="color: rgba(0, 0, 0, 1)">})
</span><span style="color: rgba(0, 128, 128, 1)">16</span> <span style="color: rgba(0, 0, 0, 1)">export class AppComponent implements OnInit {
</span><span style="color: rgba(0, 128, 128, 1)">17</span>
<span style="color: rgba(0, 128, 128, 1)">18</span> <span style="color: rgba(0, 0, 0, 1)"> ...
</span><span style="color: rgba(0, 128, 128, 1)">19</span> }</pre>
</div>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 0, 0, 1)">@Component({
</span><span style="color: rgba(0, 128, 128, 1)"> 2</span> <span style="color: rgba(0, 0, 0, 1)"> selector: 'app-child',
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span> <span style="color: rgba(0, 0, 0, 1)"> template: `
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">h2</span><span style="color: rgba(0, 0, 255, 1)">></span>Child Component<span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">h2</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 128, 128, 1)"> 5</span> <span style="color: rgba(0, 0, 0, 1)"> `,
</span><span style="color: rgba(0, 128, 128, 1)"> 6</span> <span style="color: rgba(0, 0, 0, 1)"> styles: [
</span><span style="color: rgba(0, 128, 128, 1)"> 7</span> <span style="color: rgba(0, 0, 0, 1)"> `
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span> <span style="color: rgba(0, 0, 0, 1)"> h2 {
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span> <span style="color: rgba(0, 0, 0, 1)"> background-color: aqua;
</span><span style="color: rgba(0, 128, 128, 1)">10</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">11</span> <span style="color: rgba(0, 0, 0, 1)"> `
</span><span style="color: rgba(0, 128, 128, 1)">12</span> <span style="color: rgba(0, 0, 0, 1)"> ],
</span><span style="color: rgba(0, 128, 128, 1)">13</span> <span style="color: rgba(0, 0, 0, 1)"> encapsulation: ViewEncapsulation.Emulated
</span><span style="color: rgba(0, 128, 128, 1)">14</span> <span style="color: rgba(0, 0, 0, 1)">})
</span><span style="color: rgba(0, 128, 128, 1)">15</span> <span style="color: rgba(0, 0, 0, 1)">export class ChildComponent implements OnInit {
</span><span style="color: rgba(0, 128, 128, 1)">16</span>
<span style="color: rgba(0, 128, 128, 1)">17</span> <span style="color: rgba(0, 0, 0, 1)"> ...
</span><span style="color: rgba(0, 128, 128, 1)">18</span> }</pre>
</div>
<p>以下是运行时的代码,</p>
<p><img src="https://img2022.cnblogs.com/blog/1754447/202211/1754447-20221126212837629-1497369155.png"></p>
<p> 可以看到app-root自定义元素上添加了一个奇怪的属性:_nghost-jtq-c16属性;在根组件中的HTML元素有一个看起来很奇怪但不同的属性:_ngcontent-jtq-c16;app-child自定义元素上添加了另一个属性:_nghost-jtq-c17,以及组件内HTML元素有一个_ngcontent-jtq-c17属性。</p>
<p>因此,我们可以知道Angular样式隔离的基本原理:</p>
<p>1.在应用程序启动时(或在使用AOT构建时),每个组件都将具有附加到宿主元素的唯一属性,具体取决于组件的处理顺序:_nghost-jtq-c16, _nghost-jtq-c17。</p>
<p>2.除此之外,每个组件模板中的每个元素也将应用该特定组件独有的属性:_ngcontent-jtq-c16, _ngcontent-jtq-c17。</p>
<p> Angular将这些样式应用到相应的独特属性上:</p>
<p><img src="https://img2022.cnblogs.com/blog/1754447/202211/1754447-20221126213003261-41492777.png"></p>
<p><img src="https://img2022.cnblogs.com/blog/1754447/202211/1754447-20221126213050613-784486607.png"></p>
<h3>2.选择器(:host, :host-context, ::ng-deep)的使用</h3>
<p><strong>:host</strong></p>
<p>每个组件都与一个和组件的选择器相匹配的元素相关联。呈现模板的这个元素称为<strong>宿主元素</strong>。:host 伪类选择器用于创建以宿主元素本身为目标的样式,而不是以宿主内部的元素为目标。</p>
<p>当我们想要为app-root组件本身添加样式(加一个边框),就需要用到 :host 伪类选择器,原因是所有与组件关联的样式(通过css文件或内联形式在组件装饰器中声明),通常作用于模板内的元素。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> <span style="color: rgba(0, 0, 0, 1)">:host {
</span><span style="color: rgba(0, 128, 128, 1)">2</span> <span style="color: rgba(0, 0, 0, 1)"> display: block;
</span><span style="color: rgba(0, 128, 128, 1)">3</span> <span style="color: rgba(0, 0, 0, 1)"> border: 5px solid palegreen;
</span><span style="color: rgba(0, 128, 128, 1)">4</span> }</pre>
</div>
<p> </p>
<p><img src="https://img2022.cnblogs.com/blog/1754447/202211/1754447-20221126223222946-376283977.png"></p>
<p> 应用样式后,组件显示入下:<img src="https://img2022.cnblogs.com/blog/1754447/202211/1754447-20221126222850491-678955808.png"></p>
<p> :host与其他选择器组合使用</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> <span style="color: rgba(0, 0, 0, 1)">:host h2 {
</span><span style="color: rgba(0, 128, 128, 1)">2</span> <span style="color: rgba(0, 0, 0, 1)"> color: red;
</span><span style="color: rgba(0, 128, 128, 1)">3</span> }</pre>
</div>
<p> </p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> <span style="color: rgba(0, 0, 0, 1)">:host(.active) {
</span><span style="color: rgba(0, 128, 128, 1)">2</span> <span style="color: rgba(0, 0, 0, 1)">font-weight: bold;
</span><span style="color: rgba(0, 128, 128, 1)">3</span> }</pre>
</div>
<p> 在:host()选择器中,括号内的条件决定了要设置样式的宿主元素(宿主元素具有active类)。</p>
<p><strong>::ng-deep</strong></p>
<p>将::ng-deep伪类应用于任何CSS规则会完全禁止视图封装规则;<em><span style="text-decoration: underline">任何应用了::ng-deep的样式都会成为全局样式</span></em>。为了将指定样式限定在当前组件以及后代,<span style="text-decoration: underline"><em>确保在::ng-deep之前包含:host选择器</em></span>。如果在没有:host伪类选择器的情况下使用::ng-deep选择器,样式可能会渗入其他组件。</p>
<p>如果希望组件的样式级联到组件的所有子元素,而不是页面上的任务其他元素,我们可以通过将:host与::ng-deep选择器结合使用来实现:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> <span style="color: rgba(0, 0, 0, 1)">:host ::ng-deep h2 {
</span><span style="color: rgba(0, 128, 128, 1)">2</span> <span style="color: rgba(0, 0, 0, 1)"> color: red;
</span><span style="color: rgba(0, 128, 128, 1)">3</span> }</pre>
</div>
<p>运行时生成如下样式:</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> <span style="color: rgba(128, 0, 0, 1)"><style>
</span><span style="color: rgba(0, 128, 128, 1)">2</span> <span style="color: rgba(128, 0, 0, 1)">h2 </span>{
<span style="color: rgba(0, 128, 128, 1)">3</span> <span style="color: rgba(255, 0, 0, 1)"> color</span>:<span style="color: rgba(0, 0, 255, 1)"> red</span>;
<span style="color: rgba(0, 128, 128, 1)">4</span> }
<span style="color: rgba(0, 128, 128, 1)">5</span> <span style="color: rgba(128, 0, 0, 1)"></style></span></pre>
</div>
<p>此样式将应用于app-root内所有h2元素。</p>
<p>这种选择器的组合很有用,当将样式应用到使用ng-content传递给模板的元素。</p>
<p><strong>:host-context</strong></p>
<p>根据宿主元素的祖先元素的某些条件,将样式应用于组件模板中的元素,这时:host-context会很有用。</p>
<p>注:只有宿主元素及其后代会受到样式影响,而不是祖先元素。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)"> 1</span> <span style="color: rgba(0, 0, 0, 1)">@Component({
</span><span style="color: rgba(0, 128, 128, 1)"> 2</span> selector: 'themeable-button'<span style="color: rgba(0, 0, 0, 1)">,
</span><span style="color: rgba(0, 128, 128, 1)"> 3</span> <span style="color: rgba(0, 0, 0, 1)">template: `
</span><span style="color: rgba(0, 128, 128, 1)"> 4</span> <button class="btn btn-theme">Themeable Button</button>
<span style="color: rgba(0, 128, 128, 1)"> 5</span> <span style="color: rgba(0, 0, 0, 1)">`,
</span><span style="color: rgba(0, 128, 128, 1)"> 6</span> <span style="color: rgba(0, 0, 0, 1)">styles: [`
</span><span style="color: rgba(0, 128, 128, 1)"> 7</span> :host-context(.red-theme) .btn-<span style="color: rgba(0, 0, 0, 1)">theme {
</span><span style="color: rgba(0, 128, 128, 1)"> 8</span> <span style="color: rgba(0, 0, 0, 1)"> background: red;
</span><span style="color: rgba(0, 128, 128, 1)"> 9</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">10</span> :host-context(.blue-theme) .btn-<span style="color: rgba(0, 0, 0, 1)">theme {
</span><span style="color: rgba(0, 128, 128, 1)">11</span> <span style="color: rgba(0, 0, 0, 1)"> background: blue;
</span><span style="color: rgba(0, 128, 128, 1)">12</span> <span style="color: rgba(0, 0, 0, 1)"> }
</span><span style="color: rgba(0, 128, 128, 1)">13</span> <span style="color: rgba(0, 0, 0, 1)">`]
</span><span style="color: rgba(0, 128, 128, 1)">14</span> <span style="color: rgba(0, 0, 0, 1)">})
</span><span style="color: rgba(0, 128, 128, 1)">15</span> <span style="color: rgba(0, 0, 0, 1)">export class ThemeableButtonComponent {
</span><span style="color: rgba(0, 128, 128, 1)">16</span>
<span style="color: rgba(0, 128, 128, 1)">17</span> }</pre>
</div>
<p>现在的样式是不起作用的;为了使样式生效,需要向该组件的任何父元素添加一个主题激活类。</p>
<div class="cnblogs_code">
<pre><span style="color: rgba(0, 128, 128, 1)">1</span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">div </span><span style="color: rgba(255, 0, 0, 1)">class</span><span style="color: rgba(0, 0, 255, 1)">="blue-theme"</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 128, 128, 1)">2</span> <span style="color: rgba(0, 0, 255, 1)"><</span><span style="color: rgba(128, 0, 0, 1)">themeable-button</span><span style="color: rgba(0, 0, 255, 1)">></</span><span style="color: rgba(128, 0, 0, 1)">themeable-button</span><span style="color: rgba(0, 0, 255, 1)">></span>
<span style="color: rgba(0, 128, 128, 1)">3</span> <span style="color: rgba(0, 0, 255, 1)"></</span><span style="color: rgba(128, 0, 0, 1)">div</span><span style="color: rgba(0, 0, 255, 1)">></span></pre>
</div>
<p> </p><br><br>
来源:https://www.cnblogs.com/sparkler/p/16928600.html 看到这么详细的Angular样式隔离教程,必须来支持下!总结得很到位,尤其是对Emulated模式下_nghost和_ngcontent属性的解释,让我对Angular的样式隔离机制有了更清晰的理解。
之前一直只知道用ViewEncapsulation.Emulated是默认值,但具体怎么实现隔离的原理不太清楚,看完你的图示就完全明白了。那些属性原来是Angular自动加上的"魔法属性",用来做样式匹配的~
想请教几个小问题:
[*]关于::ng-deep,虽然你提到建议配合:host使用,但实际项目中会不会有场景不得不单独使用::ng-deep?比如要覆盖第三方组件的样式但又不能改它们的selector?
[*]对于:host-context,如果我想同时检测多个祖先类名,有没有什么好的写法?还是只能写多个:host-context?
[*]在ShadowDom模式下,这些伪类选择器还能正常工作吗?会不会有什么兼容性问题?
另外补充一点个人经验:在实际项目中,能不用::ng-deep就尽量别用,因为确实容易造成样式污染。我们后来都改成通过Input传class或者用::ng-deep + :host严格限定范围。
总之感谢楼主的分享,博客园上这种实战型的技术文章真的很受用!期待更多Angular相关的干货~(手动点赞)
頁:
[1]