深度解读CSS工程化实践成果

2017 年 11 月 3 日 CSDN大数据 张伟

作者 | 张伟

责编 | 陈秋歌

CSS技术的演进

CSS是Web开发中不可或缺的一部分,在前端工程化不断进步的今天,一方面CSS特性随着规范的升级越来越丰富,另一方面,前端业务复杂性的增加带来的工程愈加庞大, 驱使着开发者不断寻找CSS工程化的最佳实践。

Web开发模块化趋势

不可否认, 无论从现代前端框架(React、 Vue、Angular、 Polymer等), 还是从W3C的Web Components草案来看,组件化已经是前端开发的主流之选和未来的发展方向,正如在Reddit上有网友说道“Facebook.com's codebase includes over 20,000 components”。 广义上看,所有页面上都可以被划分成一个个组件,相对于过去以网页作为开发单位,以组件为单位开发有着可复用、可扩展等一系列有利于项目工程化的优点。

在这种组件化趋势的背景下,CSS模块化也渐渐有着各种尝试。

预处理与后处理

预处理

比较流行的CSS预处理器有Sass、Less和Stylus,CSS预处理器的出现主要因CSS缺少编程语言的灵活性而生,是引入了一些编程概念而生的DSL,开发者编写简洁的语义化DSL代码,由预处理器编译成CSS。

以Sass为例,该预处理器支持.scss、.sass文件类型,其语法支持变量、选择器嵌套、继承(extend)、混合(mixin)和一些逻辑语句,同时还支持跨文件的导入功能,因而使得开发者能够很好地使用编程思想书写样式。

从实际使用情况来看,几个预处理器各有优缺点,从社区活跃度上看Sass>Less>Stylus。Sass是三个中间最早也是最成熟的,因而有着很多开源积累和很好编程范式,像内置了很多Sass函数的Compass框架,就是很好的一个例子。Less相对于Sass的优点在于十分的轻量,也完全兼容CSS,但另一方面可编程能力不如Sass,Bootstrap最新版本的CSS预处理器也从Less换成Sass。Stylus来源于Node社区,使用体验上并不输给Sass和Less,无论是编译速度还是语法范式,个人看来,Stylus在某种程度上更加优于其他两个。

后处理

后处理器是对原生CSS进行处理并最终生成CSS的处理器,广义上还是个预处理器,与上面提到的预处理器不同的是,它处理的对象是标准CSS(如图1所示),比较典型的后处理工具有以下几种。

  • clean-css:压缩CSS

  • AutoPrefixer:自动添加CSS3属性各浏览器的前缀

  • Rework:取代Stylus的插件化框架

  • PostCSS

图1 预处理器与后处理器关系

PostCSS

PostCSS最初是从AutoPrefixer项目中抽象出来的框架,它本身并不对CSS做具体的业务操作,只是将CSS解析成抽象语法树(AST),样式的操作由之后运行的插件系统完成(如图2所示)。正如其本身所言Transforming styles with JS plugins”。

图2 PostCSS 工作机制

更多时候我们在讨论PostCSS的时候,并不止停留在它是解析CSS的核心工具,更包括它创建的插件系统,而今PostCSS最为吸引开发者的正是其扩展性较强的插件系统和丰富的插件支持。

常用的插件有:

  • Autoprefixer:自动补全CSS属性兼容性前缀

  • postcss-cssnext:使用最新的CSS语法

  • postcss-modules:组件内自动关联样式至选择器

  • Stylelint:CSS语法检查器等

如果已有的插件不能满足现有的需求,完全可以手写一个插件:

// 官方示例rem转px
var custom = function(css, opts){
    css.eachDecl(function(decl){
        decl.value = decl.value.replace(/\d+rem/, function(str){            
return 16 * parseFloat(str) + "px";
        });
    });
};

当然,PostCSS的解析并不局限于CSS,结合它提供的自定义语法解析接口,完全可以定义自己的语法。其实类似于postcss-scss的插件社区已经有很多了,使用这些插件,可以将原来基于Sass、Less等预处理器的代码迁移至PostCSS。相对于传统的预处理器,PostCSS这种开放平台型的体系,不拘束开发者的开发方式,同时也促进了更多对于CSS解决方案的探索。

回过头来看,为什么会有CSS预处理操作后的处理操作?其实主要的原因在于前端项目的膨胀使得用传统手工编写并维护CSS变得很不堪,根本原因则是CSS缺少编程语言特性,要做到CSS代码的模块化以及高复用的抽象处理,就必须引入一些编程的思想。相对于JavaScript标准推进以及基础设施的完备,CSS在编程方面的探索更多来自于社区,也并无统一的事实标准,这也是CSS发展落后于JavaScript的原因。

