CSS的工程化 + CSS 方法论
CSS 是 Web 开发中不可或缺的一部分,在前端工程化不断进步的今天,一方面, CSS 特性随着规范的升级越来越丰富,另一方面,前端业务复杂性性的增加带来的工程愈加庞大,驱使着开发者不断寻找 CSS 工程化的最佳实践。因此,CSS 工程化就顺应而生,主要是为了解决在使用标准 CSS 时遇到的各种难点。
在使用CSS时遇到的难点
- class命名难,经常把各种名字都想了个遍,为了不重复也是很拼了。
- 选择器的全局污染问题,经常改了一个地方的样式,影响到很多地方。
- 样式难以复用,没有办法像 JS 的函数一样,写一次,然后多个地方可以调用,经常有这种我不是代码的生产者,我只是代码的搬运工的感觉。
- 难以维护,在接手一个旧项目的时候,看到代码中一堆的
!important
或者各种语义不详的类名,这时是不是很想把上一个写这段代码的人拉出来批斗一番呢。
为了让我们在开发的过程中能保持愉悦的心情,当然更重要的是提高开发效率,拒绝无止尽的 Ctrl + C
、 Ctrl + V
,也为了同事之间的关系能更加融洽,所以 CSS 的工程化迫在眉睫。
下面将介绍在 CSS 工程化的道路上都提出了哪些解决方案。
预处理器
前面也提到了 CSS 难以复用的问题,另外如果你想给下面这段 HTML 添加样式。
<div class="box-container">
<div class="boxs">
<div class="box-item">
<span class="item"></span>
</div>
</div>
</div>
你有两个选择:
第一:一层套一层,像俄罗斯套娃一样,这样写有个好处就是可以一定程度上解决样式全局污染的问题,你只需要修改一下 box-container
就可以开启新一波的套娃旅程了。但是这样写出来的代码不仅可读性差,而且也很冗余。
.box-container {
color: red;
}
.box-container .boxs {
color: orange;
}
.box-container .boxs .box-item {
color: yellow;
}
.box .boxs .box-item .item {
color: green;
}
第二:像下面代码所示。优点很明显,摆脱了冗余的代码,代码看起来也清晰很多。一开始这样写确实很爽,但是一旦项目大了起来,你就会开始会为起名字而感到烦恼,一不小心起了个重复的名字,这时灾难便开始了,各种样式开始互相覆盖,不得不起更多的名字来覆盖前面的样式。
.box-container {
color: red;
}
.boxs {
color: orange;
}
.box-item {
color: yellow;
}
.item {
color: green;
}
很明显,上面的两种方案都不能给我们一个良好的编码体验,为了解决 CSS 的这个问题以及编码不灵活等问题,于是出现了 CSS 预处理器,目前比较流行的 CSS 预处理器有 Sass、Less 和 Stylus。
那么什么是 CSS 预处理器呢?
定义:CSS 预处理器定义了一种新的语言,其基本思想是,用一种专门的编程语言,为 CSS 增加了一些编程的特性,将 CSS 作为目标生成文件,然后开发者就只要使用这种语言进行编码工作。
简单的说:开发者可以用特定的编程语言编写代码,然后由预处理器帮你将写好的代码编译成 CSS 。
预处理器增强了 CSS 的语法。让标准 CSS 具备了以下的这些能力。
- 变量
- 混合(Mixin)Extend
- 嵌套规则
- 运算
- 函数
- Namespaces & Accessors(命名空间和访问器)
- scope
- 注释
只看这些乏味的说明,是体会不到预处理器可以给我们带来怎样的便利的。下面介绍最常见的三种预处理器,然后你可以从中选择适合你的预处理器来实践一下。相信一旦用习惯了你就会爱上它~
Sass
2007年诞生。最早也是最成熟的 CSS 预处理器。基于 Ruby,和 CSS 编写规范有一定的出入,有学习成本,以 .sass
后缀为扩展名。SCSS 是 Sass 3 引入新的语法,其语法完全兼容 CSS3,并且继承了 Sass 的强大功能,因此也更加容易上手,以 .scss
后缀为扩展名。其语法支持变量、选择器嵌套、继承(extend)、混合(mixin)和一些逻辑语句,同时还支持跨文件的导入功能,因而使得开发者能够很好地使用编程思想书写样式。
关于 **dart-sass **与 **node-sass **的选择?
node-sass 是用 node(调用 cpp 编写的 libsass)来编译 sass,网络原因容易安装失败。
dart-sass 是用 dart VM 来编译 sass,同时也是官方主推。
由于考虑到篇幅过长,不详细介绍如何使用 sass 以及下面提到的预处理器,有兴趣的可以自行学习哈。
使用 Dart Sass 代替 Node Sass
Ruby Sass
Less
2009年诞生。受 Sass 的影响较大,但又使用 CSS 的语法,让大部分开发者和设计师更容易上手。 Less 相对于 Sass 的优点在于十分的轻量,也完全兼容 CSS, 但另一方面可编程能力不如Sass,Bootstrap4 的 CSS 预处理器也从 Less 换成 Sass。
Less介绍及其与Sass的差异
Stylus
2010年诞生。来源于 Node 社区。主要用来给 Node 项目进行 CSS 预处理支持。
stylus中文版参考文档之综述
后处理器
既然预处理器这么优秀,为什么还需要后处理器?
当我们要使用预处理器的时候,还需要专门去学习一下预处理器的语法,而后处理器是面向标准 CSS 的,把兼容性、优化交给后处理器自动完成,学习成本更低。
后处理器是对原生 CSS 进行处理并最终生成 CSS 的预处理器,广义上还是个预处理器,与上面提到的预处理器不同的是,它处理的对象是标准 CSS。
实现原理:
- 将标准 CSS 解析,获得AST。
- 对 AST 进行后处理。
- 将 AST 转换为 CSS 代码。
优点:使用 CSS 语法,容易进行模块化,贴近 CSS 的未来标准。
缺点:逻辑处理能力有限。
比较典型的后处理工具有以下几种。
- clean-css:压缩CSS
- AutoPrefixer:自动添加 CSS3 属性各浏览器的前缀
- Rework:它在 2012年9月 发布第一个版本,由 Stylus 的作者 TJ Holowaychuk 开启。是一个高效、简单、易扩展并且模块化的 CSS 预处理器。
PostCSS
PostCSS 的第一个版本发布于 2013年11月 ,是从 AutoPrefixer 项目中抽象出来的框架。
它本身并不对 CSS 做具体的业务操作,只是将 CSS 解析成抽象语法树(AST),样式的操作由之后运行的插件系统完成。所以说,PostCSS 既不属于预处理器也不属于后处理器。它需要一个插件系统才能发挥作用。我们可以通过“插件”来传递 AST ,然后再把AST转换成一个串,最后再输出到目标文件中去。
PostCSS的优点
- 多样化的功能插件,创建了一个生态的插件系统;
- 根据你需要的特性进行模块化
- 快速编译
- 创建自己的插件,且具可访问性
- 可以像普通的 CSS 一样使用它
- 不依赖于任何预处理器就具备创建一个库的能力
- 可以与许多流行构建工具无缝部署
常用的插件有:
- Autoprefixer:自动补全CSS属性兼容性前缀
- postcss-cssnext:使用最新的CSS语法
- postcss-modules:组件内自动关联样式至选择器
- Stylelint:CSS语法检查器
- postcss-pxtorem:把px转换成rem
- …
当然,PostCSS 的解析并不局限于 CSS ,结合它提供的自定义语法解析接口,完全可以定义自己的语法。其实类似于 postcss-scss 的插件社区已经有很多了,使用这些插件,可以将原来基于 Sass、Less 等预处理器的代码迁移至 PostCSS。相对于传统的预处理器,PostCSS 这种开放平台型的体系,不拘束开发者的开发方式,同时也促进了更多对于 CSS 解决方案的探索。
PostCSS与 Sass、Less 等预处理器并不冲突。常用的解决方案是 预处理器 + PostCSS 的方案,当然也可以使用 PostCSS 来替代预处理器。
相关链接:
虽然SASS、LESS、Stylus等预处理器实现了 CSS 的文件拆分,但没有解决CSS模块化的一个重要问题:选择器的全局污染问题。按道理,一个模块化的文件应该要隐藏内部作用域,只暴露少量接口给使用者。而按照目前预处理器的方式,导入一个 CSS 模块后,已存在的样式有被覆盖的风险。
于是从工具层面,社区又创造出 Shadow DOM 、 CSS in JS 和 CSS Modules 三种解决方案。
- Shadow Dom 是 Web Components 的标准。它能解决全局污染问题,但目前很多浏览器不兼容,对我们来说还很久远;
- CSS in JS 是彻底抛弃 CSS ,使用 JS 或 JSON 来写样式。这种方式很激进,不能利用现有的 CSS 技术,而且处理伪类等问题比较困难;
- CSS Modules 仍然使用 CSS ,只是让 JS 来管理依赖。它能够最大化地结合 CSS 生态和 JS 模块化能力,目前来看是最好的解决方案。 Vue 的 scoped style 也算是一种。
CSS in JS
彻底抛弃 CSS ,使用 JS 或 JSON 来写样式。是2014年提出的一种方法。 Radium , jsxstyle , react-style 属于这一类。
它的观点是:不在单独的样式表中定义 CSS 样式,而是直接在每个组件中定义。
CSS in JS 的目标是能够用 HTML / JS / CSS 封装的“硬边界”定义组件,使得每个组件中的 CSS 不会影响任何其他组件。
优点:能给 CSS 提供 JS 同样强大的模块化能力。
缺点:不能利用成熟的CSS预处理器(或后处理器)Sass/Less/PostCSS,:hover
和 :active
伪类处理起来复杂。
可以分成两类:
一类还写我们熟悉的CSS语法,比如 styled-compnents:
import React from 'react'
import styled from 'styled-components'
const Header = styled.header`
color: #333;
font-size: 3rem;
`
export default () => <Header>this is heading</Header>
另一类则不再写 CSS 语法,而要把 font-size
写成 fontSize
,比如 jsxstyle:
import React from 'react'
import { Block } from 'jsxstyle'
export default () => <Block component='header' color='#333' fontSize='3rem'>this is heading</Block>
CSS Modules
CSS 的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。
产生局部作用域的唯一方法,就是使用一个独一无二的 class
的名字,不会与其他选择器重名。这就是 CSS Modules 的做法。
来看看一个简单的CSS Modules的例子:
// example.css
.article {
font-size: 14px;
}
.title {
font-size: 20px;
}
之后,将这些命名打乱:
.zxcvb {
font-size: 14px;
}
.zxcva {
font-size: 20px;
}
将之命名对应的内容,放入到JSON文件中去:
{
"article": "zxcvb",
"title": "zxcva"
}
之后,在js文件中运用:
import style from 'style.json';
class Example extends Component {
render() {
return (
<div classname={style.article}>
<div classname={style.title}></div>
</div>
)
}
}
这样子,就描绘出了一幅 CSS Modules 的原型。我们需要自动化的插件帮助我们完成这一个过程。
CSS Modules 不是一个官方的规范,也不是浏览器的一种机制,它是一种构建步骤中的一个进程。(构建通常需要 webpack 或者 browserify 的帮助)。通过构建工具的帮助,可以将 class 的名字或者选择器的名字作用域化。
CSS Modules 提供各种插件,支持不同的构建工具。本文使用的是 Webpack 的 css-loader 插件,因为它对 CSS Modules 的支持最好,而且很容易使用。
CSS Modules实现了:
- 所有样式都是局部化,解决了命名冲突和全局污染的问题
- class 名的生成规则配置灵活,可以以此来压缩 class 名
- 只需引用组件的 JavaScript,就能搞定组件所有的 JavaScript 和 CSS
CSS架构
也可以说是 CSS 中的一些设计模式,遵循这些设计模式,能让你的 HTML 与 CSS 更好的解耦,抽取样式中可复用的部分,使你的 HTML 代码更具语义。让你的 CSS 代码更加的可维护,同时更具模块化。
良好的 CSS 架构应该具备以下几个特性:
- 预测
- 复用
- 维护
- 延展
OOCSS
OOCSS(Object-Oriented CSS)即面向对象的CSS。由Nicole Sullivan提出于 2009 年提出。
使用这种结构,开发人员获得可以在不同地方使用的CSS类。
优点:通过复用来减少代码量(DRY原则)。让你的代码更灵活、更具有可复用性、可维护性及可扩展性。
缺点:维护非常困难(复杂)。当你修改某一个具体的元素的样式的时候,大部分情况下,除了修改CSS本身,你还不得不添加更多的标记类。
特点:
- 使用类名来扩展基础对象
- 坚持语义化的命名方式
有以下两大设计原则:
一、分离结构和设计
设计即一些重复的视觉特征,如边框、背景、颜色,分离是为了更多的复用;结构是指元素大小特征,如高度、宽度、边距等等。将对象设置为基本的样式,而如果这个对象存在多种多样的样式,我们通过添加皮肤的方式给他添加样式。
.button{
padding:10px;
box-shadow:rgba(0, 0, 0, .5)2px 2px 5px;
}
.widget{
overflow:auto;
box-shadow:rgba(0, 0, 0, .5)2px 2px 5px;
}
根据此原则,我们需要对公用的皮肤进行提取并分离,如下:
.button{
padding:10px;
}
.widget{
overflow:auto;
}
.skin{
box-shadow:rgba(0, 0, 0, .5)2px 2px 5px;
}
**
二、分离容器和内容
打破容器内元素对于容器的依赖,元素样式应该独立存在。
通常在我们写CSS的时候,我们通常回根据html元素的位置来规定样式,例如:
<div class="nav">
<ul>
<li>列表1</li>
<li>列表2</li>
<li>列表3</li>
</ul>
</div>
.nav ul li {...}
而在OOCSS认为,我们的样式不应该根据元素的位置来判断对象的样式。
而是应该给元素定义自己的样式,如:
<div class="nav">
<ul class="list">
<li class="list-item">列表1</li>
<li class="list-item">列表2</li>
<li class="list-item">列表3</li>
</ul>
</div>
.list {...}
.list-item {...}
MCSS
多层级的CSS。这种样式写法建议将样式分成多个部分,每个部分称为层(layers)。
- 第0层或基础(Zero layer or foundation):负责重置浏览器样式的代码(如:react.css 或者 normalize.css)。
- 基层(Base layer):包括可重用元素的样式,如 button , input , hints 等等。
- 项目层(Project layer):包括单独的模块和“上下文”—— 根据用户端浏览器或用于浏览的设备,用户权限等对元素的样式进行调整。
- 装饰层(Cosmetic layer):使用OOCSS风格来书写样式,对元素外观做微小的调整,建议仅留下影响外观的风格,而不能破坏网站的布局(例如颜色和非关键缩进等)。
层与层之间的交互层次是非常重要的:
- 在基层中定义中性的样式,并且不影响其它层。
- 基层中的元素只能影响基层的CSS类。
- 项目层中的元素可以影响基层和项目层。
- 装饰层是以描述OOCSS类的形式进行设计,不会影响其他CSS代码,而是在标记中有选择的使用。
SMACSS
SMACSS即可扩展和可模块化结构的CSS,该方法的主要目标是减少代码量并简化代码维护。Jonathan Snook 于2011 年提出了这一思想。
与 OOCSS 相比, SMACSS 有更多的细节,但在决定哪些 CSS 规则应该进入哪些类别时仍需要慎重考虑。后来像 BEM 这样的方法避免了做决策的步骤,使其更容易被采用。
它主要是将规则分为5类。
一、基础(Base)
这些是网站的主要元素的样式,如body,input,button,ul,ol等。在这一步中,我们主要使用HTML标签和属性选择器,在特殊情况下,使用CSS类(如:如果你有JavaScript-Style选择)。
html {
background-color: #fff;
}
body, form {
padding: 0;
margin: 0;
}
a {
text-decoration: none;
}
h1, h2, h3 {
margin: 1em 0;
}
二、布局(Layout)
主要是些全局元素,顶部,页脚,边栏等模块的大小。
#header {
width: 960px;
margin: 0 auto;
}
.l-article {
...
}
.l-grid {
...
}
.l-grid > li {
...
}
三、模块(Module)
模块(类似于卡片布局)可以在一个页面中使用多次。避免使用 id 或标记名称做选择器。子模块以原模块名称加 -
作为名称。如:.mod-header
, .mod-body
。
.modal-body {
width: 100%;
}
.modal-header {
height: 50px;
width: 100%;
}
四、状态(State)
规定了模块各种状态以及网站的基础部分。与 Layout,Module搭配,表示 Layout 或 Module 的状态变化。由 class 定义,命名规则是 .is-*
开头。
.is-hidden {
display: none;
}
.is-error {
font-weight: 700;
color: #f00;
}
.is-tab-active {
border-bottom-color: transparent;
}
五、主题(Theme)
设计你可能需要更换的样式。定义网站主视觉,与 Layout 类似,但影响的是网站整体视觉的变化。class 名称通常以 .theme-*
做开头。
Atomic CSS
在 2014 年引入的一种方法。是 Yahoo 提出来的一种独特的 CSS 代码组织方式,应用在 Yahoo 首页和其他产品中。是bootstrap中的工具类。
使用 Atomic CSS,为每个可重用的属性创建单独的CSS类。例如:margis-top: 1px
; 就可以创建一个类似于 mt-1
的 CSS 类,或者 width: 200px
; 对应的 CSS 类为 w-200
。
这种方法与 OOCSS、 SMACSS 和 BEM 完全相反,它不是将页面上的元素视为可重用的对象,而是完全忽略了这些对象,并使用可重用的单一实用工具类来对每个元素的样式进行设置。
优点:这种样式允许你通过重用声明来最大程度地减少你的 CSS 代码数量,并且也能很轻松的更改模块。写出基于视觉功能的小的,单用途的 CSS 类。
缺点: CSS 类名是属性名称的描述,而不是元素的自然语义。容易使人在开发过程中变得迷茫,开发本身也十分容易复杂化。
AMCSS
AMCSS是Attribute Modules for CSS的缩写,即针对属性的CSS设计。表示借组HTML属性来进行CSS相关开发。
传统我们多个模块是通过多个类名进行控制的,典型如下:
<div class="button button-large button-blue">Button</div>
而AMCSS则是基于属性控制,例如:
<div button="large blue">Button</div>
为了避免属性冲突,我们可以加一个统一的前缀,例如 am-
,于是有:
<div am-button="large blue">Button</div>
此技术能够实行离不开一个选择器:[attr~="val"]
。
BEM
是一种类名命名规则。由 Yandex 团队于 2009 年提出的一种前端命名方法论。饿了么的框架 Element UI 就是 BEM 的一种。
BEM是一种让你可以快速开发网站并对此进行多年维护的技术,能够帮助你在前端开发中实现可复用的组件和代码共享。
优点:
- 解决了命名空间的问题
- 多人协作时,只要有文档清楚标注规则,后来人可以很轻易的读懂,接手
- 更易于维护
缺点:
- 容易写得又长又丑
- 代码量比较多,没那么简洁
- 需要完善的说明文档和规则
Block(块)
我们可以将块理解为web应用中的组件或者模块。header,container,menu,checkbox,input。每个页面都可以看作是多个 Block 组成。
命名规则:
.button
.text-field
.heading
.menu
**
Element(元素)
是构成 Block 的元素,只有在对应 Block 内部才具有意义,无法独立于 Block 之外。menu item,list item,checkbox caption,head title。
命名规则:以 Block 名称加上两条底线 __
作为前缀。
.button__icon
.text-field__label
.heading__title
.menu__item
Modifier(状态)
描述 Block 或 Element 的属性或状态。同一个 Block 或 Element 可以由多个 Modifier。disabled,highlighted,checked,fixed,size big,color red。
命名规则:以 Block 或 Element 名称加上一个底线 _
作为前缀。
.button_active
.text-field_editable
.heading_align_top
.menu__item_promo
BEM的定义
【CSS模块化之路1】使用BEM与命名空间来规范CSS
总结
关于 CSS 的工程化,我认为可以遵循 CSS 架构中的方法,但是靠自觉是无法实现工程化的,所以我们必须借助 PostCSS 中提供的一些插件来规范我们的代码,解放我们的双手。同时再结合具体的情况来考虑是否需要预处理器(如果已经在用了可以继续用,没有的话其实 PostCSS 也可以达到同样的效果),以及 CSS in JS (这个我感觉有点激进哈,有点学习成本)或者 CSS Modules(因为Vue中的scoped也属于这一范畴,我觉得还挺方便的,可以考虑用一下)。但是无论使用什么工具,什么方法,定义良好的样式结构始终是最重要的。
参考文章: