CSS-架构教程-二-

110 阅读1小时+

CSS 架构教程(二)

原文:Architecting CSS

协议:CC BY-NC-SA 4.0

五、兼容性和默认值

在编写 CSS 时,大多数开发人员很快就会意识到,当相同的代码在不同的浏览器中运行,甚至在不同的设备上运行时,它们的行为会有所不同。本章介绍了浏览器的差异和处理跨浏览器兼容性的技术。

浏览器支持

测试布局时,在多种浏览器中测试应用程序很重要,因为它们并不都使用相同的布局和 JavaScript 引擎,这导致它们解释代码的方式有所不同。表 5-1 列出了一些常用的浏览器及其引擎。

表 5-1

浏览器技术

|

浏览器

|

布局引擎

|

JavaScript 引擎

| | --- | --- | --- | | 铬 | 眨眼,WebKit | V8 | | 火狐浏览器 | 壁虎,量子 | 蜘蛛猴 | | 微软公司出品的 web 浏览器 | 三叉戟 | 脉轮,JScript | | 微软边缘 | EdgeHTML,WebKit(在 IOS 上),Blink(在 Android 上)——切换到 Chromium 平台 1 | 人体精神力量的中心 | | 歌剧 | 闪烁(铬) | 铬 V8 | | 旅行队 | 网络工具包 | 硝基 |

布局引擎负责页面的外观。它根据 CSS 决定视图应该如何布局、绘制和动画。渲染细节见第一章。此外,许多都是开源的,由不同的团体和机构维护,允许任何给定规范的实现和状态存在差异。

例如,scroll-snap-type CSS 属性是 Scroll Type 模块的一部分,它的第一个公开草案于 2015 年 3 月发布,现在是推荐的候选对象,它在不同的浏览器中具有非常不同的支持和实现。这就导致了行为差异。具体浏览器支持详情见表 5-2 。

表 5-2

浏览器版本 3 支持滚动捕捉

| ![img/487079_1_En_5_Figa_HTML.png](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/deb3bb87270347f1b2e01d02c0e76614~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771253023&x-signature=xMBy6N4jg%2FJrrV6erarfNg%2BsRDg%3D) |

随着时间的推移,很明显使用这个属性会在不同的浏览器上产生不同的结果。此外,浏览器包括 CSS 默认设置,这些也略有不同。

浏览器默认值

当编写没有应用 CSS 的 HTML 时,某些标签具有默认样式,例如 header 标签(参见图 5-1 )。

img/487079_1_En_5_Fig1_HTML.jpg

图 5-1

默认样式

然而,浏览器不使用相同的样式表,因此没有相同的默认设置。虽然大多相似,但也有一些细微的差别。例如,Textarea 在 Safari 和 Firefox 中的表现会有所不同(见图 5-2 和 5-3 )。

img/487079_1_En_5_Fig3_HTML.jpg

图 5-3

旅行队

img/487079_1_En_5_Fig2_HTML.jpg

图 5-2

火狐浏览器

注意文本区域中的默认字体;在 Firefox 中是等宽字体,而在 Safari 中是无衬线字体。对齐方式也略有不同。在 Firefox 上,textarea 与文本的基线对齐,而在 Safari 上,textarea 则在基线之上。当试图让一个设计在不同的浏览器和版本中具有相同的外观和行为时,这些细微的差异可能会令人恼火。

解决这个问题的一个可靠的方法是手动设置默认值,这样所有的浏览器都可以运行相同的基本样式。虽然这没有解决兼容性差异,但是它将解决细微的非故意行为差异,例如前面概述的差异。

CSS 重置

CSS reset 是一个文件,它接受浏览器对元素设置的所有默认值,并“重置”它们。目标是获取元素样式,并使它们都达到相同的一致基线,以减少或消除浏览器之间存在的不一致。有很多选择,但最常用的是 Eric Meyer(见表 5-3 ),他是 CSS reset 的先驱之一。无论你使用哪一个,都没有一个尺寸适合所有人,它可能需要根据你的特定项目进行定制。

表 5-3

CSS 重置

|

参考

|

| | --- | --- | | 工程地点 | https://meyerweb.com/eric/tools/css/reset/index.html | | 样式表 | https://meyerweb.com/eric/tools/css/reset/reset.css |

使标准化

Normalize 是尼古拉斯·加拉格尔和乔纳森·尼尔在 2016 年 8 月发表的一个项目。它专注于修复浏览器之间的已知差异。这种方法与 CSS 重置完全不同,CSS 重置旨在通过扁平化默认样式来防止差异。Normalize 保留默认值。通过将 normalize 添加为项目中要加载的第一个 CSS,使其成为要导入的第一个样式表,或者将其作为要应用的第一个 CSS 包含在项目的 CSS 中,这些变化已经得到处理,并且焦点可以转移到实现布局上,而不是与浏览器之间的细微差异作斗争(关于在哪里找到 normalize,请参见表 5-4 )。值得指出的是,许多 CSS 框架和库,比如 Bootstrap,已经包含了某种形式的规范化。值得仔细检查正在使用的任何 UI 库或框架是否已经考虑到差异,以防止不必要的膨胀。

表 5-4

使标准化

|

参考

|

| | --- | --- | | 工程地点 | http://necolas.github.io/normalize.css/ | | GitHub 存储库 | https://github.com/necolas/normalize.css | | 新公共管理理论 | www.npmjs.com/package/normalize.css | | 加拿大 | https://yarnpkg.com/en/package/normalize.css | | 样式表 | https://necolas.github.io/normalize.css/latest/normalize.css |

尽管规范化基本样式解决了 CSS 默认值的差异,但它并没有解决实现或支持方面的差异。

Note

Normalize 和 reset 没有得到任何形式的认可,任何包的质量和相关性都可能会发生快速变化。请研究您打算使用的任何依赖项。

浏览器兼容性

跨浏览器兼容性,确保 UI 在多个浏览器上看起来一样,是 CSS 中最难做到的事情之一。有多种方法可以解决这个问题,它们经常相互结合使用。

供应商前缀

当浏览器的功能仍处于试验阶段或非标准时,浏览器通常使用特定于供应商的前缀来提供这些功能。虽然这可能有助于在用户代理之间达到相似性,但是在生产中使用依赖于供应商前缀的 CSS 不是一个好主意,因为实现是实验性的,可能不符合规范。因为历史上开发人员一直在产品中使用这些前缀,所以浏览器越来越倾向于将非标准的和实验性的特性放在特性标志之后,以结束这种做法;然而,许多仍在使用中(见表 5-5 )。 4

表 5-5

供应商前缀

|

前缀

|

浏览器

| | --- | --- | | -网络工具包- | 基于 WebKit 的浏览器(Chrome、Safari 等。) | | 蚊子 | 火狐浏览器 | | 用作复合形式的末尾元音 | Opera 的 WebKit 前版本 | | -女士- | Internet Explorer 和 Microsoft Edge |

例如,Internet Explorer 11 (IE)就有一个非标准的网格实现。其实现基于 2011 年 4 月 7 日的工作草案,而非候选人推荐。因此,为了让网格在 IE 中工作,必须使用供应商前缀。然而,即使使用了前缀,行为仍然不同。在 IE 中,明确定位网格中的每个元素是必要的,但在其他浏览器中则不然,它们将自己放置在下一个可用空间中。此外,当前规范的某些方面,如grid-gap,根本就不存在。清单 5-2 和 5-3 展示了使用 grid 时在 IE 和 Firefox 中实现相同布局的代码。两者都将使用相同的 HTML(清单 5-1 )。它们各自的输出如图 5-4 和 5-6 所示,而图 5-5 显示的是没有厂商前缀的 IE。

<body>
  <div class="grid-container">
    <aside>My Aside</aside>
    <section>Section 1</section>
    <section>Section 2</section>
    <section>Section 3</section>
    <section>Section 4</section>
  </div>
</body>

Listing 5-1Grid HTML

html, body {
  padding: 36px;
  margin: 0;
}

.grid-container {
  display: grid;
  grid-template-columns: 1fr 1fr 1fr;
  grid-template-rows: 5rem 5rem;
  grid-gap: 1rem;
}

aside {
  grid-row: 1/3;
  background: lightgray;
}

section {
  border: solid 1px gray;
}

Listing 5-2Grid Without Vendor Prefixes

img/487079_1_En_5_Fig4_HTML.jpg

图 5-4

Firefox 中的网格

当在 IE 中运行清单 5-2 中的相同代码时,不会呈现网格,元素只是简单地堆叠在一起(图 5-5 )。

img/487079_1_En_5_Fig5_HTML.jpg

图 5-5

IE 中没有供应商前缀的网格

这是因为显示值 grid 不存在。要在 Internet Explorer 中访问网格功能,需要使用供应商前缀。

html, body {
  padding: 36px;
  margin: 0;
}

.grid-container {
  margin: -.5rem;
  display: -ms-grid;
  -ms-grid-columns: 1fr 1fr 1fr;
  -ms-grid-rows: 5rem 5rem;
}

aside {
  background: lightgray;
  -ms-grid-row-span: 2;
  margin: .5rem;
}

section {
  border: solid 1px gray;
  margin: .5rem;
}
section:nth-of-type(1) {
  -ms-grid-column: 2;
  -ms-grid-row: 1;
}

section:nth-of-type(2) {
  -ms-grid-column: 3;
  -ms-grid-row: 1;
}
section:nth-of-type(3) {
  -ms-grid-column: 2;
  -ms-grid-row: 2;
}
section:nth-of-type(4) {
  -ms-grid-column: 3;
  -ms-grid-row: 2;
}

Listing 5-3Grid with Internet Explorer Vendor Prefixes

使用-ms供应商前缀并用网格间隙代替边距,可以实现相同的布局(图 5-6 )。

img/487079_1_En_5_Fig6_HTML.jpg

图 5-6

IE 中带有供应商前缀的网格

后退

当浏览器不支持某个属性时,解决供应商前缀的一个更好的方法是创建一个后备。当浏览器遇到它不支持的属性或值时,它将忽略该属性或值,因此,将保持以前设置的值。如果元素没有预先设置的值或者没有继承值,将使用默认值。

