Angular 样式隔离

本文详细讲解如何在 Angular 使用样式隔离。

在讲解 Angular 样式隔离之前,我们先整体了解一下为什么要有 CSS 样式隔离,已经有哪些样式隔离方案。

1. 原生 CSS 的一些问题

1.1. 无作用域样式污染

CSS 有一个被大家诟病的问题就是没有本地作用域,所有声明的样式都是全局的(global styles)

换句话来说页面上任意元素只要匹配上某个选择器的规则,这个规则就会被应用上,而且规则和规则之间可以层叠(cascading)

SPA 应用流行了之后这个问题变得更加突出了,因为对于 SPA 应用来说所有页面的样式代码都会加载到同一个环境中,样式冲突的概率会大大加大。由于这个问题的存在,我们在日常开发中会遇到以下这些问题:

  • 很难为选择器起名字

    为了避免和页面上其他元素的样式发生冲突,我们在起选择器名的时候一定要深思熟虑,起的名字一定不能太普通。举个例子,假如你为页面上某个作为标题的 DOM 节点定义一个叫做.title 的样式名,这个类名很大概率已经或者将会和页面上的其他选择器发生冲突,所以你不得不手动为这个类名添加一些前缀,例如.home-page-title 来避免这个问题

  • 团队多人合作困难

    当多个人一起开发同一个项目的时候,特别是多个分支同时开发的时候,大家各自取的选择器名字可能有会冲突,可是在本地独立开发的时候这个问题几乎发现不了。当大家的代码合并到同一个分支的时候,一些样式的问题就会随之出现

1.2. 无用的 CSS 样式堆积

进行过大型 Web 项目开发的同学应该都有经历过这个情景:在开发新的功能或者进行代码重构的时候,由于 HTML 代码和 CSS 样式之间没有显式的一一对应关系,我们很难辨认出项目中哪些 CSS 样式代码是有用的哪些是无用的,这就导致了我们不敢轻易删除代码中可能是无用的样式。这样随着时间的推移,项目中的 CSS 样式只会增加而不会减少(append-only stylesheets)。无用的样式代码堆积会导致以下这些问题:

  • 项目变得越来越重量级:加载到浏览器的 CSS 样式会越来越多,会造成一定的性能影响

  • 开发成本越来越高:开发者发现他们很难理解项目中的样式代码,甚至可能被大量的样式代码吓到,这就导致了开发效率的降低以及一些奇奇怪怪的样式问题的出现

1.3. 基于状态的样式定义

对于 SPA 应用来说,特别是一些交互复杂的页面,页面的样式通常要根据组件的状态变化而发生变化

最常用的方式是通过不同的状态定义不同的 className 名,这种方案代码看起来十分冗余和繁琐,通常需要同时改动 js 代码和 css 代码

“这个 CSS 重写一遍比修改老文件快”,这样的念头几乎所有人都曾有过,css 虽然看似简单,但是以上问题很容易写着写着就出现了,这在于提前没有选好方案

2. CSS 样式隔离方案

解决以上 CSS 样式问题的思路就 CSS 隔离,让页面和页面之间, 组件与组件之间的 CSS 样式相互独立。于是诞生了以下几种解决方案。

  • BEM
  • CSS Modules
  • CSS in JS
  • 预处理器
  • Shadow DOM

2.1. BEM

是由 Yandex 团队提出的一种 CSS Class 命名方法,详情可以参考BEM: A New Front-End Methodology

BEM 通过 css 选择器命名规范来避免 CSS 样式之间发生冲突,从而实现样式与样式之间互相隔离,BEM 的意思是块(Block)、元素(Element)、修饰符(Modifier)的简写

这种命名方法让 CSS 便于统一团队开发规范和方便维护
以 .blockelement–modifier 或者说 block-nameelement-name–modifier-name 形式命名,命名有含义,也就是模块名 + 元素名 + 修饰器名
如.dropdown-menu__item–active

