背景
- CSS 自诞生以来,基本语法和核心机制一直没有本质上的变化,它的发展几乎全是表现力层面上的提升。最开始 CSS 在网页中的作用只是辅助性的装饰,轻便易学是最大的需求;然而如今网站的复杂度已经不可同日而语,原生 CSS 逐渐让开发者力不从心
- 当一门语言的能力不足而用户的运行环境又不支持其它选择的时候,这门语言就会沦为 “编译目标” 语言,开发者将选择另一门更高级的语言来进行开发,然后编译到底层语言以便实际运行
- 于是在前端领域,CSS 预处理器应运而生,而 CSS 这门古老的语言以另一种方式 “重新适应” 了网页开发的需求
百花齐放
- CSS 预处理器是一个能让你通过预处理器自己独有的语法来生成 CSS 的程序
- 市面上有很多 CSS 预处理器可供选择,且绝大多数 CSS 预处理器会增加一些原生 CSS 不具备或不完善的高级特性,这些特性让 CSS 的结构更加具有可读性且易于维护,当前社区代表的 CSS 预处理器主要有一下几种:
- Sass:2007 年诞生,最早也是最成熟的 CSS 预处理器,拥有 Ruby 社区的支持和 Compass 这一最强大的 CSS 框架,目前受 LESS 影响,已经进化到了全面兼容 CSS 的 Scss
- Less:2009 年出现,受 Sass 的影响较大,但又使用 CSS 的语法,让大部分开发者和设计师更容易上手,在 Ruby 社区外支持者远超过 Sass,其缺点是比起 Sass 来可编程功能不够,不过优点是简单和兼容 CSS,反过来也影响了 Sass 演变到了 Scss 的时代,著名的 Twitter Bootstrap 就是采用 Less 做底层语言的
- Stylus:Stylus 是一个 CSS 的预处理框架,2010 年产生,来自 Node.js 社区,主要用来给 Node 项目进行 CSS 预处理支持,所以 Stylus 是一种新型语言,可以创建健壮的、动态的、富有表现力的 CSS。比较年轻,其本质上做的事情与 Sass/Less 等类似
预处理器赋予的 “超能力”
文件切分
- 页面越来越复杂,需要加载的 CSS 文件也越来越大,有必要把大文件切分开来,否则难以维护
- 传统的 CSS 文件切分方案基本上就是 CSS 原生的 @import 指令,或在 HTML 中加载多个 CSS 文件,这些方案通常不能满足性能要求
模块化
- 把文件切分的思路再向前推进一步,就是 “模块化”,一个大的 CSS 文件在合理切分之后,所产生的这些小文件的相互关系应该是一个树形结构
- 树形的根结节一般称作 “入口文件”,树形的其它节点一般称作 “模块文件”,入口文件通常会依赖多个模块文件,各个模块文件也可能会依赖其它更末端的模块,从而构成整个树形
- 模块化是一种非常好的代码组织方式,是开发者设计代码结构的重要手段,模块可以很清晰地实现代码的分层、复用和依赖管理,让 CSS 的开发过程也能享受到现代程序开发的便利
选择符嵌套
-
选择符嵌套是文件内部的代码组织方式,它可以让一系列相关的规则呈现出层级关系。在以前若要达到这个目的,写法如下,这种写法需要手工维护缩进关系,当上级选择符发生变化时所有相关的下级选择符都要修改;此外把每条规则写成一行也不易阅读,为单条声明写注释也很尴尬(只能插在声明之间了)
.nav {margin: auto /* 水平居中 */; width: 1000px; color: #333;} .nav li {float: left /* 水平排列 */; width: 100px;} .nav li a {display: block; text-decoration: none;} -
在 CSS 预处理语言中,嵌套语法可以很容易地表达出规则之间的层级关系,为单条声明写注释也很清晰易读
.nav {
margin: auto // 水平居中
width: 1000px
color: #333
li {
float: left // 水平排列
width: 100px
a {
display: block
text-decoration: none
}
}
}
变量
- 在变量出现之前,CSS 中的所有属性值都是 “幻数”,不知道这个值是怎么来的、它有什么意义等,有了变量之后就可以给这些 “幻数” 起个名字了,便于记忆、阅读和理解
- 当某个特定的值在多处用到时,变量就是一种简单而有效的抽象方式,可以把这种重复消灭掉,变量让开发者更容易实现网站视觉风格的统一,也让 “换肤” 这样的需求变得更加轻松易行
// 原生 CSS 代码
strong {
color: #ff4466;
font-weight: bold;
}
.notice {
color: #ff4466;
}
// 用 Stylus 来写
$color-primary = #ff4466
strong
color: $color-primary
font-weight: bold
.notice
color: $color-primary
运算
- 光有变量还是不够的,还需要有运算,若说变量让值有了意义,运算则可以让值和值建立关联
- 有些属性值跟其它属性值是紧密相关的,CSS 语法无法表达这层关系;而在预处理语言中可用变量和表达式来呈现这种关系
// 只能用注释来表达 max-height 的值是怎么来的,且注释中 3 这样的值也是幻数,还需要进一步解释
// 未来当行高或行数发生变化的时候,max-height 的值和注释中的算式也需要同步更新,维护起来很不方便
.wrapper {
overflow-y: hidden;
line-height: 1.5;
max-height: 4.5em; /* = 1.5 x 3 */
}
// 预处理语言来改良
// 在后期维护时,只要修改那两个变量就可以了
.wrapper
$max-lines = 3
$line-height = 1.5
overflow-y: hidden
line-height: $line-height
max-height: unit($line-height * $max-lines, 'em')
// 这种写法还带来另一个好处:$line-height 这个变量可以是 .wrapper 定义的局部变量也可以从更上层的作用域获取
// 这意味着 .wrapper 可向祖先继承行高,而不需要为这个 “只显示三行” 的需求把自己的行高写死
// 有了运算,就有能力表达属性与属性之间的关联,它令代码更加灵活、更加 DRY
$line-height = 1.5 // 全局统一行高
body
line-height: $line-height
.wrapper
$max-lines = 3
max-height: unit($line-height * $max-lines, 'em')
overflow-y: hidden
函数
- 把常用的运算操作抽象出来,就得到了函数
- 开发者可自定义函数,预处理器自己也内置了大量的函数,最常用的内置函数应该就是颜色的运算函数,有了它们,甚至都不需要打开 Photoshop 来调色,就可以得到某个颜色的同色系变种了
- 预处理器的函数往往还支持默认参数、具名实参、arguments 对象等高级功能,内部还可设置条件分支,可满足复杂逻辑需求
// 给一个按钮添加鼠标悬停效果
.button {
background-color: #ff4466;
}
.button:hover {
background-color: #f57900;
}
// 很难分清 #ff4466 和 #f57900 这两种颜色到底有什么关联
// 若代码是用预处理语言来写,那事情就直观多了
.button
$color = #ff9833
background-color: $color
&:hover
background-color: darken($color, 20%)
Mixins
Mixins 是 CSS 预处理器语言中最强大的特性,简单点来说 Mixins 可将一部分样式抽出,作为单独定义的模块,被很多选择器重复使用
Sass 的混合
- Sass 样式中声明 Mixins 时需要使用
@mixin,后面紧跟 Mixins 名,也可以定义参数同时可给这个参数设置一个默认值,但参数名是使用$符号开始且和参数值之间需要使用冒号:分开 - 在选择器调用定义好的 Mixins 需要使用
@include,然后在其后紧跟要调用的 Mixins 名,不过在 Sass 中还支持老的调用方法,就是使用加号+调用 Mixins,在+后紧跟 Mixins 名
// 声明一个 Mixins 叫作 error
@mixin error($borderWidth: 2px) {
border: $borderWidth solid #f00;
color: #f00;
}
// 调用 error mixins
.generic-error {
@include error(); /*直接调用error mixins*/
}
.login-error {
@include error(5px); /*调用error mixins,并将参数$borderWidth的值重定义为5px*/
}
Less 的混合
- 在 Less 中,混合是指将定义好的
ClassA中引入另一个已经定义的Class,就像在之前的 Class 中增加一个属性 - LESS 样式中声明 Mixins 和 Sass 声明方法不一样,它更像 CSS 定义样式,在 Less 可将 Mixins 看成是一个类选择器,当然 Mixins 也可以设置参数并给参数设置默认值,不过设置参数的变量名是使用
@开头,同样参数和默认参数值之间需要使用冒号:分隔开
// 声明一个 Mixin 叫作 error
.error(@borderWidth: 2px){
border: @borderWidth solid #f00;
color: #f00;
}
// 调用 error Mixins
.generic-error {
.error(); /*直接调用error mixins*/
}
.login-error {
.error(5px); /*调用error mixins,并将参数@borderWidth的值重定义为5px*/
}
Stylus 的混合
- Stylus 中的混合和前两款 CSS 预处理器语言的混合略有不同,它可不使用任何符号就直接声明 Mixins 名,然后在定义参数和默认值之间用等号
=来连接
// 声明一个 Mixin 叫作 error
error(borderWidth=2px){
border: borderWidth solid #f00;
color: #f00;
}
// 调用error Mixins
.generic-error {
error(); /*直接调用error mixins*/
}
.login-error {
error(5px); /*调用error mixins,并将参数$borderWidth的值重定义为5px*/
}
以上三个示例都将会转译成相同的 CSS 代码
.generic-error {
border: 2px solid #f00;
color:#f00;
}
.login-error {
border: 5px solid #f00;
color: #f00;
}
缺点
-
额外的编译配置:在写样式前需要做一些额外的编译配置工作,sass-node 安装以及编译的配置就能卡住一批前端新手
-
编译成本:每次修改代码都需要重新编译,占用时间和 CPU
-
学习成本:不同的 CSS 预处理器语法不同,增加学习成本。在同一个团队甚至项目里,可能同时使用了好几种样式预处理器
// Sass $color: #f00; $images: "../img"; @mixin clearfix { &:after { content: " "; display: block; clear: both; } } body { color: $color; background: url("#{images}/1.png"); @include clearfix; } // Less @color: #f00; @images: "../img"; .clearfix() { &:after { content: " "; display: block; clear: both; } } body { color: @color; background: url("@{images}/1.png"); .clearfix; } -
调试:在使用 CSS 预处理器时,通常会配置 SourceMap 来辅助调试,但即使这样,还是会碰到一些调试困难的情况
回归 CSS
各种 CSS 预处理器在更新迭代的过程中功能越来越繁杂花哨,但绝大部分人用到的核心功能还是那几样:Variables、Mixing、Nested、Module,顶多再加上一些工具类函数。既想要预处理器的优点,又不想要它带来的成本和缺点,有没有两全其美的办法?CSS 这么多年一直也在从社区汲取养分加速进化和迭代,能不能从 CSS 标准里面找到答案呢?
Variables in CSS
- CSS 自定义属性(CSS Custom Properties),又叫 CSS 变量(CSS Variable),允许在样式中声明变量并通过 var() 函数使用
- CSS Custom Properties for Cascading Variables 规范在 2012 年 10 月首次作为 工作草案(WD) 提出,并在 2015 年 10 月到达候选人推荐标准(CR)阶段,现在浏览器支持程度已经接近 93%
- CSS 变量定义及使用如下所示,可定义的类型极其丰富,不同于 SASS 预处理器变量的编译时处理,CSS 变量是浏览器在运行时进行处理的,因此 CSS 变量会更加强大和灵活
/* declaration */
--VAR_NAME: <declaration-value>;
/* usage */
var(--VAR_NAME)
/* root element selector (global scope), e.g. <html> */
:root {
/* CSS variables declarations */
--main-color: #ff00ff;
--main-bg: rgb(200, 255, 255);
--logo-border-color: rebeccapurple;
--header-height: 68px;
--content-padding: 10px 20px;
--base-line-height: 1.428571429;
--transition-duration: .35s;
--external-link: "external link";
--margin-top: calc(2vh + 20px);
}
body {
/* use the variable */
color: var(--main-color);
}
为什么变量的定义以
--开头?原因在这里:Let’s Talk about CSS Variables
Operators
- 可以使用 calc() 进行计算
:root {
--block-font-size: 1rem;
}
.block__highlight {
/* WORKS */
font-size: calc(var(--block-font-size)*1.5);
}
Generate Colors
可以用于通过 RGB 等函数生成和计算颜色:Generate Colors
CSS to JS
CSS 变量出现前,从 CSS 传值给 JS 非常困难,甚至需要借助一些 Hack 的手法。现使用 CSS 变量,可直接通过 JS 获取变量值并进行修改
.breakpoints-data {
--phone: 480px;
--tablet: 800px;
}
const breakpointsData = document.querySelector('.breakpoints-data');
// GET
const phone = getComputedStyle(breakpointsData).getPropertyValue('--phone');
// SET
breakpointsData.style.setProperty('--phone', 'custom');
Custom Theme
- 使用 CSS 变量,定制和动态切换网站主题非常简单方便
- 首先定义好不同主题下的变量,然后正常书写样式即可
- 通过 JS 改变元素属性,动态切换主题
html {
--hue: 210; /* Blue */
--text-color-normal: hsl(var(--hue), 77%, 17%);
...
}
html[data-theme='dark'] {
--text-color-normal: hsl(var(--hue), 10%, 62%);
...
}
// 通过 JS 改变元素属性,动态切换主题
document.documentElement.setAttribute('data-theme', 'dark')
document.documentElement.setAttribute('data-theme', 'light')
Mixins in CSS
- CSS 的有一个提案:CSS @apply Rule,按照该草案描述,用户可直接使用 CSS 变量存放声明块,然后通过 @apply rule 使用
- 可惜这个提案已被废弃,具体废弃原因感兴趣的可以看看这篇文章:Why I Abandoned @apply。尽管 Mixins 现在 CSS 还没有好的实现标准,但我们坚信迟早会有更优秀的规范涌现出来弥补 CSS 的这一块空白
:root {
--pink-schema: {
color: #6A8759;
background-color: #F64778;
}
}
body{
@apply --pink-schema;
}
Nesting in CSS
- CSS 里已经有 Nesting 的规范出现,尽管现在只处于 Editor’s Draft 阶段:CSS Nesting Module Level 3,可以看到按照 CSS Nesting Module ,Nesting 规范基本和预处理器一模一样
/* Dropdown menu on hover */
ul {
/* direct nesting (& MUST be the first part of selector)*/
& > li {
color: #000;
& > ul { display: none; }
&:hover {
color: #f00;
& > ul { display: block; }
}
}
}
Module in CSS
- 其实 CSS 很早就有了模块化方案,即 @import,使用 CSS 的 @import 规则可引用其他的文件样式,这特性从 IE 5.5 开始就被所有的浏览器支持,那为什么一直以来使用者寥寥无几呢,原因很多:
- 在一些老的浏览器有加载顺序的 bug
- 无法并行加载
- 导致过多的请求数量
- ...
- 现在前端项目基本都使用构建工具(Gulp、Webpack 等)打包后再上线,因此以上哪些缺点也就不存在了,而在 Webpack 的 css-loader 中,是可以配置是否开启 @import 的
Selector Helpers
- 除了上面介绍的一些主要特性,CSS 还提供了一些全新的特性来帮助更优雅的书写样式
:matches pseudo-class(已更名为 :is()) - :matches() CSS 伪类函数将选择器列表作为参数,并选择该列表中任意一个选择器可以选择的元素,这对于以更紧凑的形式编写大型选择器非常有用,而且浏览器支持程度也已经接近 93%
- 想要了解更多详情可以查看规范:Selectors Level 4
/* 语法 */
:matches( selector[, selector]* )
.nav:matches(.side,.top) .links:matches(:hover, :focus) {
color: #BADA55;
}
/* 相当于以下代码 */
.nav.side .links:hover,
.nav.top .links:hover,
.nav.side .links:focus,
.nav.top .links:focus {
color: #BADA55;
}
- @custom-selector
- 还可使用自定义选择器来定义可以匹配复杂选择器的别名
/* 语法 */ @custom-selector: <custom-selector> <selector-list>;- 定义的方式和 CSS 变量类似,使用起来稍微有点区别
@custom-selector:--text-inputs input[type="text"], input[type="password"]; :--text-inputs.disabled, :--text-inputs[disabled] { opacity: 0.5 } /* 相当于以下代码 */ input[type="text"].disabled, input[type="password"].disabled, input[type="text"][disabled], input[type="password"][disabled] { opacity: 0.5 }
用起来
- 尽管上述的 CSS 特性还处于不同阶段,浏览器的支持程度也不尽相同,但使用 postcss-preset-env,就可以抢先尝试 CSS 的最新特性
- postcss-preset-env 的配置也十分简单,以 Webpack 为例
rules: [
{
test: /\.css$/,
use: [
'style-loader',
{ loader: 'css-loader', options: { importLoaders: 1 } },
{ loader: 'postcss-loader', options: {
ident: 'postcss',
plugins: () => [
postcssPresetEnv(/* pluginOptions */)
]
}
]
}
]
总结
- 经过一番梳理,尽管 CSS 在社区的刺激下加快了更新迭代的速度,但到目前为止依然达不到 CSS 预处理器 VS CSS 的地步,只能说在使用 CSS 预处理器时,也可在项目中尝试一些优秀的 CSS 新特性,即:CSS 预处理器 + CSS
- 依然坚信,在 W3C 的推动下,随着 CSS 自身不断完善,CSS 预处理器终究会像当年的 CoffeScript 、Jade 一样变成时代的过渡产物,到那时也就不用纠结各种 CSS 预处理器的环境配置和技术选型等,直接打开编辑器,就能愉快的书写样式