例如,(在撰写本文时)cross-fade 有一个实验版本,在 Safari 中位于供应商前缀(-webkit)之后,在 Firefox 中不受支持。要开始使用它,可以创建一个后备。清单 5-4 和 5-5 显示了交叉淡入淡出及其回退的使用(图 5-8 显示了期望的输出)。

html, body {
  box-sizing: border-box;
  padding: 36px;
  margin: 0;
}

.container {
  background-image: url(child.png);
  background-repeat: no-repeat;
  background-size: contain;
  background-position: bottom;
  background-image: -webkit-cross-fade(url(beach.png), url(child.png), 50%);
  background-image: cross-fade(url(beach.png) 50%, url(child.png) 50% );
  box-sizing: border-box;
  padding: 1rem;
  height: 30rem;
  max-width: 100%;
  width: 100%;
}

Listing 5-5Cross-Fade Fallback CSS

<body>
  <div class="container"></div>
</body>

Listing 5-4Cross-Fade Fallback HTML

首先,设置一个背景图像,然后使用供应商前缀通过交叉淡入淡出覆盖它,最后,通过标准交叉淡入淡出再次覆盖它。不支持交叉淡入淡出或供应商前缀版本的浏览器,如 Firefox(图 5-7 ),将只显示背景图像。

img/487079_1_En_5_Fig7_HTML.jpg

图 5-7

后退到背景图像

支持厂商前缀的浏览器,如 Safari(图 5-8 ),将显示实验版本。

img/487079_1_En_5_Fig8_HTML.jpg

图 5-8

使图像交替淡变

最后,支持最终版本的浏览器将显示规范定义的交叉渐变。

支持 At 规则

@supports at-rule 允许检查是否支持特定的属性和值对,从而允许相应地定制用户体验。除了 IE 之外,这个功能一般都得到很好的支持。当属性受支持时,表达式@supports(property:value {}返回 true,而当属性不受支持时,@supports not (property:value){}返回 true。只有当选择器返回 true 时,才会应用选择器中的样式。这些可以用andor操作符连接起来,以创建新的表达式。一般来说,最好使用@supports来逐步增强新特性,而回退可以用来提供与旧浏览器的向后兼容性。

为了看到@supports的实际效果,让我们看看背景滤镜,它在 Opera 中有效,但在 Firefox 中无效。清单 5-6 和 5-7 显示了使用@supports来创建使用支持的条件样式。

html, body {
  padding: 36px;
  margin: 0;
}

.container {
  background-image: url('art.png');
  padding: 1rem;
}
p {
  background-color: rgba(255, 255, 255, 0.6);
  backdrop-filter: blur(20px);
  margin: 5rem;
  padding: 1rem;
}

@supports not (backdrop-filter: blur(20px) ) {
  p {
    background-color:white;
  }
}

Listing 5-7Cross-Fade Fallback CSS

<body>
  <div class="container">
    <p>Lorem ipsum dolor sit amet, consectetur adipiscing</p>
  </div>
</body>

Listing 5-6Cross-Fade Fallback HTML

支持backdrop-filter时,段落背景模糊,不透明度为 60%(图 5-9 )。如果不是,段落背景被设置为完全不透明的白色,以增加从模糊中获得的易读性(图 5-10 )。

img/487079_1_En_5_Fig10_HTML.jpg

图 5-10

撤退

img/487079_1_En_5_Fig9_HTML.jpg

图 5-9

backdrop-filter

项目默认值

重置浏览器默认值有助于创建跨浏览器的一致性。创建应用程序默认值有助于创建应用程序的一致性。这在使用基于组件的架构时尤其重要。将主题从布局中分离出来也有助于一致性。可以在元素本身和基类上设置样式,以便在整个应用程序中重用。如果改变主题,这些改变只需要在一个地方更新。此外,当创建新视图时,唯一关心的是布局,因为主题已经被考虑了。清单 5-8 和 5-9 是一个示例摘录;图 5-11 显示了输出。

img/487079_1_En_5_Fig11_HTML.jpg

图 5-11

主题

body {
  --border: solid 1px rgba(0, 0, 0, .2);
  --dark: rgba(0, 0, 0, .87);
  --light: rgba(255, 255, 255, .87);
  --shadow: box-shadow: 5px 5px 5px var(--dark);
  color: rgba(0, 0, 0, .87);
  font-family: sans-serif;
}

h1 { font-family: cursive; }

a {
  font-size: .75rem;
  font-variant: small-caps;
  text-decoration: none;
}

button {
  background: none;
  border: var(--border);
  border-radius: 45px;
  box-shadow: var(--shadow);
  box-sizing: border-box;
  font-size: .75rem;
  font-variant: small-caps;
  padding: .5rem 1rem;
}

.actions {
  align-items: center;
  border-top: var(--border);
  display: flex;
  justify-content: flex-end;
  margin-top: 1rem;
}
.actions > * { margin-left: 1rem;}

.card {
  border: var(--border);
  border-radius: 3px;
  margin-bottom: 1rem;
}
.card > div { padding: 1rem; }
.card .header {
  background: rgba(0, 0, 0, .87);
  color: rgba(255, 255, 255, .87);
}

/*  Layout */
.container {
  column-width: 30rem;
}

Listing 5-9Default Styles CSS

<body class="view">
  <h1>Theme</h1>
  <div class="container">
    <div class="card">
      <div class="header">Header</div>
      <div class="body">
        <p>Lorem ipsum dolor sit amet, ... </p>
      </div>
      <div class="actions">
        <button>My Button</button>
        <a>My Link</a>
      </div>
    </div>
    <div class="card"> ... </div>
    <div class="card"> ... </div>
    </div>
</body>

Listing 5-8Default Styles HTML

通过设置变量、元素的默认样式,并创建默认的容器类,如清单 5-9 中的.card类,可以创建一个主题。可以作为主题的一部分包含的属性是围绕外观和感觉的东西,比如颜色、版式、边框、填充等等。从那里开始,创建视图变得更加容易,因为剩下的主要问题是布局。通过设置默认主题,即使在基于组件的架构中,外观和感觉,或者品牌,也可以保持一致。此外,更新主题可以像更改自定义属性值一样简单。

摘要

这一章讲述了浏览器的差异和在它们之间标准化 CSS 的技术。还讨论了处理不同浏览器中 CSS 支持差异的技术。最后,主题化被提出。下一章将着眼于使用过渡和动画来支持用户交互。

Footnotes 1

t .沃伦(2019 年 4 月 8 日)。检索于 2019 年 9 月 3 日,来自 www.theverge.com/2019/4/8/18300077/microsoft-edge-chromium-canary-development-release-download

  2

CSS 滚动捕捉模块级别 1 发布历史记录。检索 2019 年 9 月 1 日,来自 www.w3.org/standards/history/css-scroll-snap-1

  3

我能使用滚动快照吗?检索 2019 年 9 月 1 日,来自 https://caniuse.com/#search=scroll-snap

  4

供应商前缀。检索 2019 年 9 月 1 日,来自 https://developer.mozilla.org/en-US/docs/Glossary/Vendor_Prefix

  5

我能使用网格吗?检索 2019 年 9 月 1 日,来自 https://caniuse.com/#search=grid

 

六、交互和过渡

当我们想到 HTML 和 CSS 时,我们通常会想到“静态”当想到交互或动画时,JavaScript 更常出现在脑海中。然而,CSS 包括几个特性,这些特性允许作为用户交互的结果来操纵元素。在这一章中,我们将看看如何使用 CSS 来响应用户交互,以及如何使用动画和过渡来支持这些交互。

用户交互响应

在 CSS 中响应用户交互最常用的方式之一是使用伪元素:hover:focus和:active

当使用定点设备与元素交互时,伪元素:hover匹配。最常见的是,当用户将鼠标悬停在元素 1 上时,如链接或按钮。这可以用来给用户一个可视的指示,指示该元素可以被交互。

当一个元素获得焦点时,比如使用键盘或者被点击,就会触发:focus伪类。焦点是很重要的,因为它向用户提供了一个视觉指示器,指示他们当前正在交互或者将要交互的元素。当输入字段处于焦点时,改变输入字段的边框样式将告诉用户他们将要输入哪个字段,这对于用户定位他们当前在页面中的位置非常有帮助。并非所有元素都可以获得焦点。除了 video 元素之外,按钮、锚标记和表单项(如 input 和 select)是唯一可以获得焦点的元素,而无需向元素添加tabindex属性。

当一个元素被激活时,比如一个按钮被按下或者一个链接被点击,伪元素被触发。按钮样式的变化,例如在按下物理按钮时消除阴影,反映了按下物理按钮这一动作在现实世界中的预期。尽管用户可能无法清楚地说出原因,但像这样的小交互会让用户感觉交互更自然。

img/487079_1_En_6_Figa_HTML.gif Accessibility and Focus

当应用焦点时,大多数浏览器在元素周围会有默认行为。如果压缩默认行为,则需要重新应用一些焦点的视觉指示,以便用户可以在视觉上将处于焦点的元素与其他元素区分开来。 2 此外,焦点不应该改变上下文、功能、意义或可操作性。 3 4

通过交互,可以设置响应,比如改变元素的外观、大小甚至位置。如果动画提供了将要发生的变化的信息,那么向视觉变化添加过渡将有助于用户理解所应用的变化。例如,当展开一个可折叠部分时,动画显示可折叠部分的打开将帮助用户保持他们在页面中的位置,特别是因为下面的内容将被移动到不同的位置,可能在视口之外。

当响应 CSS 触发的事件(如悬停、聚焦或活动)时,在 CSS 中保留相关的转换比使用 JavaScript 更容易维护。这使得触发和反应保持在一起,并且它们的关联保持清晰和明显。这有助于在样式表中保留可视说明。

改变

创建过渡和动画时,虽然不是必需的,但经常使用 CSS transform 属性。Transform 允许使用 CSS 样式的元素在二维空间中进行转换。变换函数基于变换矩阵。matrix()函数是matrix3d()的简写,它采用六个参数 a、b、c、d、tx 和 ty,在图 6-1 中以粗体显示。

img/487079_1_En_6_Fig1_HTML.png

图 6-1

变换矩阵

参数 a、b、c 和 d 描述线性变换,tx 和 ty 描述要应用的平移。CSS 提供了基于前面矩阵的变换函数来操作元素,如平移、缩放、旋转、倾斜和透视。使用translate()函数来改变一个项目的位置,比如将某个东西滑动到视图中,通常会比操纵它的位置更有效。对于scale()来说,改变一个元素的高度或宽度也是一样的,比如展开或折叠一个菜单或手风琴。rotate()功能常用于微电影;继续 accordion 示例,它可用于旋转 accordion 标题中的箭头或插入符号,以区分相关面板是打开的还是关闭的。当面板被打开时,箭头可以同时旋转,以告知用户所述面板的状态。虽然看起来微不足道,但像这样的小细节,如果信息丰富,可以帮助用户定位和理解他们正在看什么和正在发生什么。关于转换功能的细节可在表 6-1 中找到。

表 6-1

转换函数

|

功能

|

描述

|

尺寸

| | --- | --- | --- | | matrix() | matrix3d()的简写。参见前面的描述。需要六个参数。 | 2D | | matrix3d() | 三维上的线性变换和平移。参见前面的矩阵描述。取 16 个值。 | 三维(three dimension 的缩写) | | translate(tx, ty) | 通过向量进行平移,其中 x 是第一个平移值,y 是第二个平移值。要单独操作 x 轴或 y 轴,可以使用 translateX(tx)和 translateY(ty)。 | 2D | | translate3d(tx, ty, tz) | 与 translate()相同,但在三维空间中。TranslateZ(tz)可用于平移 z 索引上的元素。此 tz 值不能是百分比,它必须是长度。 | 三维(three dimension 的缩写) | | scale(sx, sy) | 缩放向量,其中x缩放高度,y缩放宽度,初始值为 1。要单独缩放高度或宽度,可以使用 scaleX(sx)和 scaleY(sy)。 | 2D | | scale3d() | 与 scale()相同,但是是三维的。ScaleZ(tz)可用于平移 z 索引上的元素。 | 三维(three dimension 的缩写) | | )``) | 将元素从变换原点旋转所提供的角度。 | 2D | | rotate3d(x, y, z, a) | 围绕三维空间中的固定轴旋转元素,其中xyz描述旋转轴,a描述旋转角度。 | 三维(three dimension 的缩写) | | skew(``x,``y) | 根据 x 轴和 y 轴上提供的角度扭曲元素。要按轴倾斜元素,可以使用 skewX(≈x)和 skewY(≈y)。 | 2D | | perspective(z) | 为三维元素提供透视,其中 0 是默认值。当 z 增大时,元素变大,当 z 减小时,元素缩小。 | 三维(three dimension 的缩写) |