社区里面对 BEM 命名的褒贬不一,但是对其的思想基本上还是认同的,所以可以用它的思想,不一定要用它的命名方式

BEM 思想通常用于组件库,业务代码中结合 Sass, less 等预处理器

2.1.1. BEM 优缺点分析

优点:

人为严格遵守 BEM 规范,可以解决无作用域样式污染问题
可读性好,一目了然是那个 dom 节点,对于无用 css 删除,删除了相应 dom 节点后,对应的 css 也能比较放心的删除,不会影响到其他元素样式

缺点

BEM 命名其最大的争议就是其命名风格,命名太长(个人开发习惯、部分人会觉得,我认为命名长提高了可读性,能解决一些问题,也不叫缺点),至于体积增大,gzip 可忽略

2.2. CSS Modules

什么是 CSS Modules?

顾名思义,css-modules 将 css 代码模块化,可以避免本模块样式被污染,并且可以很方便的复用 css 代码

根据 CSS Modules 在Gihub 上的项目,CSS Modules 解释为:

A CSS Module is a CSS file in which all class names and animation names are scoped locally by default. All URLs (url(…)) and @imports are in module request format (./xxx and ../xxx means relative, xxx and xxx/yyy means in modules folder, i. e. in node_modules).

CSS Modules 既不是官方标准,也不是浏览器的特性,而是在构建步骤(例如使用 Webpack,记住 css-loader)中对 CSS 类名和选择器限定作用域的一种方式(类似于命名空间)

依赖 webpack css-loader,配置如下,现在 webpack 已经默认开启 CSS modules 功能了

1
2
3
4
5
6

{
test: /.css$/,
loader: "style-loader!css-loader?modules"
}

我们先看一个示例:

将 CSS 文件 style.css 引入为 style 对象后,通过 style.title 的方式使用 title class:

1
2
3
4
5
6
7
8
9
10
11

import style from './style.css';

export default () => {
return (
<p className={style.title}>
I am KaSong.
</p>
);
};

对应 style.css:

1
2
3
.title {
color: red;
}

打包工具会将 style.title 编译为带哈希的字符串

1
<h1 class="_3zyde4l1yATCOkgn-DBWEL">Hello World</h1>

同时 style.css 也会编译:

._3zyde4l1yATCOkgn-DBWEL {
color: red;
}

这样,就产生了独一无二的 class,解决了 CSS 模块化的问题

使用了 CSS Modules 后,就相当于给每个 class 名外加加了一个 :local,以此来实现样式的局部化,如果你想切换到全局模式,使用对应的 :global。

:local 与 :global 的区别是 CSS Modules 只会对 :local 块的 class 样式做 localIdentName 规则处理,:global 的样式编译后不变

1
2
3
4
5
6
7
.title {
color: red;
}

:global(.title) {
color: green;
}

可以看到,依旧使用 CSS,但使用 JS 来管理样式依赖, 最大化地结合现有 CSS 生态和 JS 模块化能力,发布时依旧编译出单独的 JS 和 CSS

2.2.1. CSS Modules 的优缺点分析

优点

  • 能 100%解决 css 无作用域样式污染问题
  • 学习成本低:API 简洁到几乎零学习成本

缺点

  • 代码可读性差,hash 值不方便 debug,不利于问题排查。

  • 写法没有传统开发流程有很大差异,需要频繁引入 style.css 等样式文件。

  • 没有变量,通常要结合预处理器。

如果你不想频繁的输入 styles.,可以试一下 [react-css-modules](gajus/react-css-modules · GitHub),它通过高阶函数的形式来避免重复输入 styles.
css modules 通常结合 less 等预处理器在 react 中使用,更多可参考CSS Modules 详解及 React 中实践

2.3. CSS in JS

CSS in JS 是 2014 年推出的一种设计模式,它的核心思想是把 CSS 直接写到各自组件中,也就是说用 JS 去写 CSS,而不是单独的样式文件里。

这跟传统的前端开发思维不一样,传统的原则是关注点分离,如常说的不写行内样式、不写行内脚本,如下代码

