前言
这篇文章你将学到以下内容:
- CSS 变量
- CSS 常用函数
- iPhone X 系列机型适配
- CSS At-rules
正文
一、简述
CSS 变量(CSS Variables),也称作 CSS 自定义属性(CSS Custom Properties),它是带有前缀 --
属性名,且带有值的自定义属性。然后通过 var
函数在全文范围复用。
1.1 语法
定义 CSS 变量的语法非常简单,在变量名称之前添加两个短横线 --
:
--<custom-property-name>: <declaration-value>
其中 表示变量名称,
表示变量值,形如:
--*
。这类自定义 CSS 属性与 color
、font-size
、background-image
等属性并没有什么不同,只是它没有默认含义罢了,它必须通过 var()
函数复用之后,才会产生意义。
其中「变量名称」命名约束是比较宽松的,可以是数字、字母、下划线 _
、短横线 -
的组合,但不能包含 $
、[
、^
、(
、%
等字符。比如:
--some-keyword: left; --some-color: #f00; --some-complex-value: 3px 6px rgb(20, 32, 54);
甚至可以是以数字开头、也可以是中文、韩文等。
:root { --红色: #f00; /* 有效 */ --1: 1px; /* 有效 */ } body { background-color: var(--红色); height: var(--1); }
当然,实际项目中,千万别以这种花里胡哨、奇奇怪怪的组合来命名变量名称,主要是避免被打。建议使用 kebab-case
方式进行命名,比如 --theme-primary
等。
请注意,CSS 变量名称是大小写敏感的,
--foo
和--Foo
是两个不同的变量。这一点与 CSS 属性大小写不敏感是有区别的。
1.2 作用域
同一个 CSS 变量,可以在多个选择器内声明,读取顺序与 CSS 匹配规则一致,优先级最高的生效。请注意,CSS 变量并没有 !important
用法,变量的覆盖规则由 CSS 选择器权重决定。
一般情况下,全局性变量放在 :root
内声明,也可以在任意元素中声明 CSS 变量,视实际情况而定即可。如果是小程序,则在全局样式 app.wxss
的 page
内声明。
:root
这个 CSS 伪类匹配文档树的根元素。对于 HTML 来说,:root
表示 元素,除了优先级更高之外,与html
选择器相同。
:root { --theme-primary: #f00; /* 全局可复用 */ } header { --theme-primary: #0f0; /* 仅 header 范围内可复用 */ } section { --theme-primary: #00f; /* 仅 section 范围内可复用 */ }
比如 内使用 color: var(--theme-primary)
,生效的将会是 color: #00f
。再者,以下示例中,在
中引用 --color
变量,最终生效的是 ID 选择器的变量值。
:root { --color: #f00; } div { --color: #0f0; } #id { --color: #00f; }
总的来讲,CSS 变量是有作用域概念的,它只能作用于自身或后代元素,而兄弟元素、祖先元素都是不用享用的。
可以试下这个示例:css-variable-scope-demo。
1.3 兼容性
兼容性如下,还是挺不错的(如果忽略 IE 的话),更多请看 Can I use。
很棒,IE 全系不支持,骂骂咧咧地说:还是用 SCSS 或 LESS 吧。可以参考下这个项目:css-vars-ponyfill。
对于不支持 CSS 变量的浏览器,可以采用如下方式兼容处理:
:root { --color-primary: #00f; } a { color: #00f; color: var(--color-primary); }
也可以使用 @supports
规则,然而它也不兼容 IE 浏览器。
@supports (--foo: 0) { /* supported */ } @supports (not (--foo: 0)) { /* unsupported */ }
二、JavaScript 操作
利用 CSS.supports()
方法即可判断当前浏览器是否支持 CSS 变量,如下:
const isSupported = window.CSS.supports('--foo', 0)
由于 CSS 变量就是自定义的 CSS 属性嘛,因此按照平常设置 CSS 属性的方式去操作即可,如下:
const element = document.querySelector('selectors') // 定义 CSS 变量 element.style.setProperty('--color', '#f00') // 读取 CSS 变量 element.style.getPropertyValue('--color', '#f00') // 删除 CSS 变量 element.style.removeProperty('--color')
另外有一个比较奇怪的用法(来自 EXAMPLE 7),如下:
:root { --foo: if(x > 5) this.width = 10; }
尽管这个属性值是「无用」的,不会使得任意 CSS 属性产生实际效果,但是这个 CSS 变量定义是「有效」的。它可以被 JavaScript 读取,至于有什么用,我也不知道。
二、CSS 函数
2.1 var 函数
var()
函数用于读取 CSS 变量,它可以替代元素中「任何属性」中的「值的任何部分」,不能作为属性名、选择器或其他处理属性值之外的值。
语法如下:
var(<custom-property-name>[, <declaration-value>])
表示自定义属性名
(可选)表示声明值(后备值),仅自定义属性没有定义时,它才会有效(类似 ES6 中的函数参数设定默认值)。
2.1.1 CSS 变量不合法的缺省特性
看看以下示例,变量 --color
的值为 20px
,显然它作为 background-color
值的话是无效的,那么 会显示什么背景颜色呢?红色?绿色?还是...
body { --color: 20px; background-color: #f00; background-color: var(--color, #0f0); /* 正确语法,与 background-color: 20px 有着本质上的区别 */ }
它最终生效的属性值为 transparent
,即 background-color
的默认值,因此相当于:
body { --color: 20px; background-color: #f00; background-color: transparent; }
可看 EXAMPLE 13。
但请注意,以下示例生效的是 background-color: #f00
,就怕有人看到上面示例之后,对原来的认知产生怀疑,特意说明下。
body { background-color: #f00; /* 有效 */ background-color: 20px; /* 语法错误,这条规则声明被丢弃,因此上一条规则生效 */ }
因此,当 CSS 变量值不合法时,生效的是 CSS 属性的“默认值”。
但注意,CSS 变量值不合法并不能使得
声明值生效,它仅限于 CSS 变量没有定义才会生效(类似函数参数的默认值仅实参为
undefined
才会生效,即便是null
等 falsy 实参也不会使其生效一样)。
为什么这里默认值要打双引号呢,原因是标准 EXAMPLE 13 部分明确说明了:
If the property was one that’s inherited by default, such as color, it would compute to the inherited value rather than the initial value.
也就是说,如果一个 CSS 属性是可继承的,那个当它应用了一个不合法的 CSS 变量值,最终生效的是其继承值,而不是默认值。比如:
<style> :root { --not-a-color: 20px; } body { color: #f00; } p { color: var(--not-a-color); } </style> <body> <p>字体会是什么颜色呢?</p> </body>
你看最终
生效的 color
是其从 中继承过来的 #f00
红色,而不是 color
的默认颜色 canvastext。
插个话题,我很好奇 canvastext
颜色是什么颜色,一般来说它会是黑色 rgb(0, 0, 0)
,然后我尝试将系统调至深色模式,然而它并不会默认变为白色,哈哈。然后我翻查了下标准,发现它跟 有关,它一般由浏览器来定义(如下),可看 6.2 System Color 章节。
因此,比较严谨的说法是:当 CSS 变量值不合法时,生效的是 CSS 属性的继承值或初始值。
2.1.2 var 函数的尾随空格
<style> body { --size: 20; font-size: var(--size)px; } </style> <body> <p>字号是多大呢?</p> </body>
猜一下 font-size
会是预期的 20px
吗?它不是,如下图:
请注意,浏览器最终解析出来的规则是:font-size: var(--size) px;
,它在 var(--size)
和 px
之间多了一个「空格」,因此这条规则是无效的(注意并不是引用 CSS 变量无效),所以字号是浏览器默认字体大小 16px
。
如果你使用诸如 VS Code 等编辑器,它一般会有 semi-colon expected
错误提醒的,如果保存自动格式化,它将会被保存为:font-size: var(--size) px;
。
这种情况可结合 calc()
函数处理,比如:
body { --size: 20; font-size: calc(var(--size) * 1px); /* 这样就能正常计算得出 20px 了 */ }
但个人更推荐这样用:对于一些长度、大小等 CSS 属性值,在定义 CSS 变量时,应带上单位:
body { --size: 20px; font-size: var(--size); }
请注意,如果变量值包含单位,就不能写成字符串形式。
body { --size: '20px'; font-size: var(--size); /* 注意,CSS 变量引用的语法是有效的,但经 CSS 解析器计算之后,其值并不符合 font-size 属性值的要求,因此被判定为语法错误,规则会被丢弃。 */ }
相当于 font-size: '20px';
语法错误,规则会被丢弃,因此取其继承值或默认值。
2.1.3 CSS 变量的相互传递性
我们在某个选择器中定义了一个 CSS 变量,它除了在子元素中被复用,它本身作用域内也可以复用,而且与编写顺序无关。比如:
body { --size: 20px; font-size: var(--size); }
body { font-size: var(--size); --size: 20px; }
以上两个示例,均是有效的。后者并不会因为 --size: 20px;
定义在后,就不会生效。这样规则,对于我们通过 JavaScript 动态设置 CSS 变量有着非常重要的意义。
2.2 calc 函数
calc()
语法非常地简单,如下:
property: calc(expression)
该函数接收一个表达式作为它的参数,表达式的返回值作为 calc()
函数的值。表达式可以是 +
、-
、*
、/
的组合,而且可以混用不同单位进行运算。
它同样支持 CSS 变量,例如:
.foo { --height: 30px; width: calc(100% - 30px); height: calc(100vh - var(--height)) }
注意点:
- 对于
+
和-
运算,运算符两边必须要有「空格」,而*
和/
运算则没有要求,因此建议都加上空白符。 - 对于
*
运算,参与运算的至少有一个数值(),且不能为0
。 - 对于
/
运算,运算符/
右侧必须是一个数值()。 calc()
函数支持嵌套写法,但其实被嵌套的calc()
函数只会被当做普通的括号,因此函数内直接使用括号就好了。
那么嵌套语法有什么用呢,比如:
.foo { --widthA: 100px; --widthB: calc(var(--widthA) / 2); --widthC: calc(var(--widthB) / 2); width: var(--widthC); }
那么,以上 --widthC
的值就会变成 calc(calc(100px / 2) / 2)
,即 25px
。
Web 前端总是绕不开兼容性,那么看下 calc()
函数的兼容性如何:
绿悠悠的一片,甚好!可以看到 IE9 以上都支持,可 IE 浏览器不支持嵌套写法,由于 IE 浏览器都不支持 CSS 变量,因此这个无伤大雅。
2.3 env 和 constant 函数
2017 年 Apple 公司发布了 iPhone X 和 iOS 11,开启了「刘海屏」和底部小横条之路。
于是就有了「安全区 Safe Area」之说(详见):
A safe area defines the area within a view that isn’t covered by a navigation bar, tab bar, toolbar, or other views a view controller might provide.
简单来讲,以 iPhone X 为例,其安全区是指不受刘海(Sensor Housing)、底部小横条(Home Indicator)、设备圆角(Corners)影响的区域,如下图的浅蓝色区域所示,其中粉色部分是指浏览器默认的 Margin 值,通常为了抹平各浏览器不同的外边距,都会设置 * { margin: 0 }
。
我们知道,viewport 是规则的矩形,如果显示设备的屏幕是不规则(比如圆形)的话,页面中的某些部分就会被裁剪。那么 viewport-fit
可以通过设置可视 viewport 大小来控制裁剪区域。
其中 viewport-fit
提供了 auto
(默认)、contain
、cover
三种属性值(详见):
<meta name="viewport" content="viewport-fit=auto|contain|cover" />
属性值 | 描述 |
auto |
默认值,表现与 contain 一致,viewport 会显示在「安全区」之内,相当于 viewport-fit: contain 。 |
contain |
将可视 viewport 设置为页面所有内容均可见的最大矩形。 |
cover |
将可视 viewport 大小设置为显示设备屏幕的外接矩形。 |
以圆形屏幕为例:
Apple 公司为了适配旗下的全面屏设备,(iOS 11 起)WebKit 内核的浏览器中定义了 safe-area-inset-*
四个环境变量。
safe-area-inset-top
safe-area-inset-right
safe-area-inset-bottom
safe-area-inset-left
需要注意的是,在竖屏和横屏状态下,
safe-area-inset-*
值是不同的。比如,竖屏状态下环境变量safe-area-inset-left
和safe-area-inset-right
的值为0
,横屏状态下环境变量safe-area-inset-top
的值为0
。
上图源自 Deng's Blog。
通过 env()
或 constant()
函数就能引用以上几个环境变量,对于不支持 env()
或 constant()
的浏览器,包含它的样式规则将被忽略。
自 Safari Technology Preview 41 和 iOS 11.2 beta 起,
constant()
函数已被移除,并用env()
函数替换(详见)。可为了兼容性,一般两个都会写。另外,若要环境变量
safe-area-inset-*
生效,需将页面设置为viewport-fit: cover
。
接下来,会介绍如何适配 iPhone X 系列刘海屏手机,有以下示例:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no" /> <title>Document</title> <style> * { margin: 0; padding: 0; } body { width: 100%; background-color: darkorange; } .container { height: 100%; min-height: 200vh; background-color: forestgreen; } </style> </head> <body> <div class="container"></div> </body> </html>
当我们不做任何处理,以上示例在 iPhone X 系列手机横屏状态下,左右边框会空出一部分,究其原因就是 Safari 浏览器会将网页内容置于「安全区之内」,相当于 viewport-fit: contain
。
当我们将 标签内的
viewport-fit
改为 cover
之后,并在页面中添加一首诗。
这样,Viewport 就占满了显示设备最大的矩形,但因为设备的刘海、圆角等因素,会导致页面中的部分内容无法完全显示。
然后,我们试着在 container
内添加左右外边距,其值分别取 safe-area-inset-left
、safe-area-inset-right
环境变量。
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no, viewport-fit=cover" /> <title>Document</title> <style> * { margin: 0; padding: 0; } body { width: 100%; background-color: darkorange; } .container { height: 100%; min-height: 200vh; background-color: forestgreen; /* 新增 */ margin-left: env(safe-area-inset-left); margin-right: env(safe-area-inset-left); } </style> </head> <body> <div class="container"> <article> <h1>蒹葭</h1> <p>蒹葭苍苍,白露为霜。所谓伊人,在水一方。溯洄从之,道阻且长。溯游从之,宛在水中央。</p> <p>蒹葭萋萋,白露未晞。所谓伊人,在水之湄。溯洄从之,道阻且跻。溯游从之,宛在水中坻。</p> <p>蒹葭采采,白露未已。所谓伊人,在水之涘。溯洄从之,道阻且右。溯游从之,宛在水中沚。</p> <p></p> </article> </div> </body> </html>
横屏、竖屏显示如下:
考虑 env()
和 constant()
函数兼容性的写法如下:
@supports (bottom: constant(safe-area-inset-bottom)) or (bottom: env(safe-area-inset-bottom)) { .container { /* 请注意,constant 和 env 先后顺序如下 */ margin-left: constant(safe-area-inset-left); /* 兼容 iOS < 11.2 */ margin-left: env(safe-area-inset-left); /* 兼容 iOS >= 11.2 */ margin-right: constant(safe-area-inset-right); margin-right: env(safe-area-inset-right); } }
2.4 max 和 min 函数
从文章排版来看,这是极不美观的,我们希望在左右两边再加点内边距。PS:上述图片为了更方便对比,采用了外边距(Margin),接下来会将其修改为内边距(Padding)。
.container { /* 这里将原先的 margin 修改为 padding */ padding-left: env(safe-area-inset-left); padding-right: env(safe-area-inset-left); }
但有个小小的需求,我们只希望竖屏状态下,添加
10px
的左右内边距,而横屏状态下取safe-area-inset-*
的值就好。
它们提供了两个 CSS 函数:min()
和 max()
,利用它们就能实现这需求。
.container { /* 这里使用到 max() 函数,表示取二者最大值。 */ padding-left: max(10px, env(safe-area-inset-left)); padding-right: max(10px, env(safe-area-inset-left)); }
效果如下:
如果考虑兼容性的话,可以使用 @supports
语法来处理:
@supports (padding: max(0px)) { .container { padding-left: max(10px, env(safe-area-inset-left)); padding-right: max(10px, env(safe-area-inset-left)); } }
对于 max()
和 min()
的语法和使用非常地简单,分别表示取最大值和最小值。
两者语法是一致的,以 max
为例:
property: max(expression [, expression])
它接受一个或多个值,若有多个值则采用逗号 ,
分隔,选择最大的值作为 CSS 属性的值。每个值除了可以是直接数值,还可以是数学运算(如 calc()
)、其他表达式(如 attr()
)。
还支持嵌套 max()
和 min()
函数,需要时可以使用小括号 ()
来设定运算顺序。
兼容性如下,一如既往 IE 全系不支持:
至此,相信你对 iPhone X 等机型的适配有更深刻的了解,适配起来就完全没有压力了。
三、CSS At-rules
一个 at-rule 是一个 CSS 语句,它以 @
符号开头,后接一个标识符,并包括直到下一个分号 ;
的所有内容或下一个 CSS 块,以先到者为准。
主要可分为不可嵌套、可嵌套两类:
不可嵌套 at-rule:
@charset
:指定样式表中使用的字符编码。@import
:导入其他外部样式表。@namespace
:指示 CSS 引擎必须考虑XML命名空间。
可嵌套 at-rule:
@media
:用于基于一个或多个媒体查询的结果来应用样式表中的一部分。@font-face
:指定一个用于显示文本的自定义字体。@keyframs
:通过在动画序列中定义关键帧的样式来控制 CSS 动画序列中的中间步骤。@supports
:指定依赖于浏览器中的一个或多个特定的 CSS 功能的支持声明。@document
:根据文档的 URL 限制其中包含的样式规则的作用范围(实验特性)。@page
:用于在打印文档时修改某些 CSS 属性。
每个 at-rule 规则都有不同的语法,有一部分 at-rule 可以归为一类:条件规则组。
这些规则组所指的条件总等效于
true
或false
,如果为true
那么它里面的 CSS 语句生效。
本文仅介绍 @supports
和 @media
,其他规则请看 CSS At-rules。
3.1 @supports
@supports
常用于 CSS 兼容性判断。
它由一组支持条件和一组样式声明组成。支持条件可以是一个或多个条件使用逻辑与 and
、逻辑或 or
、逻辑非 not
组合而成。
- 单一条件:由一个 CSS 属性和属性值组成,中间用分号
;
隔开。
@supports (transform-origin: 5% 5%) { /* 样式声明 */ }
当 transform-origin
的实现语法认为 5% 5%
是有效的值,表达式会返回 true
,此时规则内声明的样式就会生效。
- 多个条件:使用
not
、and
、or
操作符组合。
相当于 JavaScript 中的
!
、&&
、||
操作符啦,需设定运算顺序,则使用括号包裹。
/* 当 transform-origin: 10em 10em 10em 无效时,表达式返回 true */ @supports not (transform-origin: 10em 10em 10em) { /* 样式声明 */ }
/* 当所有条件同时为真时,表达式才返回 true */ @supports (display: table-cell) and (display: list-item) { /* 样式声明 */ }
/* 当条件至少有一个为真时,表达式才返回 true */ @supports (transform-style: preserve) or (-moz-transform-style: preserve) { /* 样式声明 */ }
还有一个实验性的语法:selector()
,有兴趣请看这里。
兼容性仍然是 IE 全系不支持,呵呵~
3.2 @media
未完待续...