过渡

当元素的样式改变时,过渡允许从初始状态到新状态的转变在视觉上是平滑的。顾名思义,transition 属性控制值如何随时间从一种状态变化到另一种状态的可视化方面。

transition 属性是以下内容的简写属性:属性、持续时间、计时功能和延迟。其语法在清单 6-1 中描述,其属性在表 6-2 中定义。??

transition: property duration timing-function delay;

Listing 6-1Transition Property Shorthand Syntax

表 6-2

转换属性值

|

值名

|

行为

|

基础资料

| | --- | --- | --- | | transition``-property | 定义转换将影响的属性 | 全部 | | transition``-duration | 定义完成过渡需要多长时间 | 0s | | transition``-timing``-function | 定义在过渡期间如何应用值的加速度路缘 | 缓解 | | transition``-delay | 定义过渡开始前的延迟时间 | 0s |

清单 6-2 和 6-3 显示了悬停过渡。

html, body {
  padding: 36px;
  margin: 0;
}

a {
  align-items: center;
  background: gray;
  border: solid 1px white;
  color: white;
  display: flex;
  font-size: 36px;
  height: 100px;
  justify-content: center;
  text-decoration: none;
  transition: all 250ms ease-in-out;
}

a:hover {
  background: white;
  border-color: gray;
  color: gray;
  border-radius: 45px;
}

Listing 6-3CSS for Transition Example

<body>
  <a href="">
    <span>Transitions</span>
  </a>
</body>

Listing 6-2HTML for Transition Example

在前面的列表中,将鼠标悬停在链接上,会导致背景色、边框颜色、字体颜色和边框半径在 250 毫秒内逐渐变化(参见图 6-2 )。

img/487079_1_En_6_Fig2_HTML.png

图 6-2

一段时间内的动画代码输出

User Experience

当一个动作被执行时,通过增强元素之间的关系,过渡可以是一个很好的方式来引导用户通过应用程序。然而,为了实现这个目标,动画应该是信息性的聚焦性的表现性的6 对于较小、不太复杂的动画,动画应该持续 200 到 500 毫秒,或者在较小的屏幕上,在 200 到 300 毫秒的范围内。 7

关键帧动画

与用户触发事件时只能发生一次的过渡不同,动画可以无限期重复。当一个元素被添加到 DOM 时,比如一个元素从display:nonedisplay:block,它们也可以被应用。打开菜单时可能会出现这种情况。菜单项对用户来说是隐藏的,它们需要滑动到视图中,而不是突然显示出来。通过动画显示菜单元素,用户隐含地理解了菜单项的来源。动画还提供了对动画步骤的更多控制,允许比过渡更复杂。通过动画中的百分比,关键帧规则设置何时需要发生什么变化。清单 6-4 和 6-5 显示了使用关键帧的示例。

img/487079_1_En_6_Fig3_HTML.png

图 6-3

一段时间内的动画代码输出

html {
  padding: 0;
  margin: 0;
}

body {
  box-sizing: border-box;
  padding: 36px;
  margin: 0;
}

body > div {
  box-sizing: border-box;
  margin-bottom: 3rem;
}

@keyframes myAnimation {
  0% {
    background: gray;
    border-color: white;
    color: white;
    border-radius: 0px;
    transform: scale(0);
  }
  25% {
    transform: rotate(5deg) scale(.25);
  }
  50% {
    transform: rotate(-10deg) scale(.5);
  }
  75% {
    transform: rotate(35deg) scale(.75);
  }

  100% {
    background: white;
    border-color: gray;
    color: gray;
    border-radius: 45px;
    transform: rotate(0) scale(1);
  }
}

.animations {
  animation: myAnimation 500ms ease-in-out 1;
  background: white;
  border: solid 1px gray;
  border-radius: 45px;
  box-sizing: border-box;
  color: gray;
  font-size: 2rem;
  padding: 2rem;
  text-align: center;
  width: 100%;
}

Listing 6-5Keyframes CSS

<body>
  <div class="animations">Animations</div>
</body>

Listing 6-4Keyframes HTML

背景颜色、边框颜色、颜色、边框半径和比例仅在 0%和 100%时定义,因此是插值的。元素将以每个百分比旋转到指定的角度。即使 100%没有指定旋转角度,在动画结束时,元素会将其旋转设置为元素上设置的值,即 0。

为了触发关键帧,使用了 animation 属性(参见清单 6-6 )。动画属性最多可以取七个值:名称、持续时间、计时功能、延迟、迭代计数、方向和填充模式(详见表 6-3 )。

表 6-3

动画属性值

|

值名

|

行为

|

基础资料

| | --- | --- | --- | | animation-name | 定义动画将使用的关键帧 at-rule | 没有人 | | animation-duration | 定义动画需要多长时间才能完成 | 0s | | animation-timing``-function | 定义在动画过程中如何应用值的加速度路缘 | 缓解 | | animation-delay | 定义动画开始前的延迟时间 | 0s | | animation-iteration``-count | 定义动画播放的次数 | one | | animation-direction | 定义动画应该向前、向后播放,还是向前和向后切换 | 标准 | | animation-fill-mode | 定义动画完成前后如何将样式应用于目标 | 没有人 |

animation: name duration timing-function delay iteration-count direction
           fill-mode;

Listing 6-6Animation Property

另一个可以用于动画的属性是animation-play-state,它允许开发者暂停和开始动画。继续播放时,动画将从暂停的地方重新开始,而不是从序列的开头开始。animation-play-state的默认值是running。然而,它需要被单独定义为自己的属性,而不是清单 6-4 中描述的动画速记的一部分。给予用户暂停动画的能力,特别是如果动画对于理解应用程序的内容或状态是不必要的,可以从根本上提高应用程序的可用性。例如,当考虑自动前进的转盘时,添加暂停面板自动递增的能力将允许用户控制他们查看内容的速度。

当从 DOM 中移除一个对象时,也可以使用动画,例如当添加一个显示值 none 时,但是因为display:none属性将在动画结束之前应用和完成,所以这不能仅用 CSS 来完成。如果在关闭菜单时,display:none被添加到菜单项,不管元素上设置的任何动画或过渡,菜单将突然消失,因为在菜单项被隐藏之前动画没有时间运行。为了解决这个问题,JavaScript animationend事件与 CSS 一起使用来监听动画状态。animationend将在动画完成时触发,此时display:none可以添加到需要隐藏的元素中(参见清单 6-7 和 6-8 )。

@keyframes roll {
  0%   { transform: translateX(-75vw) rotate(-360deg);  }
  100% { transform: translate(0) rotate(0)}
}

@keyframes roll-reverse {

  0% { transform: translate(0) rotate(0)}
  100%   { transform: translateX(-75vw) rotate(-360deg);  }
}

.animation-container {
  background: linear-gradient(lightgrey, grey);
  border-radius: 50%;
  display: none;
  height: 100px;
  margin: 1rem auto;
  width: 100px;
}

.show {
  display: block;
  animation: roll 1s cubic-bezier(0.280, 0.840, 0.420, 1);
}

.close {
  display: block;
  animation: roll-reverse 1s cubic-bezier(0.280, 0.840, 0.420, 1);
}

Listing 6-8Animation End Event CSS

<body>
  <div class="show-hide">
    <button onclick="toggleAnimation()" id="button">
      Show
    </button>
    <div
      class="animation-container"
      id="animationContainer">
    </div>
  </div>

  <script>
    function showContainer() {
      animationContainer.classList.add('show');
    }

    function hideContainer() {
      animationContainer.addEventListener('animationend', cleanup);
      animationContainer.classList.replace('show', 'close');
    }

    function cleanup() {
      animationContainer.classList.remove('close');
      animationContainer.removeEventListener('animationend', cleanup);
    }
  </script>
</body>