1
<h1 style="color:red;font-size:46px;" onclick="alert('Hi')">Hello World</h1>

CSS-in-JS 不是一种很新的技术,可是它在国内普及度好像并不是很高,它当初的出现是因为一些 component-based 的 Web 框架(例如 React,Vue 和 Angular)的逐渐流行,使得开发者也想将组件的 CSS 样式也一块封装到组件中去以解决原生 CSS 写法的一系列问题

CSS-in-JS 在 React 社区的热度是最高的,这是因为 React 本身不会管用户怎么去为组件定义样式的问题,而 Vue 和 Angular 都有属于框架自己的一套定义样式的方案

上面的例子使用 React 改写如下

1
2
3
4
5
6
7
8
9
10
11
12
13
const style = {
color: "red",
fontSize: "46px",
};

const clickHandler = () => alert("hi");

ReactDOM.render(
<h1 style={style} onclick={clickHandler}>
Hello, world!
</h1>,
document.getElementById("example")
);

上面代码在一个文件里面,封装了结构、样式和逻辑,完全违背了”关注点分离”的原则。
但是,这有利于组件的隔离。每个组件包含了所有需要用到的代码,不依赖外部,组件之间没有耦合,很方便复用。所以,随着 React 的走红和组件模式深入人心,这种”关注点混合”的新写法逐渐开始流行。

实现了 CSS-in-JS 的库有很多,据统计现在已经超过了 61 种。

从实现方法上区分大体分为两种:

唯一 CSS 选择器,代表库:styled-components
内联样式(Unique Selector VS Inline Styles)

2.3.1. CSS-in-JS 优缺点分析

优点

  • 没有无作用域问题样式污染问题

  • 通过唯一 CSS 选择器或者行内样式解决

  • 没有无用的 CSS 样式堆积问题

    CSS-in-JS 会把样式和组件绑定在一起,当这个组件要被删除掉的时候,直接把这些代码删除掉就好了,不用担心删掉的样式代码会对项目的其他组件样式产生影响。

    而且由于 CSS 是写在 JavaScript 里面的,我们还可以利用 JS 显式的变量定义,模块引用等语言特性来追踪样式的使用情况,这大大方便了我们对样式代码的更改或者重构

  • 更好的基于状态的样式定义

    CSS-in-JS 会直接将 CSS 样式写在 JS 文件里面,所以样式复用以及逻辑判断都十分方便

缺点:

  • 一定的学习成本

  • 违背了”关注点分离”的原则

  • 代码可读性差

  • 大多数 CSS-in-JS 实现会通过生成唯一的 CSS 选择器来达到 CSS 局部作用域的效果。这些自动生成的选择器会大大降低代码的可读性,给开发人员 debug 造成一定的影响

  • 运行时消耗

    由于大多数的 CSS-in-JS 的库都是在动态生成 CSS 的。这会有两方面的影响。首先你发送到客户端的代码会包括使用到的 CSS-in-JS 运行时(runtime)代码,这些代码一般都不是很小,例如 styled-components 的 runtime 大小是 12.42kB min + gzip,如果你希望你首屏加载的代码很小,你得考虑这个问题。其次大多数 CSS-in-JS 实现都是在客户端动态生成 CSS 的,这就意味着会有一定的性能代价。不同的 CSS-in-JS 实现由于具体的实现细节不一样,所以它们的性能也会有很大的区别,你可以通过这个工具来查看和衡量各个实现的性能差异

  • 不能结合成熟的 CSS 预处理器(或后处理器)Sass/Less/PostCSS,:hover 和 :active 伪类处理起来复杂

2.4. 预处理器

CSS 预处理器是一个能让你通过预处理器自己独有的语法的程序

市面上有很多 CSS 预处理器可供选择,且绝大多数 CSS 预处理器会增加一些原生 CSS 不具备的特性,例如

  • 代码混合
  • 嵌套选择器
  • 继承选择器