namespace约束

一方面我们需要关注技术能够带来代码上的模块化,另一方面我们又要思考如何使用一个良好的风格架构起项目中的CSS。CSS除了代码外,另一个很重要的就是CSS选择标记。而CSS选择器的命名空间是全局的,并没有局部的概念,因而如何利用好这个全局的空间,选择良好的结构风格,也是在开发过程中必须考虑的。

OOCSS

OOCS(Object-Oriented CSS)即面向对象CSS,主要有两个核心原则。

  • 分离结构和皮肤(Separate Structure and Skin)

皮肤即一些重复的视觉特征,如边框、背景、颜色,分离是为了更多的复用;结构是指元素大小特征,如高度、宽度、边距等等。

.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;
}
  • 分离容器和内容(Separate Container an Content)

打破容器内元素对于容器的依赖,元素样式应该独立存在。如下面示例。

<div class="container"><h2>xxx</h2></div>
.container h2 {...}

上面的h2元素依赖于父元素container,对应此原则,h2元素需要使用一个单独的选择器,如下。

<div class="container"><h2 class="category">xxx</h2></div>
.category {...}

从实践中看出,使用OOSCC范式,遵守了DRY的原则,能够大量减少重复的样式代码,提高代码复用;同时,视觉元素可以灵活组合各个类名,展示不同的效果,丰富的类名也同时使得元素有着更好的可读性;另一方面,由于容器和内容的分离,CSS完成了与HTML结构解耦。

但同时也会带来一些缺点,抽象复用会使class越来越多,极端情况下可能会产生很多原子类,这对于那些偏向于“单一来源原则”的开发者来说并不受欢迎。

SMACSS

SMACSS(Scalable and Modular Architecture for CSS)即模块化架构的可扩展CSS,它主要将规则分为五类。

  • 基础(Base)

tag select的样式, 定义最基础全局样式, 如CSS REST。