Listing 6-7Animation End Event HTML and JavaScript

当元素被“关闭”或隐藏时,首先添加一个带有退出动画的类。一旦动画结束,animationend事件监听器被触发,只有这时 display 属性值才能被更改为 none。使用transitionend事件监听器可以实现相同的转换。添加和删除类,而不是在 JavaScript 中处理关闭动画,有助于在 CSS 样式表中保持与显示相关的逻辑,增加可维护性并保持关注点的分离。

计时功能

无论是创建过渡还是动画,要定义的一个公共值是计时函数。它决定了值在动画完成所需时间内的变化速度。计时有助于使动画感觉更自然,并更紧密地反映物理世界的相互作用。当制作一个弹跳球的动画时,人们会期望球落地后加速。如果动画是线性的,球总是以相同的速度移动,动画看起来会关闭。有两种特定类型的计时功能可用。

缓解功能

缓动函数基于以法国工程师皮埃尔·贝塞尔命名的贝塞尔曲线定义平滑过渡。曲线是参数化的, 8 和三次变量由四个点定义:P 0 ,P 1 ,P 2 ,P 3 。P 0 和 P 3 分别定义曲线的起点和终点。P 1 和 P 2 代表赋予曲线形状的控制点。每个点由(x,y)坐标定义。

CSS cubic-bezier在代表动画初始和最终状态的(0,0)和(1,1)固定点预定义 P 0 和 P 3 。剩下要定义的是 P 1 和 P 2 ,它们的 x 值需要保持在[0,1]范围内,而 y 值可能存在于边界框之外。

CSS 函数如下:cubic-bezier(x1, y1, x2, y2)

虽然时序可以自定义,但为了方便起见,CSS 包括命名的常见时序函数,包括线性、缓入、缓出和缓出(见表 6-4 )。

表 6-4

命名缓动功能 9

|

名字

|

公式

|

曲线

| | --- | --- | --- | | 线性 | cubic-bezier(0.0, 0.0, 1.0, 1.0) | img/487079_1_En_6_Figb_HTML.gif | | 缓和 | cubic-bezier(0.25, 0.1, 0.25, 1.0) | img/487079_1_En_6_Figc_HTML.gif | | 渐强 | cubic-bezier(0.42, 0.0, 1.0, 1.0) | img/487079_1_En_6_Figd_HTML.gif | | 缓进缓出 | cubic-bezier(0.42, 0.0, 0.58, 1.0) | img/487079_1_En_6_Fige_HTML.gif | | 缓出 | cubic-bezier(0.42, 0.0, 0.58, 1.0) | img/487079_1_En_6_Figf_HTML.gif |

要创建弹跳效果,一个或两个 y 值应设定在[0,1]范围之外。为此,需要编写一个自定义函数,如下面的函数:cubic-bezier(0, 0.71, 0.64, 1.23)

曲线绘制在图 6-4 中。

img/487079_1_En_6_Fig4_HTML.png

图 6-4

样本反弹曲线

步进函数

虽然目前没有很好地支持跨浏览器,但也可以使用步进函数来代替曲线,该函数将动画划分为相等的时间段。两个值用于定义动画的计时:步数(n)和步位置(见表 6-5 )。语法如下:

 animation-timing-function: steps(n, step-position);

表 6-5

命名步进功能 10

|

名字

|

功能

|

步伐

| | --- | --- | --- | | 步进启动 | steps(1, start) | img/487079_1_En_6_Figg_HTML.gif | | 步骤结束 | steps(1, end) | img/487079_1_En_6_Figh_HTML.gif | | 跨接启动 | steps(3, jump-start) | img/487079_1_En_6_Figi_HTML.gif | | 跳转结束 | steps(3, jump-end) | img/487079_1_En_6_Figj_HTML.gif | | 无跳转 | step(3, jump-none) | img/487079_1_En_6_Figk_HTML.gif | | 跳转-两者都有 | step(3, jump-both) | img/487079_1_En_6_Figl_HTML.gif |

应用时,代码和输出将如清单 6-9 和 6-10 以及图 6-5 所示。

img/487079_1_En_6_Fig5_HTML.png

图 6-5

跳跃启动输出

body {
  box-sizing: border-box;
  padding: 36px;
  margin: 0;
}

body > div {
  box-sizing: border-box;
  margin-bottom: 3rem;
}

@keyframes jumpStart {
 0% {
    width: 0;
    background-color: white;
    border: 1px solid gray;
 }
 100% {
    width: 90vw;
    background-color: gray;
    border: 1px solid gray;
 }
}
.jump-start {

  animation-name: jumpStart;
  animation-duration: 5s;
  animation-iteration-count: infinite;
  margin-bottom: 4px;
  animation-timing-function: steps(5, jump-start);
}

Listing 6-10jump-start Sample Code CSS

<body>
  <div class="jump-start">jump-start</div>
</body>

Listing 6-9jump-start Sample Code CSS

请注意,动画已经部分开始。因为使用了jump-start,宽度为 0 且颜色为白色的初始状态被跳过,动画从宽度为最终状态宽度 20%的容器开始。如果使用了jump-end,容器将从宽度 0 开始,但从未达到 100%的宽度。动画结束时,容器的宽度只有 80%。

img/487079_1_En_6_Figm_HTML.gif Accessibility and Timing

考虑计时时,确保内容在一秒钟内不闪烁超过三次是很重要的。这是为了防止由于使用者的光敏性而诱发癫痫发作。 11

性能考虑因素

当考虑动画对性能的影响时,并不是所有的动画都是一样的。导致布局改变或视图被重画的动画特别费力。 12 例如,高度、宽度或位置的变化会影响布局,并导致页面上的元素被重新定位。导致视图重画的属性包括颜色、背景位置和可见性。影响布局和绘画的动画会比不影响布局和绘画的动画性能差。

通常,为了获得最佳性能,使用 transform 属性是最好的方法,因为它可以依赖于 GPU。只要有可能,最好使用不透明度、平移、旋转和缩放来尝试和坚持动画。 十三

当出现性能问题时,使用will-change属性会很有诱惑力。Will-change提前通知浏览器将要被动画化的变化,允许浏览器针对这些变化进行优化;然而,如果被滥用,它可能弊大于利。正确使用will-change的一些指南包括以下内容:

  • 稀疏使用——应该只在实际需要的时候使用。浏览器已经试图优化一切。不必要的使用实际上会降低页面速度。

  • 仅在需要时打开–应在动画触发前打开,然后再次关闭以释放用于优化的浏览器资源。

  • 足够的时间——优化耗时;因此,will-change需要在动画设置开始之前,用足够的时间应用到元素上才能生效。 14

摘要

本章介绍了过渡、动画及其差异,以及用于更改动画和变换应用时间的函数。还包括处理动画时的性能和可访问性考虑。第七章将介绍预处理器及其架构考虑因素和优势。

Footnotes 1

:悬停。(2019 年 8 月 14 日)。检索自 https://developer.mozilla.org/en-US/docs/Web/CSS/:hover

  2

焦点可见。(2019 年 8 月 14 日)。检索自 www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-focus-visible.html

  3

理解成功标准 3.2.1:专注。(2019 年 8 月 14 日)。检索自 www.w3.org/WAI/WCAG21/Understanding/on-focus.html

  4

焦点顺序。(2019 年 8 月 14 日)。检索自 www.w3.org/TR/UNDERSTANDING-WCAG20/navigation-mechanisms-focus-order.html

  5

过渡。(2019 年 8 月 15 日)。检索自 https://developer.mozilla.org/en-US/docs/Web/CSS/transition

  6

理解运动。(2016 年 8 月 26 日)。https://material.io/design/motion/understanding-motion.html

  7

头,瓦尔。你的 UI 动画应该有多快?(2019 年 8 月 26 日)。检索自 https://valhead.com/2016/05/05/how-fast-should-your-ui-animations-be/

  8

贝塞尔曲线的定义及其性质。(2019 年 8 月 29 日)。检索自 http://web.mit.edu/hyperbook/Patrikalakis-Maekawa-Cho/node12.html

  9

。(2019 年 8 月 29 日)。检索自 https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function

  10

。(2019 年 8 月 29 日)。检索自 https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function

  11

网页内容可访问性指南(WCAG 2.1)。(2019 年 8 月 31 日)。www.w3.org/TR/WCAG21

  12

动画和表演。(2019 年 8 月 31 日)。检索自 https://developers.google.com/web/fundamentals/design-and-ux/animations/animations-and-performance

  13

高性能动画。(2019 年 8 月 31 日)。检索自 www.html5rocks.com/en/tutorials/speed/high-performance-animations/

  14

CSS 将改变模块级别 1。(2019 年 8 月 31 日)。检索自 www.w3.org/TR/css-will-change-1/

 

七、预处理器

对于 CSS,有几个预处理程序可用。它们将获取数据,以自己特定的语法编写,然后输出 CSS 供浏览器使用。这样做的好处包括可以使用 CSS 中还没有的功能,如颜色编辑功能或嵌套规则。在 CSS 变量被语言本身支持之前,他们也给了我们访问 CSS 变量的权限。一些最流行的处理器包括 Sass、Less 和 Stylus。

Note

本章中的例子将使用 SCSS1 这些技术可以使用其他的预处理器;但是,功能可用性和语法会因使用的预处理器而异。

对架构的启示

由于增加了功能,例如混合和扩展类的能力,使用预处理程序和使用纯 CSS 时,组织和构建代码的方式可能会非常不同。如今,用预处理器之外的方式计算值的能力带来了编写枯燥语义代码的能力。它可以只在一个地方定义,然后在整个样式表中重用,这与其他编程语言中使用的一些面向对象原则没有太大区别。

使用预处理程序的缺点是它们给应用程序增加了一层复杂性,而这在使用纯 CSS 时是不存在的。尽管一些预处理程序,如 Less、 2 可以直接在浏览器中运行,但不建议在生产中使用,因为它的性能和可靠性不如普通 CSS。因此,当使用预处理程序时,我们需要某种构建步骤来将代码编译成 CSS。