这些特性让 CSS 的结构更加具有可读性且易于维护

我们常见的预处理器:

  • Sass
  • LESS
  • Stylus

CSS 预处理器的模块化功能可以通过以下两种方式实现:

  • 导入和导出:CSS 预处理器可以使用导入和导出语句来将 CSS 模块相互引用。导入语句用于将其他模块的样式引入当前模块,导出语句用于将当前模块的样式导出供其他模块使用。
  • 命名空间:CSS 预处理器可以使用命名空间来为 CSS 模块分配唯一的名称。命名空间可以防止不同模块之间的 CSS 样式冲突。

例如,以下 Sass 代码使用导入和导出语句来实现模块化:

1
2
3
4
5
6
7
8
9
10

// 导入模块 `my-styles`
@import './my-styles';

// 定义模块 `main`
.main {
background-color: #fff;
color: #000;
}

以下 Sass 代码使用命名空间来实现模块化:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

// 定义模块 `my-styles` 的命名空间
@namespace my-styles;

// 使用命名空间定义模块 `my-styles`
.my-styles {
background-color: #fff;
color: #000;
}

// 在其他模块中使用模块 `my-styles`
.other-module {
@import 'my-styles/my-styles';
}

使用 CSS 预处理器的模块化和变量功能可以有效实现 CSS 隔离。通过 CSS 隔离,可以避免不同模块之间的 CSS 样式冲突,从而提高 CSS 代码的维护性和可扩展性。

2.4.1. 预处理器优缺点分析

优点:

  • 利用嵌套,人为严格遵守嵌套首类名不一致,可以解决无作用域样式污染问题
  • 可读性好,一目了然是那个 dom 节点,对于无用 css 删除,删除了相应 dom 节点后,对应的 css 也能比较放心的删除,不会影响到其他元素样式

缺点

  • 需要借助相关的编译工具处理

预处理器是现代 web 开发中必备,结合 BEM 规范,利用预处理器,可以极大的提高开发效率,可读性,复用性

2.5. Shadow DOM

熟悉 web Components 的一定知道 Shadow DOM 可以实现样式隔离,由浏览器原生支持。

Shadow DOM 是通过将 DOM 树封装在一个 shadow root 中来实现 CSS 隔离的。shadow root 是一个独立的 DOM 树,它与主文档 DOM 树是隔离的。

shadow root 由 shadow host 元素创建。shadow host 元素是容纳 shadow root 的元素。

要创建 shadow root,可以使用 shadowRoot 属性。例如,以下代码创建了一个 shadow root

1
2
3
<div id="my-element">
<button>Click me</button>
</div>
1
2
3
4
const myElement = document.getElementById("my-element");

// 创建 shadow root
const shadowRoot = myElement.shadowRoot;

一旦创建了 shadow root,就可以将 CSS 样式应用于 shadow root 中的元素。这些样式将仅影响 shadow root 中的元素,而不会影响主文档中的元素。

例如,以下代码将 CSS 样式应用于 shadow root 中的按钮:

1
2
3
4
#my-element > shadow button {
background-color: red;
color: white;
}

我们经常在微前端领域看到 Shadow DOM,如下创建一个子应用

1
2
3
const shadow = document.querySelector('#hostElement').attachShadow({mode:
'open'}); shadow.innerHTML = '<sub-app>Here is some new text</sub-app
><link rel="stylesheet" href="//unpkg.com/antd/antd.min.css" />';

子应用的样式作用域仅在 shadow 元素下。

Shadow DOM 还可以帮助提高 CSS 代码的维护性和可扩展性。通过将 CSS 样式与组件封装在 shadow root 中,可以使 CSS 代码更加清晰和易于理解。

2.5.1. Shadow DOM 优缺点分析

优点

  • 浏览器原生支持
  • 严格意义上的样式隔离,如 iframe 一样

缺点

  • 浏览器兼容问题
  • 只对一定范围内的 dom 结构起作用