html, body, form { margin: 0; padding: 0; }
a { color: #039; } a:hover { color: #03C; }
  • 布局(Layout)

将页面分为各个区域的元素块。

.header{}
....
.footer{}
  • 模块(Module)

可复用的单元。在模块中,需要注意的是选择器一律选择class selector,避免嵌套子选择器,减少权重, 方便外部覆盖。

<div class="pod pod-constrained">...</
div>
<div class="pod pod-callout">...</div>
.pod { width: 100%; }
.pod .pod-callout { width: 200px; }
.pod .pod-constrained{}
  • 状态(State)

状态class一般通过JavaScript动态挂载到元素上,可以根据状态覆盖元素上特定属性。

.tab { background-color: purple;... }
.is-tab-active { background-color:white; }
  • 主题(Theme)

可选的视觉外观。一般根据需求有颜色、字体、布局等等,实现是将这些样式单独抽出来,根据外部条件(data属性、媒体查询等)动态设置。

SMACSS的主要优点在于按照不同的业务逻辑,将整个CSS结构化分更加细致,约束好命名,最小化深度,在编写的时候,使用SMACSS规范能够更好地组织CSS文件结构和class命名。

BEM

BEM即Block Element Modifier,类名命名规则为Block__Element--Modifier。

  • Block所属组件名称

  • Element组件内元素名称

  • Modifier元素或组件修饰符

其核心思想就是组件化。首先一个页面可以按层级依次划分出多个组件,其次就是单独标记这些元素。BEM通过简单的块、元素、修饰符的约束规则确保类名的唯一,同时将类选择器的语义化提升了一个新的高度。

<form class="form form--theme-xmas form--simple">
<input class="form__input" type="text"/>
<input class="form__submit form__submit--disabled" type="submit" />
</form>
.form { }
.form--theme-xmas { }
.form--simple { }
.form__input { }
.form__submit { }
.form__submit--disabled { }

BEM通过简单的命名规则使得关联类名元素语义性、可读性更强,有利于项目管理和多人协作。同时BEM方案中并没有嵌套,所有类名最浅深度,并不会出现嵌套过深难以覆盖的情况,易于维护、复用。

另一方面,BEM强调单一职责原则和单一样式来源原则,意味着传统纯手工CSS可能会产生大量重复的代码,但是结合各种CSS预处理和PostCSS就可以很好避免问题的产生。另外,虽说原则简单,但在实际使用中,维护BEM的命名确实需要一些成本,很多时候命名反而成了一件难事。

CSS in JS

CSS in JS方案一开始是由Facebook工程师Vjeux在一次分享中提出的,针对CSS在React开发中遇到的各种问题,随后社区涌现了各样方案。

虽然以上模块化的命名约定可以解决风格上的问题,但正如上文而言,也引入一些成本。而对于一些高复用的组件,使用以上高度语义化的方案是个很好的选择,这种成本是必需的,但对于没有复用的业务组件来说,显然这种命名的成本大于收益,特别是在多人协作时候。另外,面对现代前端框架的发展,纯靠CSS方案并不能很好地解决。

CSS Modlue

CSS Module不同于Vjeux完全放弃CSS的做法,它只是选择了用JavaScript来管理样式与元素的关联,CSS Module为每个本地定义的类名动态创建一个全局唯一类名,然后注入到UI上,实现编写样式规则的局部模块化。

css-loader内置支持CSS Module, 只需设置下查询参数, 即可在JavaScript中使用CSS文件的导入。

{
  loader: 'css-loader',
  query: {
    module: true,
    localInentName: '[name]__[local]--[hash:base64:5]'  //  
  }
}

在JavaScript中导入CSS文件,最终得到的其实是一个CSS文件经过parse后生成的类名映射对象{[localName]: [hashed-Name], ....}。

// Header.jsx
import style from './Header.css'
...
console.log(style)//{header: 'Header__header--3kSIq_0'}
export default () => <div className={style.header}></div>

同时CSS文件也会被编译成对应的类名。

.Header__header--3kSIq_0- {}
// from Header.css .header{}

从开发体验上看,CSS-Module这种做法让开发者不必在类名的命名上小心翼翼,直接使用随机编译生成唯一标识,让类名成为局部变量成为了可能。但同时也因为随机性,失去了通过此局部类名实现样式覆盖的可能性,覆盖时不得不考虑使用其他选择器(如属性选择器)。对于复用的组件而言,灵活性是必不可少的,这种局部模块化方案并不适合这种高度抽象复用的组件,而对于一次性业务组件确实能够提升开发效率。

同时CSS Module还支持使用composes实现CSS代码的组合复用。

/* button.css */
.base{}
.normal {
composes: base
...
}
// button.jsx
import style from './button.css'
export default () => <button className={style.normal}>按按</button>// <button class="button__base--180HZ_0 button__normal--x38Eh_0">按按</button>

当然CSS Module还可以配合各种预处理器一起使用,只需在css-loader之前添加对应的loader,但在编写的时候要注意CSS Module的语法要在处理器之后合法。实际使用中,对于CSS代码的解耦,如果引入了预处理器,代码文件的模块化就不建议使用composes来解决。

styled-components

styled-components也是一个完全的CSS in JS方案,先看语法。

// button
import styled from 'styled-compenents'
const Button = styled.button`
  padding: 10px;
  ${props => props.primary ? 'palevioletred' : 'white'};
`
<Button>按钮</Button>
<Button primary>按钮</Button>

其编译后也是如同CSS-in-module一样,随机混淆生成全局唯一类名,对应生成CSS文件。styledcomponent的核心是“样式即组件”,将字符串解析成CSS,并创建对应该样式的JSX元素,它有着JavaScript强大的编程能力,完全可以胜任,同时让组件样式与组件逻辑耦合在一起,真正做到组件紧耦合少依赖。当然有些开发不喜欢这种耦合,也完全可以将样式组件和逻辑组件分离,而在JavaScript中分离代码本身也是件易事。

当然,styled-components真正的应用并不仅仅如此,它完全是一个完备的样式解决方案,有着如扩展、主题、服务端渲染、Babel插件、ReactNative等一系列支持,也深受一些开发者欢迎。这里比较有趣的是看似奇怪的语法形式, 其实是ES6中模板字符的特性。

styled-components本身是React社区针对JSX产生的一种方案,当然在Vue中通过vue-styledcomponents也能使用该功能,但是使用体验一般,无论是在模板里还是在JSX中,使用组件都需提前声明并注入到组件构建参数中,过程十分繁琐,而且不同于React纯JSX的组件渲染语法,Vue中并不能对既有的组件使用styled语法。

但另一方面,将CSS完全写在JavaScript中,社区里中也有很多人持反对态度,react-css-modules的作者就专门发文表示反对styled-component这种完全抛弃CSS文件的开发模式。

总结

我们在开发之前,面对各种技术方案,一定要选取并组合出最适合自己项目的方案,是选用传统的CSS预处理器,还是选用PostCSS?是全局手动维护模块,还是完全交给程序随机生成类名?都需要结合业务场景、团队习惯等因素。另一方面,CSS本身并无编程特性,但在其工程化技术的发展中不乏很多优秀的编程思想,无论是自定义DSL还是基于JavaScript,这其中带给我们思考的正是“编译思想”。

作者:张伟,饿了么前端工程师,目前参与饿了么物流平台前端业务迭代,关注前端技术发展,业余也研究Java、Node.js等服务端开发技术,推崇各种自动化。 
本文为《程序员》原创文章,未经允许不得转载,更多精彩文章请点击「阅读原文」订阅《程序员》


登录查看更多
1

相关内容

层叠样式表(Cascading Style Sheet)是一种用来为结构化文档(如 HTML 文档或 XML 应用)添加样式(字体、间距和颜色等)的计算机语言。
FPGA加速系统开发工具设计:综述与实践
专知会员服务
65+阅读 · 2020年6月24日
专知会员服务
183+阅读 · 2020年6月21日
最新《深度多模态数据分析》综述论文,26页pdf
专知会员服务
298+阅读 · 2020年6月16日
【Manning新书】现代Java实战,592页pdf
专知会员服务
99+阅读 · 2020年5月22日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
234+阅读 · 2020年5月21日
【书籍】深度学习框架:PyTorch入门与实践(附代码)
专知会员服务
163+阅读 · 2019年10月28日
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
硬核实践经验 - 企鹅辅导 RN 迁移及优化总结
IMWeb前端社区
5+阅读 · 2019年5月6日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
教程 | PyTorch经验指南:技巧与陷阱
机器之心
15+阅读 · 2018年7月30日
深度学习在CTR预估中的应用 | CTR深度模型大盘点
PaperWeekly
15+阅读 · 2018年4月11日
【人工智能架构】深度解密京东登月平台基础架构
产业智能官
11+阅读 · 2017年9月26日
十五条有用的Golang编程经验
CSDN大数据
5+阅读 · 2017年8月7日
Revealing the Dark Secrets of BERT
Arxiv
4+阅读 · 2019年9月11日
Hardness-Aware Deep Metric Learning
Arxiv
6+阅读 · 2019年3月13日
Parsimonious Bayesian deep networks
Arxiv
5+阅读 · 2018年10月17日
Large-Scale Study of Curiosity-Driven Learning
Arxiv
8+阅读 · 2018年8月13日
Arxiv
8+阅读 · 2018年5月15日
Arxiv
4+阅读 · 2018年3月30日
Arxiv
7+阅读 · 2018年3月22日
VIP会员
相关VIP内容
FPGA加速系统开发工具设计:综述与实践
专知会员服务
65+阅读 · 2020年6月24日
专知会员服务
183+阅读 · 2020年6月21日
最新《深度多模态数据分析》综述论文,26页pdf
专知会员服务
298+阅读 · 2020年6月16日
【Manning新书】现代Java实战,592页pdf
专知会员服务
99+阅读 · 2020年5月22日
【实用书】Python技术手册,第三版767页pdf
专知会员服务
234+阅读 · 2020年5月21日
【书籍】深度学习框架:PyTorch入门与实践(附代码)
专知会员服务
163+阅读 · 2019年10月28日
相关资讯
美团:基于跨平台框架Flutter的动态化平台建设
前端之巅
14+阅读 · 2019年6月17日
硬核实践经验 - 企鹅辅导 RN 迁移及优化总结
IMWeb前端社区
5+阅读 · 2019年5月6日
从webview到flutter:详解iOS中的Web开发
前端之巅
5+阅读 · 2019年3月24日
教程 | PyTorch经验指南:技巧与陷阱
机器之心
15+阅读 · 2018年7月30日
深度学习在CTR预估中的应用 | CTR深度模型大盘点
PaperWeekly
15+阅读 · 2018年4月11日
【人工智能架构】深度解密京东登月平台基础架构
产业智能官
11+阅读 · 2017年9月26日
十五条有用的Golang编程经验
CSDN大数据
5+阅读 · 2017年8月7日
相关论文
Revealing the Dark Secrets of BERT
Arxiv
4+阅读 · 2019年9月11日
Hardness-Aware Deep Metric Learning
Arxiv
6+阅读 · 2019年3月13日
Parsimonious Bayesian deep networks
Arxiv
5+阅读 · 2018年10月17日
Large-Scale Study of Curiosity-Driven Learning
Arxiv
8+阅读 · 2018年8月13日
Arxiv
8+阅读 · 2018年5月15日
Arxiv
4+阅读 · 2018年3月30日
Arxiv
7+阅读 · 2018年3月22日
Top
微信扫码咨询专知VIP会员