调试也可能是一项挑战,尤其是在使用一些更复杂或更高级的代码特性时。这是因为生成的 CSS 与编写的代码没有一对一的匹配。例如,被添加到一个类中的属性和特性可能来自一个 mixin(本章后面将详细介绍 mixin ),而不是规则集的一部分。被应用的 CSS 是输出,而不是 mixin 本身,所以追溯到哪个 mixin 创建了输出可能是困难的。Sourcemaps 可以在这方面提供帮助。sourcemap 是一个可以用 CSS 生成的文件,它将输出链接回生成它的代码。但是同样,这需要作为构建过程的一部分进行专门设置。

因此,首先,在选择要使用的处理器之前,应该问一下增加的复杂性是否必要。

嵌套

嵌套让我们有了清晰的视觉层次,这是 CSS 所没有的。下面的 CSS(清单 7-1 )可以嵌套(清单 7-2 ),让层次结构一目了然。

nav {
  padding: 0;
  margin: 0;
  ul {
    padding: 0;
    li {
      padding: 10px;
      border: solid 1px blue;
      background: yellow;
      color: blue;
    }
  }
}

Listing 7-2Nested SCSS

nav {
  padding: 0;
  margin: 0;
}
nav ul {
  padding: 0;
}
nav ul li {
  padding: 10px;
  border: solid 1px blue;
  background: yellow;
  color: yellow;
}

Listing 7-1CSS

虽然很容易知道列表项上设置的样式将只应用于导航列表项,但是嵌套使得创建过于具体的规则变得非常容易。在前面的例子中,在无序列表中嵌套列表项是多余的,不会增加任何值。仅将其嵌套在导航下,比其当前位置高一级,就足够了。

然而,嵌套可以使某些情况变得清晰。我们来看看清单 7-3 。

a:link, a:visited {
  color: gray;
  font-variant: small-caps;
  border: dotted 1px rgba(0, 0, 0, 0);
  text-decoration: none;
  &:hover { border: dotted 1px cornflowerblue; }
  &:focus { border: solid 1px cadetblue; }
  &:active { border: double 1px darkcyan; }
}

Listing 7-3Nested SCSS

“与”符号指的是父元素,所以当它是一个链接或一个已访问的链接时,鼠标悬停在锚标记上。这里的嵌套非常清楚地表明了悬停、焦点和活动选择器都是a:linka:visited的子元素。

如果没有嵌套,代码应该是这样的(清单 7-4 ):

a:link,
a:visited {
  color: gray;
  font-variant: small-caps;
  border: dotted 1px rgba(0, 0, 0, 0);
  text-decoration: none;
}
a:link:hover,
a:visited:hover {
  border: dotted 1px cornflowerblue;
}
a:link:focus,
a:visited:focus {
  border: solid 1px cadetblue;
}
a:link:active,
a:visited:active {
  border: double 1px darkcyan;
}

Listing 7-4Nonnested CSS

如果没有嵌套,很难一眼看出链接和访问过的链接也有悬停、焦点和活动状态的样式。此外,嵌套代码更简洁,不重复根元素,减少了打字错误或错误的机会。

嵌套时必须小心,以免创建过于具体的规则。当元素嵌套太深时会发生这种情况。但是,它有助于提高代码的可读性。

颜色函数和变量

变量,虽然今天在 CSS 中可用,如第二章所讨论的,但最初是通过使用预处理器来实现的。CSS 版本(自定义属性)虽然受预处理程序变量的影响,但确实比预处理程序变量有一些优势。自定义属性可以通过 JavaScript 访问和更改,而预处理器变量则不能。在创建 CSS 输出时,预处理器变量不再是变量;它们被它们的赋值所取代。然而,CSS 自定义属性是静态变量,可以在任何时候操作,包括在运行时。

虽然变量可以用于任何值,如默认填充量,但当与颜色函数结合使用来定义应用程序的主题时,它们是非常强大的。应用的品牌颜色包括表 7-1 中给出的颜色。

表 7-1

颜色值和用法