普通业务开发我们还是用框架、如 Vue、React;Shadow DOM 适用于特殊场景,如微前端

2.6. 隔离方案小结

说明优点缺点
BEM不同项目用不同的前缀+命名规则避开冲突简单依赖约定,容易出现纰漏
CSS modules通过编译生成不冲突的选择器名可靠易用,避免人工约束只能在构建期使用,依赖打包工具如 css-loader
CSS in JSCSS 和 JS 编码在一起,最终实现不冲突的选择器基本彻底避免冲突运行时开销大,缺失完整的 CSS 能力
预处理器利用嵌套实现简单,提高工作效率同依赖约定,容易出现纰漏
Shadow DOM浏览器原生 CSS 沙箱支持原生支持只适用与特定场景

3. Angular 样式隔离

Angular 样式隔离在 Angular 领域有一个新的名词 View encapsulation(视图封装),其实现原理非常 Smart, 他为每个组件(也可以称为视图 view)添加了一个用于表示其唯一性的属性,而在最终生成的 css 样式,如果属于特定视图都会携带一个属性选择器。这样通过属性标识,特定组件的样式只会被应用于相应组件而不会污染其它组件,从而实现样式隔离。

这样的做法带来很多好处,开发人员依然使用以前在编写 MPA 时一样 CSS 编写习惯,在命名规范上不必有太多约束。而 Angular 平台或者框架在编译项目时,会处理所有样式隔离相关事项。

跟以上所有 CSS 隔离方案相比,它几乎没有给用户带来任何的约束和负担。比如 BEM 的命名规范;CSS Modules 在 CSS 引用方式的改变;CSS in JS 在样式定义以及引用方面的用户习惯改变;Shadow Dom 样式只能局限在子应用的局限,Angular 提供了混合模式,相比 Shadow Dom 它更加灵活,它提供了将组件样式应用到全局的能力。

预处理器是对 CSS 定义方式的一种增强,Angular 很好的结合预处理器在 CSS 处理上的优势。

3.1. Angular 样式隔离的原理

这样描述 Angular 样式隔离有些抽象,我们以实际的例子来说明 Angular 样式隔离的原理。

我们来看一个样式污染的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<body>
<style>
/* style for component1 */
a {
font-style: italic;
}
</style>

<div id="component1">
<a>Link 1</a>
</div>

<body></body>
</body>

在上面的例子中, 我们的本意是给 component1 组件的超连接添加上斜体样式。当只有这一个组件的时候, 它工作得很好,与我们期待得一模一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<style>
/* style for component1 */
a {
font-style: italic;
}
</style>

<style>
/* style for component2 */
a {
color: red;
}
</style>

<div id="component1">
<a>Link 1</a>
</div>

<div id="component2">
<a>Link 1</a>
</div>
<body></body>
</body>

如上例,随着我们业务得增长, 我们又有了新的功能,这时我们添加了 Component2,这是一件很令人开心的事情. component2 的颜色我们为它设置为红色。但是当两个组件组合到一起时样式就发生了冲突,在引入 Component 2 后,component1 的 a 标签字体颜色发生了改变,而这种改变不是我们所期待的。component2 的样式也因为 component1 的存在也受到了影响变成。

上面的例子只是对现实 CSS 污染现象一个简化,我们不可能懒惰到不为组件添加区块标识,元素标识,以实现样式隔离。

而 Angular 为会为我们自动完成标识组件标识,只要我们的项目结构符合 Angular 规范。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<body>
<style>
/* style for component1 */
a[componentIdentifier-xxzz] {
font-style: italic;
}
</style>

<style>
/* style for component2 */
a[componentIdentifier-yyqq] {
color: red;
}
</style>

<div id="component1" componentIdentifier-xxzz>
<a componentIdentifier-xxzz></a>
</div>

<div id="component2" componentIdentifier-yyqq>
<a componentIdentifier-yyqq></a>
</div>
<body></body>
</body>

如上, Angular 会自动为我们添加上组件标识,从而避免样式污染, 这就是 Angular 样式隔离的基本原理。

