为了克服 CSS 本身的缺陷,前端大牛们已先后发明了 LESS, SASS, BEM, CSS Modules, Styled Components, PostCSS 等等多种有相当流行度的技术方案。然而遗憾的是,虽然中级以上开发者们对组件和函数库都有了相当好的抽象能力,但即使是一些资深的开发者,在编写 CSS 时的表现也并未与初级开发者显著区分开。
本文在概括 CSS 失控模式,总结前端代码抽象方向,回顾 CSS 工程化方案进化史的基础上,识别技术方案需要解决的核心问题,并提供一套以全局类名系统为核心的解决方案。
问题与背景
CSS 失控模式
大部分前端开发者写 CSS 时仍在重复这样一个一步步走向失控的模式:
- 起初,没有人觉得自己写 CSS 的方式有问题(无意间会犯很多错,后文再总结)
- 后来,随着工程规模的增长,维护自己代码时为赶时间往往选择复制粘贴。维护别人代码时因读懂并保持一致性的成本过高,理所当然地另起炉灶并复制粘贴。于是,代码量随需求线性膨胀(正常模式下,因受益于复用,需求与代码量本应接近对数关系)
- 最后,场面逐渐失控:样式层层覆盖定位困难;优先级困局让人焦头烂额;一组样式多处使用,欲改而不敢;无从判断哪些是有效样式,哪些是地雷;CSS 代码行数超过了 HTML 和 JS 的总和。
前端代码抽象方向
前端代码有三个主要的抽象领域:函数库、组件/插件和全局样式。JS 打头阵的函数库诞生已久,供我们在页面/组件和其他模块内直接调用。组件分多种层级,从小到大如同 HTML 标签最终汇聚成页面。这两者,做过组件化工程的开发者都很熟悉,也是大部分开发者日常的关注点所在,此处不再赘述。然而 CSS 的复用度始终停留在低水平,长此以往,样式表成了前端开发者的难言之隐。
从作用域的角度来说,能够高度复用的样式一定是全局样式。但谈及全局样式,大部分开发者通常想起的是以下几种:
- 初始化场景中的样式(如 normalize.css)
- UI 组件库和动画库(如 animate.css)带来的全局样式
- jQuery 时代的由 Bootstrap 等 CSS 框架提供的类名系统
- 多个页面用到的样式(有跨页面用到的就放进去)
尽管这些样式都是全局样式,然而我们在此要先提示一下:真正重要的全局样式,是可以在各级组件内高度复用的样式。按照这个标准,以上第 4 类是不符合的。且 Bootstrap 与 UI 组件库提供的类名,往往与 HTML 结构耦合过于紧密,复用度也打了折扣。
再看 CSS 进化史
2017年初,随着 React 的火爆,一篇名为 《CSS Evolution: From CSS, SASS, BEM, CSS Modules to Styled Components》的文章在社区传播开,颇为系统地盘点了几种主流的 CSS 技术方案,最终热情洋溢地向开发者安利了一波 Styled Components,这种新颖的 CSS 书写方式在 React 社区引起极大开发者的极大兴趣。文章开头有一张极富表现力的配图:
这张配图足以让重视 CSS 的开发者过目不忘,但也传达了错误的认知:类比进化论的逻辑,后来的新方案必然替代原有旧方案。然而深入了解这几种技术后会发现,它们其实是为解决不同问题而生的:
- 提高 CSS 复用度的方案:LESS/SASS(提供变量、Mixin 等)、原生 CSS 类名系统
- 提高命名规范性与可读性的方案:技术型语义化、业务型语义化(命名规范推荐 BEM)
- 规避命名冲突的方案:Scoped(Vue), CSS Modules, Styled Components(React), 行内样式
- 兼容性工具:PostCSS, Flexible, px2rem 等等
关于语义化,大家都有努力在做,但能做好的其实不多。一方面是受限于英语能力,但更主要的原因在于没有区分两种不同的风格。全局样式类如同 JS 函数库,需要脱离业务高度复用,适用技术型语义化,要能表示样式本身。而组件与业务紧密相关,组件样式类也应使用业务型语义化以表明业务目的。需要注意的是,组件分很多层级,越是底层的越偏技术,越是接近页面的,越偏向业务。本文所述组件多指后者。
将技术方案分类后可以发现,为了解决复用性、规范性、兼容性和命名冲突四大问题,开发者的工具箱中必然需要一套组合。在下文的分析中我们会看到,原生 CSS 类名系统可以替代 LESS/SASS 预处理器,因而抛开兼容性工具,针对 Vue 和 React 开发者推荐的方案组合分别是:
- Vue: 原生 CSS 类名系统、BEM 规范、Scoped(Vue 内置功能)
- React:原生 CSS 类名系统、BEM 规范、CSS Modules/Styled Components(任选其一)
因为 Scoped、CSS Modules 和 Styled Components 适用于组件自身的样式,而本文更关注可在各级组件中高度复用的全局样式,故接下来将重点展示如何打造一个有效的全局类名系统,而非介绍这几套方案。关于组件样式,下文重点关注基于 BEM 命名规范的写法及与全局类名系统的搭配使用。
核心方案
全局类名系统
类名系统的有效性,取决于两方面:
- 类名的可读性、易记性。可读性差不易记忆的类名显然不可接受。
- 类提供的样式对开发需求的覆盖度。如果无法覆盖大部分可以标准化的样式模式,方案本身便不可靠。
为了覆盖大部分场景,需要对常用样式做有效分类。推荐分为下面四大类:
- 布局类:layout.css
- 色彩类:painting.css (黑白灰) + theme.css (彩色系)
- 装饰类:decoration.css(如 box-shadow)
- 动画类:animate.css(已有开源 CSS 动画库)
布局类:layout.css
关于布局的所有内容,又可划分为定位技术、Flexbox 和经典盒子模型三大类。
关于定位技术和 Flexbox,常用模式其实不多,先给几个类名,含义就不解释了:
- .absolute-center, .absolute-full, .fixed-top, .fixed-bottom, ...
- .flex, .flex-y, .space-bettwen, .align-center, .center_space-around, .center_center, .flex-1
数量稍多的是经典盒模型:
- maring & padding:.m-15 .mt-20 .mr-8 .mx-12 .p-16 .pb-30 .pl-5 .py-10
- width & text: .full-width .text-right .text-center .text-12 .text-14 .text-18, ...
含义也不解释了,但说明一点:习惯上,t, r, b, l 表示上右下左,x 与 y 表示横纵方向或横纵方向的两侧。
对于 border,相比于宽度(高频模式其实只有 1px),我们更关注线型与色彩。
对于 height,通常是自动计算或在组件样式中做个性化定义,故不列出也罢。
当然,还有一个佛系的: .none {display: none}
色彩类:painting 与 theme
区分 painting 与 theme 是因为黑白灰三种色彩在所有模块中均是通用的,是严格意义上的全局类。彩色系的尽管只在模块/系统内跨页面复用,但命名也更适合技术型风格。
- 文本色:.text-gray-6 .text-gray-9, .text-red-light .text-green .text-blue-deep
- 背景色:.bg-white .bg-gray-fe, .bg-green-xx, ...
- 边框色及线型:.b-solid-d .bt-dashed-d .br-dotted-e .bb-solid-xx, ...
装饰类:decoration.css
因使用频率较低,目前尚未总结出足够多的模式,此处仅举两类例子:
- .text-underline, .text-ellipsis(也可归入 layout)
- .box-shadow-xxx (此处 xxx 表示可进一步抽象的空间还很大)
动画类:animate.css
因使用频率较低,且已有流行度颇高的 animate.css, 本文不再举例。
小结
两个要点:
- 关于选择器:包括 animate.css 和 Bootstrap 在内的 CSS 库几乎都通过类名为开发者提供便利,而开发者在日常开发中对选择器的选择却似乎没有受到启发。如同《JavaScript 语言精粹》中倡导摒弃 JS 语言中的糟粕,才能发挥 JS 的优势,在 CSS 编码中,我们也要回避一些副作用大的选择器,转而采用统一的 class 选择器,并使用规范、直观的命名。当然,一些装饰效果可能也会用到伪类和伪元素。
- 关于语义化:因与具体业务分离,目的是供业务组件高度复用,采用技术型的语义化,基于 CSS 样式本身进行命名,更易于记忆和使用。
几点优点:
- 因高度复用,全局样式规模高度稳定,不随项目膨胀。事实上,将全部类名补上样式并规范换行后即可发现,代码总量不过千行左右
- 归类清晰,易于定位,可读性好,易用易记
- 区分 painting 与 theme 兼顾高度复用性与设计灵活性
值得强调的是,在字体大小和色彩上,多个同组类名如同定义了一组变量,搭配 Vue 和 React 为大家提供的混合机制,足以胜任以往只能由 LESS 变量完成的任务。
基于 BEM 规范的组件样式
关于 BEM 命名规范,本文不再做详细介绍。遵守 BEM 的几种命名模式如下:
- .the-block, 如 .user
- .the-block_the-element, 如 .user_first-name
- .the-block__the-modifier, 如 .user__active
- .the-block_element__modifier, 如 .user_status__active
小结
关于分隔符:
- 最流行的模式是 B__E--M, 然而我更喜欢 B_E__M
- 个人认为 -- 这种写法太丑
- 流行的第三方组件库通常采用第一种,与之区别开来可以增加自有代码的辨识度
关于 for 循环生成的列表:
- 外层容器作为单独的 block 用英文名词复数形式或单数加 -list 命名,如 .users 或 .user-list
- 单个列表子项 block 使用名词单数形式命名,如 .user
- 建议避免 wrapper, item 这样偏技术风格而不能表明业务目的的用词
关于选择器:
- 类名为核心,伪类、伪元素为辅助
- 其他选择器慎用,原则上禁用子代、后代选择器
关于语义化:
- 因与业务联系紧密,组件内的 BEM 类应采用业务型语义化方案,力求表明业务目的
- 注意:一律启用 scoped(Vue), CSS Modules/Styled Componets(React), 杜绝污染全局样式
几个优点:
- 语义嵌套取代了选择器嵌套,无优先级困惑,渲染效率高
- 可读性极好,突出业务目的,对于英语功底较好用词准确的开发者,可以一定程度上替代注释
- 易归类和定位(同一个 block 的样式不用嵌套也容易找到)
- 易识别无效样式:没有引用的类名必定无效;组件抽象充分时,不再需要复制粘贴,冗余样式大大减少
- 天然的组件化:理论上每个 BEM 中的 block 都可抽象为组件,尽管我们不会那么做
搭配使用
全局类名可通过两种方式在各级组件内复用
- 原生 CSS:直接在 template/html 标签中使用多个类名即可,此时组件内 BEM 类名要提供的样式将大大减少。
- 借道 LESS:在 BEM 类名中通过使用 Mixin 语法快捷引用全局样式,仍然可以显著减少 BEM 组件类的代码行数。
尽管两种方式都是可行的,且不习惯在 html 标签中写大量类名的开发者会倾向于使用 Mixin 语法以保持 html 的简洁性,但实际上经过一段时间的使用就会发现,将注意力集中在 template 和 script 而避免频繁切换到 style 直接面对花括号,可以大大提高工作效率,而 html 的清晰度可以通过合理的换行来弥补。
还有一个意外收获:当我们发现自己在频繁地重复写同一组类名时,代码可以给我们很强烈的提示,提醒我们去抽象更细粒度的组件,帮助我们将组件抽象得更好。
总结
再看失控模式
上文我们略去了在初始阶段开发者们容易犯的错误,这里做一个不完全汇总:
选择器求全
- 开发者们乐于使用花样繁多的选择器
- 学会做减法,单一的 class 选择器已足够强大
命名随意化
- 此处略去三千字
作用域意识缺失
- 嵌套、继承、选择器、代码位置、冗余代码等都会带来意料之外的效果
- 一个非常常见的坏习惯是将跨页面用到的样式丢到全局样式中去,哪怕这些样式用了复杂的选择器,只应用在两个页面/组件。这将极大破坏全局样式的可靠性,造成全局样式规模不可控,组件抽象不彻底,样式影响范围过大而不敢改等一连串后果。
- 如果你需要跨页面/组件用样式,请检查:组件抽象是否不彻底?全局类名系统是否不完整?
不厌其烦重复写样式
- 如果不理解这句话,做过 H5 的可以统计下自己在多少个页面和类名下写了 {display: flex; justify-content: space-between;}
- 没用过 Flexbox 的 H5 开发者请自行反思
- 开发者普遍觉得重复样式不可避免,但我们的全局类名系统最早要解决的正是这个问题
- 复制粘贴是种假勤奋,还会造成大量冗余代码,为许多诡异问题埋下伏笔
编码体验展望
正确使用全局类名系统搭配 Scoped BEM 组件类,即可避免上述问题,写出高可维护性和高性能的 CSS 代码,享受这样的编程体验:
- 核心类名系统几乎固定,规模不可能失控
- 组件样式规模因类名系统的复用大幅缩减
- 命名规范的单一选择器,可读性强,作用范围清晰
- 层叠最小化(不同样式规则既有效组合形成 computed 样式,又避免层层覆盖),可快速定位调试,无效样式易识别
- 不再依赖预处理器
- 追求无嵌套,易解析
- 样式通过类名组合实现,避免无效覆盖,实现“层叠”的最小化
- 全局类与组件类有效混搭又肉眼可辨
- 彻底告别 !important
最终享受完整的搭积木式编程:函数库、组件、类名系统三大积木体系帮助前端开发者全面掌控 JS, HTML 与 CSS。
核心概念回顾
- 日常 CSS 开发应关注和致力于解决的两个问题:可复用性、命名冲突
- 强化作用域意识,全局类名系统提升复用度,Scoped BEM 组件类解决命名冲突
- 语义化有两种:高复用的全局类使用技术型语义化,组件类使用业务型语义化
在此强调一下,技术型语义化与业务型语义化的概念为本文首创,事实上不仅适用于 CSS,也适用于 JS 和 HTML。
实践效果
笔者已在两家分属于互联网和金融行业的中大型公司的小程序、内管和 H5 等不同类型的前端应用中了采用类似的类名系统,layout 与 painting 部分的合计代码量在 800 行以内且高度稳定,而组件/页面样式的代码行数则因全局类名的高度复用降幅达到 40% 至 90%,显著提升了开发效率和代码质量,可维护性表现良好。在只有交互没有 UI 的情况下,搭配第三方组件库,做出了商业级美观易用的内管系统,而在样式表上投入的时间与为赶工期只关注功能的内管系统无异。
最后我们以一小段应用了全局类名的 template 来结束本文:
Hello world
Happy Hacking
大家可以试着脑补下这段 template 渲染出来后的画面。
Happy hacking -_^