| ![img/487079_1_En_7_Figa_HTML.png](https://p6-xtjj-sign.byteimg.com/tos-cn-i-73owjymdk6/606bf56bb4a946bc95bfbc9c044e5161~tplv-73owjymdk6-jj-mark-v1:0:0:0:0:5o6Y6YeR5oqA5pyv56S-5Yy6IEAg5biD5a6i6aOe6b6Z:q75.awebp?rk3s=f64ab15b&x-expires=1771253023&x-signature=LxjnGRCIpOY83Hk5AEBaL92ByQc%3D) |

可以根据颜色的用途将颜色设置为语义名称,然后在需要改变颜色值或饱和度时使用颜色函数进行操作(清单 7-1 )。

颜色函数会因所用的预处理器而异;大多数都包括使颜色变亮、变暗或改变色调、饱和度或透明度的功能。在清单 7-5 中,我们使用scale-color()获取一种颜色,然后可以改变以下颜色属性的任意组合:红色、绿色、蓝色、饱和度、亮度和 alpha。当亮度设置为 10%时,我们使颜色比原始颜色浅 10%,并保持所有其他值不变。

$primary: #AEC5EB;
$accent: #E9AFA3;
$links: #AEC5EB;
$background: #F9DEC9;
$dark: #3A405A;
$light: #FAFAFA;

$border: solid 1px $light;
$dark-text: $dark;
$light-text: $light;

$spacing: 1.25rem;

body {
  background: scale-color($background, $lightness: 10%);
  color: $dark-text;
  padding: $spacing;
}
a:link, a:visited {
  color: $link;
}
a:hover, a:focus {
  color: scale-color($link, $lightness: -10%);
}
button {
  color: $light-text;
  background: $primary;
  border: $border;
  padding: $spacing;
}
section, article {
  background: scale-color($background, $lightness: 20%);
  padding: $spacing;
  margin-bottom: $spacing;
}

Listing 7-5Colors

通过使用颜色转换函数和变量,我们不仅不必记住每种颜色的确切值和我们可能使用的任何变化,而且我们还增加了保持主题一致的能力。此外,如果颜色要改变,这可以在一个地方完成。颜色变化的可能性就是为什么颜色名称应该基于它们的用法而不是它们的实际颜色。例如,如果变量名是$pink,并且强调色被改为紫色,我们现在必须到处查找变量名并更新它,或者我们将拥有一个不代表分配给它的颜色的变量名。这种情况使得可维护性变得非常困难,代码也很混乱。选择语义变量名对于代码的可维护性非常重要。

混入类

Mixins 允许开发人员创建可以在整个应用程序中轻松重用的属性和值的集合。

简单混合

清单 7-6 中的简单例子展示了如何使用一个简单的 mixin 在一个地方定义一组属性,然后将它们包含在另一个上下文中。@include属性用于将先前定义的 mixin 分配给新的上下文——在本例中是一个元素——但是它也可以很容易地包含在类定义中。

@mixin card {
  background: white;
  box-sizing: border-box;
  margin-bottom: 1rem;
  padding: 1rem;
  box-shadow: 1px 1px 3px silver
}

div {
  @include card;
}

Listing 7-6Simple Mixin

因素

mixin 也可以接受参数,以便根据传递的参数改变 mixin 的输出,如清单 7-7 中的$elevation参数所示。

@mixin card($elevation) {
  background: white;
  box-sizing: border-box;
  margin-bottom: 1rem;
  padding: 1rem;

  $offset: $elevation * 1;
  $blur: $elevation * 2;
  box-shadow: #{$offset}px #{$offset}px #{$blur}px silver;
}

div {
  @include card(3);
}

Listing 7-7Mixin with Arguments

争论

逻辑也可以添加到 mixin 中。在清单 7-8 中,基于非零值$elevation,样式被不同地应用。

@mixin card($elevation) {
  background: white;
  box-sizing: border-box;
  margin-bottom: 1rem;
  padding: 1rem;

  @if $elevation == 0 {
    border: solid 1px silver;
  } @else {
    $offset: $elevation * 1;
    $blur: $elevation * 2;
    box-shadow: #{$offset}px #{$offset}px #{$blur}px silver;
  }
}

body {
  padding: 2rem;
}
h1 {
  margin: 0;
}
header {
  @include card(0)
}
div {
  @include card(2);
}

Listing 7-8Mixin with Arguments and Logic

使用 mixins 的优点,尤其是对于参数,是它允许干代码。代码只需编写一次,在一个地方进行管理,但可以应用于多个类。前面的代码(清单 7-8 )将编译成清单 7-9 所示的内容,并显示图 7-1 。

img/487079_1_En_7_Fig1_HTML.jpg

图 7-1

混合输出

body {
  padding: 2rem;
}

h1 {
  margin: 0;
}

header {
  background: white;
  box-sizing: border-box;
  margin-bottom: 1rem;
  padding: 1rem;
  border: solid 1px silver;
}

div {
  background: white;
  box-sizing: border-box;
  margin-bottom: 1rem;
  padding: 1rem;
  box-shadow: 2px 2px 4px silver;
}

Listing 7-9CSS Output

Mixins 在防止重复代码或无意义类名的需要方面非常强大。如果没有 mixins,早期的代码可能需要无限数量的类,或者必须多次重写边框和阴影,然后在多个位置维护它。另一个很好的应用是当一个特定的参数可能应用于一个元素的许多方面,但是根据上下文而不同,而元素的其余部分需要保持一致,不管情况如何。给用户的信息框可能是一个例子,其中需要信息、成功、警告和错误。除了颜色之外,盒子需要看起来一样(清单 7-10 和 7-11 和图 7-2 )。

img/487079_1_En_7_Fig2_HTML.jpg

图 7-2

信息框

@mixin message($color) {
  background: lighten($color, 40%);
  border: solid 1px $color;
}

body {
  padding: 2rem;
}

.message {
  padding: 1rem;
}

.info {
  @include message(blue);
}

.success {
  @include message(green);
}

.warning {
  @include message(orange);
}

.error {
  @include message(red);
}

Listing 7-11Informational Boxes SCSS

<body>
  <p class="message info">Information</p>
  <p class="message success">Success</p>
  <p class="message warning">Warning</p>
  <p class="message error">Error</p>
</body>

Listing 7-10Informational Boxes HTML

尽管填充符可能已经包含在 mixin 中,但是它被分离到它自己的类中,因为当添加 mixin 时,它每次都进行计算并输出所有代码;因此,mixins 不是静态信息的好用例。静态样式只是以编程方式复制到每个类中,增加了 CSS 的大小,从而增加了上传时间。对于静态样式,类、元素的缺省值或者使用@extend at-rule 是更好的选择。

@扩展

与 mixins 不同,Extend 防止在生成的 CSS 中出现重复代码。当 mixin 为包含它的每个选择器复制声明块时,extend 创建一个声明块并合并选择器。

这种方法的优点是为基本样式创建基类,语义命名的类将指向这些基本样式。代码既不重复也不拷贝,防止在 HTML 元素上使用大量无意义的类。为了代码的可维护性,这也意味着元素的样式在 CSS 中得到控制。如果不再需要来自扩展另一个规则的样式,我们只需要移除@extend。通过简单地将类名添加到 HTML 中,而不是使用@extend,我们将不得不编辑 HTML 来改变外观。通过使用@extend,而不是将相同的类名添加到多个元素中,我们继续保持关注点的分离。我们的元素可以有与其用途相匹配的类名,而不是它们如何显示,我们通过 CSS 处理样式。

重温“Mixins”一节中的例子,而不是将消息和类型都添加到每个类中,我们可以创建一个类来确定类型,并由.message设置默认值(参见清单 7-12 和 7-13 以及图 7-3 )。

img/487079_1_En_7_Fig3_HTML.jpg

图 7-3

信息框–重新审视

@mixin message($color) {
  background: lighten($color, 40%);
  border: solid 1px $color;
}

body {
  padding: 2rem;
}

.message {
  padding: 1rem;
}

.info-message {
  @include message(blue);
  @extend .message;
}

.success-message {
  @include message(green);
  @extend .message
}

.warning-message {
  @include message(orange);
  @extend .message
}

.error-message {
  @include message(red);
  @extend .message
}

Listing 7-13Informational Boxes SCSS – Revisited

<body>
  <p class="info-message">Information</p>
  <p class="success-message">Success</p>
  <p class="warning-message">Warning</p>
  <p class="error-message">Error</p>
</body>

Listing 7-12Informational Boxes HTML – Revisited

请注意,两个示例具有相同的结果外观。但是后者只允许一个类而不是两个类来指定元素的整个类。对于代码的可维护性来说,@extend的强大之处在于能够在一个地方声明整个类,而无需在 Sass 和编译后的 CSS 中复制粘贴或复制代码(CSS 输出见清单 7-14 )。

body {
  padding: 2rem;
}

.message, .error-message, .warning-message, .success-message, .info-message {
  padding: 1rem;
}

.info-message {
  background: #ccccff;
  border: solid 1px blue;
}

.success-message {
  background: #4dff4d;
  border: solid 1px green;
}

.warning-message {
  background: #ffedcc;
  border: solid 1px orange;
}

.error-message {
  background: #ffcccc;
  border: solid 1px red;
}

Listing 7-14Informational Boxes – Revisited Output CSS

注意,message 类现在也有多个其他选择器,但是在输出中没有重复。

@导入

导入允许用户创建可以放置变量、混合和可重用代码的部分文件。Sass 导入的工作方式与 CSS 导入类似,它将包含的 SCSS 复制到导入它们的样式表中。因此,必须谨慎使用。例如,通过重复导入整个主题到每个组件中,很容易使代码膨胀。共享 mixins 和变量,因为它们不被复制,而是在一个样式中产生一个输出,这是使用@import的完美应用,因为不像类,它们不被复制。

在处理组件时,创建导入文件以使信息可以从应用程序中的任何地方访问变得非常有趣,因为通常情况下,例如在使用现成的 Angular 或在 JavaScript 和 Shadow DOM 中创建组件时,CSS 是有作用域的,因此与 CSS 的其余部分相比,它位于应用程序的一个单独的文件或区域中。向部分添加变量和混合——一个要导入到其他文件中的文件,它本身并没有用处——有助于保持代码干燥。

Note

分部有时在名称的开头用下划线来表示,以区别于样式表。

@import的另一个用例是防止应用程序的 CSS 样式表成为文件不可维护的巨石。通过将 CSS 分成更小的部分导入到主样式表中,代码可以更容易地被查找、协作和维护(参见清单 7-15 )。

@import "_variables"
@import "nav"
@import "carousel"
 .
 .
 .
a:link, a:visited { ... }
 .
 .
 .

Listing 7-15@import

摘要

在这一章中,我们研究了通过使用预处理器而使用的一小部分功能。我们研究了混合、导入、扩展、颜色函数和变量,以及它们如何影响我们组织和构建应用程序的 CSS。在下一章,我们将看看 JavaScript 如何与 CSS 交互,尤其是在现代框架的环境中。

Footnotes 1

https://sass-lang.com/documentation/syntax

  2

http://lesscss.org/usage/#using-less-in-the-browser

 

八、框架、库和 JavaScript

在现实世界的应用程序中,您的 CSS 并不是孤立运行的。本章涵盖了现代前端 web 应用程序的一些重要考虑因素,包括您对 CSS 或 JavaScript 框架的选择如何影响您的应用程序风格。

Java Script 语言

好的,是的,这是一本关于 CSS 的书,那么为什么我们突然开始谈论 JavaScript 了呢?事实是,大量的前端开发是使用某种框架和/或 UI 库完成的,其中许多依赖于 JavaScript。此外,由于状态变化或用户交互,JavaScript 经常被用来操纵 CSS。

根据 Stack Overflow 的 2019 年度调查,过去连续七年最受欢迎的编程语言是 JavaScript。前十名的细分见图 8-1 。 1

img/487079_1_En_8_Fig1_HTML.jpg

图 8-1

根据堆栈溢出,2019 年最受欢迎的编程、脚本和标记语言

最受欢迎的 web 框架仍然是 jQuery,其次是 React 和 Angular。十大细分如图 8-2 所示。 2

img/487079_1_En_8_Fig2_HTML.jpg

图 8-2

根据堆栈溢出,2019 年最受欢迎的 Web 框架

使用 JS 操作 CSS

通过 jQuery 等库和element.ClassList等普通 JavaScript 属性,我们已经通过 JS 操纵 CSS 很多年了。但是这对特异性和层叠到底有什么影响呢?表 8-1 列出了一些通过 JS 影响视觉输出的常见方式及其对特异性的影响。

表 8-1

CSS 改变 JavaScript 方法

|

属性和方法

|

它的作用

|

对特异性的影响

| | --- | --- | --- | | 元素。类别列表添加()移除()更换()切换()项目()包含() | 读取和操作附加到特定元素的类。 | 因为样式是通过引用类来应用的,所以级联和继承不会受到太大的影响。 | | element.style例子:elem.style =``"color: blue, font-size: 12px"elem.setAttribute("style", "color:blue; font-size: 12px"elem.style.color = "blue" | 在元素上内联添加样式。 | 很难覆盖,因为它们是内联的。如果元素已经有内联样式,它们将被 JavaScript 应用的样式覆盖。 |

这既是一个巨大的优势,也是一个巨大的风险,JavaScript 能够轻易地覆盖 CSS 声明的任何内容,但不是必须这样做。

如果我们不想在 JavaScripts 中混合 CSS 规则,那么通过 classList 给我们的方法允许在不接触 CSS 本身的情况下向元素添加和移除类。这带来了样式定义留在 CSS 文件中而不是在 JS 和 CSS 文件之间分割的巨大好处。

影响样式或 DOM 本身带来的好处是,所应用的 CSS 的特殊性无关紧要,因为它将被覆盖。通过这种技术,不管任何其他上下文,作为开发人员,我们可以规定,如果用户执行了某个动作,或者达到了某个特定的状态,将应用一组特定的样式,而不管其他样式或上下文。这种技术通常用于显示和隐藏对话框,例如通知消息。这种技术的优点是样式的改变可以和它的触发器或动作保持一致,并且不管上下文如何,被应用的样式不会被先前存在的 CSS 覆盖。

有些情况下,JavaScript 允许我们做纯 HTML 和 CSS 解决方案无法做到的事情。一个完美的例子是使用事件监听器来控制动画的定时、开始或结束(参见清单 8-1 to 8-3 和图 8-3 )。

img/487079_1_En_8_Fig3_HTML.png

图 8-3

动画监听器输出

var image, button;

(function() {
  'use strict';
  image = document.getElementById('image');
  image.addEventListener('animationend', reEnableButton);
  button = document.getElementById('button');
})();

function reEnableButton() {
  button.disabled = false;
  image.classList.remove('rotate');
}

function rotateImage() {
  image.classList.add('rotate');
  button.disabled = true;
}

Listing 8-3Animation Listener JavaScript

html, body {
  padding: 36px;
  margin: 0;
}

@keyframes rotate {
  0% { transform: rotate(0deg); }
  100% { transform: rotate(360deg); }
}
.rotate { animation: rotate ease-in-out 500ms 1; }

.image { text-align: center; }
img { max-width: 100%; }
button {
  display: block;
  margin: 1rem auto;
  padding: 1rem;
  width: 25%;
}

Listing 8-2Animation Listener CSS

<body>
  <div class="image">
    <img src="art.png" alt="modern art" id="image">
    <button onClick="rotateImage()" id="button">Rotate Image</button>
  </div>
</body>

Listing 8-1Animation Listener HTML

按钮触发 JavaScript 禁用按钮,并添加一个类rotate,使图像旋转一次。因为在页面加载时,我们设置 JavaScript 来监听动画的结束,一旦动画结束,我们可以重置页面。该按钮被重新启用,并且 rotate 类被删除。在这个例子中,即使我们用 JavaScript 操作 CSS,CSS 类仍然在 CSS 文件中定义和维护,因此,继承和级联没有被改变或影响。

基于组件的体系结构

当使用基于组件的架构时,不仅要根据应用程序的品牌/规范来设计应用程序的主题,而且 UI 库本身也非常重要。每个库在易用性方面都有不同程度的主题性和复杂性,以及实际上可能的样式。当选择一个 UI 库时,理解可定制性以及一个库或框架是如何可定制的,可以让你省去很多麻烦。封装——将组件 CSS 限制在组件本身——允许编写只适用于特定组件而不适用于应用程序其余部分的 CSS。如果一个 UI 库的组件有非常严格的封装和很少的主题选项,那么设计风格将会非常困难。

有许多不同的库和框架可以创建或使用组件。每一个都有稍微不同的实现。我们不会看所有的。当观察现代 JavaScript 框架如何创建组件并与组件交互时,大多数框架要么使用 web 组件,要么模仿它们。值得注意的是,特别是如果组件被模拟,它的行为将与我在下文中描述的略有不同。Angular 有一个模拟模型,并继续支持等效的阴影穿透组合器(::ng-deep),但可以设置为使用 web 组件,或者设置为根本没有封装。在 React 中,这取决于 CSS 在项目中是如何设置的。选项也涵盖了整个范围。准确理解框架提供了多少封装将有助于更好地决定如何构建 CSS。

库和框架

根据定义,库是应用程序将使用的声明的集合。框架是一种抽象,为应用程序提供基本的功能或框架。一个框架可以包含一个或多个库。

UI 库如jQuery UI3Angular Material4提供了一系列可以添加到应用程序中的组件或小部件。它们有现成的样式和功能。要定制他们的外观,他们需要有主题。主题化既可以通过工具来完成,这些工具可以提供必要的 CSS,比如 themerollers,也可以按照指南手动完成。无论哪种方式,所述库的主题性和可定制性将会变化。可变性是组件如何构造以及作者如何使元素易于定制的直接结果。超出主题允许范围的进一步定制通常会非常困难,导致使用非常特殊的选择器,比如使用!important。因此,在考虑库的时候,了解它的主题化功能以及定制它的容易程度是非常重要的,这样它的元素就可以匹配你的应用程序。

库本身的架构也可能影响它的使用方式,并且在某些情况下可能提供多种方法。Bootstrap 5 很有意思,因为它的结构允许两种完全不同的实现,各有利弊。

第一种,也可能是最常见的实现,是直接在页面中导入 CSS 和 JavaScript,从本地源,通过 CDN,或者使用包管理器,如 NPM、NuGet 或 RubyGems。该框架整体上是可用的,并且正在被应用。这意味着许多类已经添加了样式,可以在网站上使用了。一些组件,比如模态,具有依赖于与之相关的 JavaScript 的功能。这些也将随时可用。

这种方法的缺点在于三个方面:

  1. 命名不再是语义性的。

  2. 样式本质上是由 HTML 控制的。

  3. 所有内容都是完整导入的,即使没有被使用。

考虑一个包含三个页面的静态网站,所有三个页面使用相同的基本布局。布局包含一个主要部分和一个位于主要部分右侧的侧边部分(见清单 8-4 和 8-5 以及图 8-4 )。

img/487079_1_En_8_Fig4_HTML.jpg

图 8-4

自举输出

html, body {
  padding: 36px;
  margin: 0;
}

Listing 8-5Bootstrap CSS

<!DOCTYPE html>
<html lang="en">
<head>
  <title>Bootstrap</title>
  <meta charset="UTF-8">

  <!-- bootstrap -->
  <link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/css/bootstrap.min.css" integrity="sha384-ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T" crossorigin="anonymous">
</head>

<body class="container">
  <h1>Food</h1>

  <div class="row">
    <main class="col-md-9">
      <div class="jumbotron">
        <h2>Yum</h2>
        <p>Gummi bears chocolate bar powder brownie… </p>
        <button class="btn btn-warning">Call To Action</button>
      </div>
      <h2 id="cupcakes">Cupcakes</h2>
      <p>Chocolate chocolate bar tart cookie chocolate… </p>
      <a class="btn btn-light">Read More</a>
    </main>
    <aside id="bacon" class="col-md-3">
      <h2>Bacon</h2>
      <p>Bacon ipsum dolor amet pork loin chicken ham… </p>
      <p>Jowl spare ribs turkey cupim, pork chop sirloin… </p>
      <a class="btn btn-light">Read More</a>
    </aside>
  </div>

  <!-- Bootstrap scripts -->
  <script src="https://code.jquery.com/jquery-3.3.1.slim.min.js" integrity="sha384-q8i/X+965DzO0rT7abK41JStQIAqVgRVzpbzo5smXKp4YfRvH+8abtTE1Pi6jizo" crossorigin="anonymous"></script>
  <script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/1.14.7/umd/popper.min.js" integrity="sha384-UO2eT0CpHqdSJQ6hJty5KVphtPhzWj9WO1clHTMGa3JDZwrnQq4sF86dIHNDz0W1" crossorigin="anonymous"></script>
  <script src="https://stackpath.bootstrapcdn.com/bootstrap/4.3.1/js/bootstrap.min.js" integrity="sha384-JjSmVgyd0p3pXB1rRibZUAYoIIy6OrQ6VrjIEaFf/nJGzIxFDsf4x0xIM+B07jRM" crossorigin="anonymous"></script>

</body>
</html>

Listing 8-4Bootstrap HTML

这种方法的好处是简单。无论是使用自定义主题,还是使用前面例子中的默认主题,都可以快速启动并运行。许多基本样式已经存在,可以用来创建一个布局。最大的缺点是代码的可维护性。如果多个页面上有多个行动号召按钮,保持一致性就变得非常困难。

<button class="btn btn-warning">Call To Action</button>

如果更新了btnbtn-warning类,那么应用程序中包含这个通用类的所有按钮都将被更新,不管是不是动作调用。这个类没有给出它的用途,或者更糟,比如在这个例子中,它是因为它的颜色而被使用,而不是作为警告。唯一的另一个选择是找到应用程序中所有的动作调用,并更新它们的类名。

不再是样式表控制元素的外观,样式现在与 HTML 紧密绑定在一起。布局也是如此,想要将侧边改为占页面的三分之一,而不是四分之一,将涉及到进入每个页面,并更新 HTML。

另一种选择是利用 Bootstrap 提供的可用 Sass 混合(参见清单 8-6 和 8-7 )。

@import './node_modules/bootstrap/scss/functions';
@import './node_modules/bootstrap/scss/variables';
@import './node_modules/bootstrap/scss/mixins';

@import './node_modules/bootstrap/scss/jumbotron';
@import './node_modules/bootstrap/scss/buttons';

html {
  padding: 36px;
}

body {
  padding: 36px 15px;
  margin: 0 auto;
  font-family: -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,"Helvetica Neue",Arial,"Noto Sans",sans-serif,"Apple Color Emoji","Segoe UI Emoji","Segoe UI Symbol","Noto Color Emoji";
  font-weight: 400;
  line-height: 1.5;
  color: #212529;
  box-sizing: border-box;
}

h1, h2 {
  margin-top: 0;
  margin-bottom: .5rem;
  font-weight: 500;
  line-height: 1.2;
}
h1 { font-size: 2.5rem ;}
h2 { font-size: 2rem ;}

.container {
  box-sizing: border-box;
  @include make-row(15px);
  & > * {
    box-sizing: border-box;
    @include make-col-ready(1rem);
  }
}

button { @extend .btn }
.call-to-action {
  box-sizing: border-box;
  @extend .jumbotron;
  button {
    @include button-variant($yellow, $yellow);
  }
}
a.read-more {
  @extend .btn;
  @include button-variant($gray-100, $gray-100);
}

@media (min-width: 756px) {
  body {  max-width: 540px; }
}

@media (min-width: 768px) {
  body {  max-width: 720px; }
  main { @include make-col(9); }
  aside { @include make-col(3) }
}

@media (min-width: 992px) {
  body {  max-width: 960px; }
}

@media (min-width: 1200px) {
  body {  max-width: 1140px; }
}

Listing 8-7Bootstrap Mixins SCSS

<!DOCTYPE html>
<html lang="en">

<head>
  <title>Bootstrap</title>
  <meta charset="UTF-8">
  <!-- Application CSS -->
  <link rel="stylesheet" href="./styles.css">
</head>

<body>
  <h1>Food</h1>
  <div class="container">
    <main>
      <div class="call-to-action">
        <h2>Yum</h2>
        <p>Gummi bears chocolate bar powder brownie… </p>
        <button>Call To Action</button>
      </div>
      <h2>Cupcakes</h2>
      <p>Chocolate chocolate bar tart cookie chocolate… </p>
      <a class="read-more">Read More</a>
    </main>
    <aside>
      <h2>Bacon</h2>
      <p>Bacon ipsum dolor amet pork loin chicken ham pancetta… </p>
      <a class="read-more">Read More</a>
    </aside>
  </div>
</body>
</html>

Listing 8-6Bootstrap Mixins HTML

这种方法不容易启动和运行。因为它使用 SCSS,它将需要处理 SCSS 到 CSS 的能力。还需要了解 SCSS 和框架中 mixins 可用的内容。然而,一旦过了设置和学习曲线,我们会得到一些很大的好处。因为我们现在通过@include@extend将样式分配给类,而不是将通用类名应用于 HTML 中的元素,所以我们知道我们的元素在所有页面上看起来都是一样的。整个应用程序中的元素也可以从一个地方更新,而不是在站点中搜索特定概念的所有实例。最后,只有我正在使用的引导部分被导入,这减少了页面重量。

!Important

每当试图对一个组件库进行主题化或者调整 CSS 框架的样式时,特殊性有时是一个挑战,因为库或框架可能已经使用了非常具体的选择器;所以,用!important可能很有诱惑力。这里有龙!

虽然有些情况下真的没有其他选择,或者重要的是小恶,但这种情况很少。

使用!important增加了声明的优先级,使得覆盖或包含在普通级联中变得非常困难。您不再能够以更具体的选择器为目标来改变元素的样式。你现在需要另一个更具体重要的。这种恶性循环使得代码难以调试和维护,甚至更难扩展。

所以当覆盖样式时,如果你使用!important,要小心谨慎,要有意图而不是沮丧。

了解所选库的架构及其功能,将有助于做出明智的决定,即如何构建代码以获得更好的长期可维护性和性能。

Web 组件

在文档对象模型(DOM)中,自定义元素可以通过使用影子 DOM 附加到 DOM 来创建和封装。阴影 DOM 是可以附加到渲染文档的 DOM 元素的子树,由阴影主机、阴影根和阴影树组成(见图 8-5 )。

img/487079_1_En_8_Fig5_HTML.png

图 8-5

影子天赋

使用这种技术创建的组件是完全封装的,作者可以完全控制消费者能够设计的样式,因为从父页面或组件的角度来看,Shadow DOM 中的所有内容都类似于一个黑盒。有一段时间,我们可以通过使用>>>::deep这样的阴影穿透组合符来忽略封装,但这些组合符已经被弃用或从大多数浏览器中删除,以支持当前正在完善的即将到来的 CSS 阴影部分 6 规范。然而,即使在实现了这个新规范之后,组件的作者仍将控制用户能够摆弄的东西;特异性、!important和阴影穿透组合将继续无法编辑组件作者不允许更改的样式。

从架构上来说,web 组件的好处是可以将样式化的 web 组件放到任何 UI 中,而不用担心被父应用程序的 CSS 修改。见清单 8-8 至 8-11 和图 8-6 。

img/487079_1_En_8_Fig6_HTML.jpg

图 8-6

Web 组件输出

html, body {
  background: #fafafa;
  padding: 36px;
  margin: 0;
}

section.components {
  display: flex;
  margin: 0 -1rem;
}

my-card { margin: 1rem; }

.actions { text-align: center; }

button {
  background: rgb(187, 255, 120);
  border: none;
  border-radius: 1rem;
  box-shadow: 1px 1px 1px 1px gray;
  margin-top: 2rem;
  padding: .5rem 3rem;
}

.dark {
  font-family: monospace;
  font-size: 1.0625rem;
}

p, h2 { font-variant: small-caps; }

Listing 8-11Web Component Page CSS (styles.css)

:host {
  font-family: sans-serif;
  --primary: mediumvioletred;
  --background: white;
  --text: #242529;
  --buttonText: white;
}
:host(.dark) {
  --background: #242529;
  --text: white;
}

.card {
  background: var(--background);
  color: var(--text);
  box-shadow: 1px 1px 1px var(--primary), 0px 0px 2px lightgrey;
  padding: 1rem;
}

h2 { margin: 0 1rem 0 0; }

.actions {
  text-align: right;
  margin-top: 1rem;
}

button {
  background: var(--primary);
  color: var(--buttonText);
  font-family: sans-serif;
  padding: .5rem 1.5rem;
  border: none;
}

Listing 8-10Web Component CSS (card.css)

const actionButtonEvent = new CustomEvent('actions', {
  bubbles: true

,
  detail: { text: 'ok button' }
});

class MyCardComponent extends HTMLElement {
  constructor() {
    super();

    const shadow = this.attachShadow({ mode: 'open' });

    const card = document.createElement('div');
    card.setAttribute('class', 'card');

    const titleText = this.getAttribute('title');
    if (titleText) {
      const title = document.createElement('h2');
      title.innerText = titleText;
      card.appendChild(title);
    }

    const slot = document.createElement('slot');
    card.appendChild(slot);

    const actions = document.createElement('div');
    actions.setAttribute('class', 'actions');
    const button = document.createElement('button');
    button.innerText = 'OK';
    button.setAttribute('type', 'button');
    button.addEventListener('click', () => {
      this.dispatchEvent(actionButtonEvent);
    });
    actions.appendChild(button);
    card.appendChild(actions);

    const style = document.createElement('style');
    style.textContent = `@import './card.css'`;

    card.appendChild(style);
    shadow.appendChild(card);
  }
}

(function() {
  'use strict';
  customElements.define('my-card', MyCardComponent);
  document.querySelector('my-card').addEventListener('actions', e => console.log('outer actions event', e));
})();

Listing 8-9Web Component JavaScript (script.js)

<html lang="en">

<head>
  <title>Web Components</title>
  <link rel="stylesheet" href="./styles.css">
  <meta charset="utf-8">
</head>

<body>
  <h1>Components</h1>
  <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Morbi sed neque semper ante mattis tempor. Morbi volutpat ex ante, eget vulputate ligula pulvinar gravida.</p>
  <p></p>
  <section class="components">
    <my-card title="Coffee">
      <slot name="content">
        <p>Aged cortado, carajillo saucer wings aftertaste...</p>
      </slot>
    </my-card>

    <my-card title="Cats" class="dark">
      <slot name="content">
        <p>Chew master's slippers I is not fat, I is fluffy...</p>
      </slot>
    </my-card>
  </section>
  <div class="actions">
    <button type="button">More Components -></button>
  </div>
  <script src="./script.js"></script>
</body>

</html>

Listing 8-8Web Component HTML

请注意,即使样式直接应用于两个样式表中的按钮元素,按钮样式也不会相互交互。这是非常强大的,因为这意味着组件和应用程序可以独立构建,并且可以互相使用,而不用担心命名冲突。然而,有些东西是可以转移的。通过:host和较少支持的:host-context选择器,组件可以知道它的上下文。在这种情况下,组件专门查看它的主机是否有一个 dark 类。如果是的话,那么它的风格就不同了。然而,这些是作者预先计划好的风格。如果将另一个类名传递给这个特定的组件,它将继续显示其默认值,如图 8-7 左侧所示。

img/487079_1_En_8_Fig7_HTML.jpg

图 8-7

包括插槽的节点树

有些样式会渗入到组件中。槽中的元素或直接分配给宿主的类(自定义标记)受组件样式和页面样式的影响。注意图 8-6 中的段落标签;如下所述,它们从父组件和组件中获取样式。

左侧组件:

Browser default (serif) -> :host (sans-serif)

右侧组件:

Browser default (serif) -> :host (sans-serif) -> .dark (monospace)

:host比浏览器默认值更具体,反过来,.dark:host更具体。

那么为什么标题也变成了等宽,而按钮却不受影响呢?通过给.dark添加一个无衬线字体系列,我们基本上将 monospace 设置为:host上的默认字体系列,这是我们能插入组件的最大限度。按钮在组件 CSS 中指定了自己的字体,覆盖了默认的:host,因此不受影响。进一步尝试通过特定性来深入组件内部的元素,比如即使没有为该属性设置样式,也不能进行.dark button { ... }操作。

我们之所以能够设计段落标签的样式,并将继续能够对它们做我们想做的任何事情,是因为它们在一个槽中。插槽的内容实际上是由父插槽控制的。查看 DOM 树,可以看到槽内容位于影子树之外,如图 8-7 所示。

阴影树中的槽只不过是一个占位符。槽的内容在shadow-root的兄弟节点中,而不是在shadow-tree本身中。因此,它不像组件的其余部分那样封装,可以像页面上的任何其他元素一样设计样式。但是,因为它是主体的子元素,所以分配给影子主体的样式通常会级联到这些元素。

使用 Web 组件的样式化应用程序

当创建一个使用组件的应用程序时,很容易开始只考虑小的可重用项目,而忽略了全局。应用程序中字体、颜色、按钮样式等的一致性是一件好事,我想我们都同意这一点。然而,如果我们在每一个组件中重写这些风格,我们就为自己设置了差异和可维护性的噩梦。组件封装的紧密程度会影响方法。

然而,不管封装如何,从这样一个文件开始,其中一些可重用的值被设置为语义的、易于读取的变量被导入到所有位置,这在两个方面有所帮助:在任何地方都使用相同的文件,因此获得了一致性,并且因为它包含的那些值也被维护在一个位置,所以如果主要品牌颜色从蓝色切换到紫色,人们就不必在应用程序中寻找该颜色的每个实例。当使用诸如 Sass 或更低版本的预编译器时,这也是设置一些 mixins 的好地方。有关预编译器的更多信息,请参见第七章。清单 8-12 显示了这样一个文件的摘录。

/∗ brand colors ∗/
--primary: #8A4F7D;
--accent: #88A096;
--border-color: #DDDDDD;
--link-color: var(--accent);
--background: #FAFAFA;
--font-family: sans-serif;

--box-shadow: 1px 1px 1px var(--primary), 0px 0px 2px lightgrey;

/∗ breakpoints ∗/
--small: 500px;
--medium: 800px;
--large: 1200px;

...

Listing 8-12Sample Variable File

如果应用程序仍然有一个基本样式表,其样式应用于所有组件,这是一个放置默认值的好地方,例如链接在悬停和聚焦时应该是什么样子,标题应该是什么样子,以及应用程序的基本字体和颜色是什么。这是一个设置主题的好地方。(参见清单 8-13 )。

@import 'variable.css';

html, body {
  Background: var(--primary);
  padding: 0;
  margin: 0;
  font-family: var(--font-family);
  color: black
}

h1, h2, h3, h4, h5, h6 {
  color: var(--primary);
}

a:link,
a:visited {
  color: var(--accent);
}
a:hover,
a:focus {
  text-decoration: underline;
}
...

Listing 8-13Sample Theme File

一旦建立了这两个文件,组件应该只需要担心布局和异常,即特定于该组件的事情,而不需要担心其他事情。如果你发现自己一遍又一遍地复制和粘贴相同的东西,这将是对属于这些文件中的一个样式或一组样式的一个很好的衡量。如果是这种情况,是时候考虑这些样式是否需要导入或者在某个地方设置为默认值了。

如果组件被紧密封装,并且主题文件不可能在整个应用程序中级联样式,那么可以导入的变量就变得非常关键。将整个主题文件导入到每个组件中只会使应用程序膨胀,因为即使在一个地方维护,它本质上也会被复制到每个组件中。这里的可能性包括使用预处理程序来创建 mixins,或者将主题文件分解成更小的块、按钮、表格、链接等等,以便只导入需要的部分。

摘要

在这一章中,我们讨论了 CSS 和 JS 之间常见的交互机制,以展示 JS 是如何与我们的样式表交互(有时会干扰)的。我们还研究了我们使用的库的架构如何影响我们构建代码的方式。在下一章中,我们将深入各种架构最佳实践以及具体的 CSS 架构模式,展示它们的优缺点。

Footnotes 1

2019 年开发者调查结果。(2019).从可见焦点中检索。(2019 年 8 月 14 日)。检索于 2019 年 10 月 29 日,来自 https://insights.stackoverflow.com/survey/2019#technology

  2

2019 年开发者调查结果。(2019).从可见焦点中检索。(2019 年 8 月 14 日)。检索于 2019 年 10 月 29 日,来自 https://insights.stackoverflow.com/survey/2019#technology

  3

https://jqueryui.com/

  4

https://material.angular.io/

  5

https://getbootstrap.com/

  6

https://drafts.csswg.org/css-shadow-parts/