3.2. Angular 三种视图封装模式

Angular 的三种封装模式分别为

  • ViewEncapsulation.Emulated

  • ViewEncapsulation.None

  • ViewEncapsulation.ShadowDom

3.2.1. ViewEncapsulation.Emulated

Emulated的意思是模仿,效仿, 仿真的意思,就是效仿 shadowDom 的行为即模仿 shadowDom 样式隔离效果。

ViewEncapsulation.Emulated:这也是 Angular 的默认视图封装模式, 使样式仅应用于组件的视图,不会影响应用程序中的其他组件。组件的样式被添加到文档的<head>区域,使它们在整个应用程序中可用,但只影响它们各自组件模板中的元素。

Emulated 的基本原理在上一节已经讲过了,现在我们再看一个复杂一点的情况。再前面的例子中,我们提到的 component1 和 component2 属于兄弟关系即平级关系。我们再来看一个父子关系的例子。

在下面的例子中,component3 是 component2 的子组件。从示例可以看出 component2 和 component3 依然是互相隔离的。假设 component3 是第三方提供的组件,但是它的样式需要一些调整才能适应目前的使用环境。此时我们可以定义全局样式来修改 component3,这种办法可以解决问题,但是不是理想的解决方案,会带来文章开头列出的一些问题即样式污染等问题,随着时间的增长,代码变得难以维护。而且如果 component2 本身也是提供给其它系统使用的,我们也没有机会去操纵调用系统的全局属性,也应该尽量避免要求调用方强制引入某些全局属性。

更好的办法是在父组件范围内即本例中的 component2 范围内定义样式来改变子组件的样式。此时就需要用到 ::ng-deep 和 :host 关键字了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<body>
<style>
/* style for component1 */
a[componentIdentifier-xxzz] {
font-style: italic;
}
</style>

<style>
/* style for component2 */
a[componentIdentifier-yyqq] {
color: red;
}
</style>

<style>
/* style for component3 */
a[componentIdentifier-ppbb] {
font-size: 50px;
}
</style>

<div id="component1" componentIdentifier-xxzz>
<a componentIdentifier-xxzz></a>
</div>

<div id="component2" componentIdentifier-yyqq>
<a componentIdentifier-yyqq></a>

<div id="component3" componentIdentifier-ppbb>
<a componentIdentifier-ppbb></a>
</div>
</div>
<body></body>
</body>

当定义样式时加上::ng-deep, 那么 Angular 在编译时就不会给样式加上属性选择器了,如果单纯只使用::ng-deep 关键字,样式又会变成全局样式。

如果我们要限定样式子在父组件及其包含的子组件有效,此时需要在::ng-deep 前添加:host 加以限度,这样样式只会作用于父组件及其子组件。

这样描述比较抽象, 我们来看一个实际的例子。

比如我们在 component2 的样式中这样定义

1
2
3
4
5
6
7
a {
color: red;
}

:host ::ng-deep a {
font-size: 16px;
}

那么编译后的代码,类似下面这样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
<body>
<style>
/* style for component1 */
a[componentIdentifier-xxzz] {
font-style: italic;
}
</style>

<style>
/* style for component2 */
a[componentIdentifier-yyqq] {
color: red;
}
/* ::ng-deep的作用及时告诉编译器要在选择器上添加属性选择器
:host的作用是限定样式只在组件2及其子组件中有效
*/
[componentIdentifier-yyqq] a {
font-size: 16px;
}
</style>

<style>
/* style for component3 */
a[componentIdentifier-ppbb] {
font-size: 24px;
}
</style>

<div id="component1" componentIdentifier-xxzz>
<a componentIdentifier-xxzz></a>
</div>

<div id="component2" componentIdentifier-yyqq>
<a componentIdentifier-yyqq></a>

<div id="component3" componentIdentifier-ppbb>
<a componentIdentifier-ppbb></a>
</div>
</div>
<body></body>
</body>

::ng-deep 的作用及时告诉编译器要在选择器上添加属性选择器
:host 的作用是限定样式只在组件 2 及其子组件中有效

以上是站在组件使用者的角度来考虑如何适配第三方组件。
如果站在组件定义者的角度能否做一些事情,让自己定义的组件变得更能适应复杂环境呢?答案当然是有的,此时就是:host-context 登场的时候了。

在 component3 中我们这样定义样式,那么我们就为调用放准备了两套方案,一套是为超大屏幕准备的字体大小是 24px, 一套是为小屏幕准备的字体大小是 16px

1
2
3
4
5
6
:host-context(.screen-xl) a {
font-size: 24px;
}
:host-context(.screen-sm) a {
font-size: 16px;
}

那么在调用放只要是由相应的 class 即可控制 component 的字体大小。

1
2
3
4
5
6

<div class=".screen-xl">
<a></a>
<component3></component3>
<div>

编译后完整的代码类似这样

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
<body>
<style>
/* style for component1 */
a[componentIdentifier-xxzz] {
font-style: italic;
}
</style>

<style>
/* style for component2 */
a[componentIdentifier-yyqq] {
color: red;
}
</style>

<style>
/* style for component3 */
.screen-xl a[componentIdentifier-ppbb] {
font-size: 24px;
}

.screen-sm a[componentIdentifier-ppbb] {
font-size: 16px;
}
</style>

<div id="component1" componentIdentifier-xxzz>
<a componentIdentifier-xxzz></a>
</div>

<div id="component2" class=".screen-xl" componentIdentifier-yyqq>
<a componentIdentifier-yyqq></a>

<div id="component3" componentIdentifier-ppbb>
<a componentIdentifier-ppbb></a>
</div>
</div>
<body></body>
</body>

3.2.2. ViewEncapsulation.None

ViewEncapsulation.None:不使用任何类型的视图封装,为组件指定的任何样式都是全局应用的,并且影响应用程序中的任何 HTML 元素。组建的样式被添加到文档的<head>中,使它们在整个应用程序中可用,所以时完全全局的,并影响文档中的任务匹配元素。

ViewEncapsulation.None 顾名思义就是不封装,其中定义的任何属性都会作用于全局范围,这可能是 Angular 为了保存框架的灵活性。在某些特殊的使用场景下可能会用到这种模式。在使用此模式时,需要特别注意的地方是,前面提到的:host ::ng-deep :host-context 关机字都不会生效。我们要了解这些关键字生效也是有前提的。

3.2.3. ViewEncapsulation.ShadowDom

ViewEncapsulation.ShadowDom: Angualr 使用浏览器内置的 Shadow Dom API 将组件的视图封装在 ShadowRoot 中,用作组件的宿主元素,并以隔离的方式应用提供的样式(只对浏览器内置 Shadow Dom 支持时才起作用)。组件的样式只添加到 Shadow Dom 宿主中,确保它们只影响各自组件视图中的元素。

ShadowDom 使用了浏览器内置的 Shadow Dom API, 样式是放在组件内部而不是 head 区域。如果使用惰性加载,那么可以大大减少 head 区域 css 的内容,这对解决 FCP 问题有帮助,以及 SEO 都有帮助。另外在微前端案例中,ShadowDom 是一种不错的选择。Angular 定义这种模式更多的可能是为微前端准备的。

其原理参考 2.5 节中关于 Shadow DOM 的 介绍。

4. 相关阅读

本技术博客原创文章位于Angular 样式隔离 | 鹏叔的技术博客, 要获取最近更新请访问原文.

更多 Angular 相关文章请访问Angular 合集 | 鹏叔的技术博客

更多技术博客请访问: 鹏叔的技术博客

5. 参考文档

Angular 样式隔离(style isolation)及选择器(:host, :host-context, ::ng-deep)的使用

你知道几种 CSS 样式隔离方案?

如何看待 CSS 中 BEM 的命名方式?

Introduction to Styling and Style Isolation in Angular