CSS – 管理
前言
CSS 有好几种写法. 它们最终出来的效果是一样的, 区别只是在你如何 "写" 和 "读" 或者说开发和维护.
这已经不是如何"实现"的问题了, 而是代码如何管理维护的问题.
参考
CSS Utility Classes and "Separation of Concerns" (Tailwind CSS 作者的文章, 一定要看)
CSS 架构系列 – OOCSS, BEM, SMACSS, ACSS
Is BEM Framework the best one? Share alternatives!
RSCSS — Styling your CSS without losing your sanity .
CSS 进化论:从CSS,SASS,BEM,CSS Modules到Styled Components (英语原文)
Youtube – 7 ways to deal with CSS
CSS 模块化方案探讨(BEM、OOCSS、CSS Modules、CSS-in-JS ...)
React拾遗:从10种现在流行的 CSS 解决方案谈谈我的最爱 (上), (中), (下)
Selector 流派
职责
HTML 只负责内容结构, 不涉及任何 "Styling" 这个是 selector 流派的重点.
所有 Styling 交给 CSS 负责.
它们之间的 connect 方式就是通过 selector.
Selector 方式与 class 命名
selector 要好维护, 就必须避开一些潜在的风险.
1. selector 要稳定.
比如我想给一个 icon styling. 我用 tag selector 的话, 可能是 <i> 也可能是 <svg> (fontawesome 用 <i>, 其它可能用 svg)
这样就不太稳定, 改成 class name .icon 就稳定了. 所以一般上大家都会建议 selector 用 class name, 不要用 tag name.
但这也不是绝对的, 关键就是你认为它是否稳定.
2. class name 撞名字
selector class 是全局的, 这意味着一不小心就会撞名字了.
比如
.section-1 .title
.section-2 .title
title 就是一个重复的 class name. 但它们的样式不一定是相同的. 因为当我们在命名的时候, 我们的潜意识, 是有作用域概念的.
BEM
BEM 就是通过 namespace 的命名方式, 解决 class name 撞名字的问题的.
其原理就是把名字取长一点...比如 block__element--modifier
.section-1 .section-1__title
.section-2 .section-2__title
CSS Modules
另一个方法是通过 CSS Modules, 比如 Angular omponent styles 或者 Asp.net Core – CSS Isolation
它们的原理是通过 pre-compile 动态帮你添加 namespace. 方式和 BEM 差不多, 只是 BEM 是 manual 的, 这个是 auto 的. 管理上差很大.
Descendant / Child selector (RSCSS)
.section-1 > title
.section-2 > title
只要确保 root selector 不要撞名字, 通过 descendant selector 是可以确保后续不会撞的. 注意: 如果有嵌套的话, 最好使用 > child selector
不然还是有可能会撞名字的.
BEM vs RSCSS BEM 的缺点是 class name 长, descendant 的缺点是并不能 100% 做到保护, child 的缺点是没有 descendant 那么干净.
左图是 BEM 的 HTML 页面, class name 很长, 很丑
右图是 RSCSS child selector 的画面, 就是多了很多 > 箭头, 而且层次结构需要完全与 HTML 一致 (耦合度比 BEM 大)
疑惑
问: class="btn-blue" 是一个合格的 class name 吗?
答: 不是, blue 是样式.
缺点和局限
selector 流派最大的问题就是 Styling 的复用. 按照它的标准, 一个 element 只会有一个 class name 来表示.
所有 styling 也只能写到这个 class 上面, 那怎么能复用呢?
.page-title-section
.call-to-action-section
比如上面 2 个 class 是不同的东西. 但是它们都是 section
想抽象 section style 有几个做法.
1. .page-title-section, .call-action-section 用"或者" selector
2. 添加多一个 class 到 HTML
class="page-title-section section" <-- 像这样.
但这样你就会觉得怪怪的了. 因为 class 的作用应该只是为了能 selector 到它. 那为什么要搞 2 个呢?
其实是为了 styling. 而我们说过 HTML 不应该被 Styling 影响.
3. 用 Sass 的 @extend 或 @include
方法 1 和 3 是比较正确的方式, 2 有一点点偏向 Atom 流派了.
伪命题
有人说, HTML 和 CSS 不要耦合, 要分开.
1. 当你修改 HTML 的时候不要影响到 CSS
2. HTML 可以独立复用, CSS 也可以独立复用.
我想说的是, 不要去搞这些东西. 绝大部分的时候 HTML, CSS 甚至 JS 是密不可分的.
这也是为什么 React, Tailwind CSS 最终会把 3 剑客写成一团.
另一个话题是, 写成一团好不好, 看上去确实乱, 但是也没有乱到不能管理.
像 Angular, 它不直接在 HTML 写 TS, 但它需要搞指令来完成那些 TS 做的事儿. 比如 *ngIf *ngFor
这算是一个 trade-off. 尽可能不要那么乱, 但也限制了一些灵活.
Atom 流派
从上面的开发过程, 我们可以看到绝大部分的情况. CSS 的职责是作为 HTML 的 makeup.
CSS 的 selector 就是一个为了 connect 而诞生的东西.
如果我们直接把 CSS 写进去 HTML 那么就不需要这些 connect 了.
Inline CSS
inline CSS 是一个解决方案, 它几乎就是把 CSS 的 Style "搬" 到了 HTML 而已.
这个价值不够大.
Atom Class
把每一个小小的 Styling 变成一个 class
class="py-3" 相等于 padding-block: var(--spacing-3)
这个做法比 inline 好了不少.
有一点点像是重新定义了 CSS 语法.
它的特点是 shorthand key/value,
padding-block 很长, 写成 py 就好.
var(--spacing-3) 很长, 改成 -3 就好了.
优缺点, 有点是写的很爽. 短嘛.
缺点是, 读的时候有点累, 短嘛.
经过一轮升华, 价值大了一些
Tree Shaking
CSS 一直写就会越来越大, 如果把它变成 Atom Class 那么你增加的就不是 CSS 而只是 HTML 上的 class name 而已了.
通过 pre-compile 技术可以把没有用到的 class 排除. 这样就可以尽可能的少 Style 了.
价值多提升了一点点
Pre-compile = whatever
TypeSciprt 是典型的 pre-compile 神器. 任何可以 pre-compile 的东西都可以重新定义一个写法.
这就好像高级语言最终会生成低级语言一样. 高级语言的好处就是开发快, 维护方便.
Tailwind CSS 也是类似的东西. 代价就是学习成本和局限性而已.
对 CSS Utility Classes and "Separation of Concerns" 的整理
这篇文章写了很多重点. 这里做一个整理
Phase 1: "Semantic" CSS
这个是 selector 流派.
开发步骤是
1. 写 HTML, 定义 class name 依据内容 (不包含 styling)
2. 写 CSS, selector + styling
CSS 的结构会和 HTML 高度一致. selector 利用 descendant / child selector 方式避免撞名字.
Phase 2: Decoupling styles from structure
Phase 1 的问题在于严重依赖结构, 以至于 HTML 稍微改动一下, CSS 也必须修改.
我个人是觉得还好. 就一起改呗. 但是有些人认为不太好. 于是他加入了 BEM
BEM 会把 CSS 结构 "打平". 不再有嵌套, 也不用 descendant / child selector. 改用长长的 class name 做 select.
由于强制结构只有一层, 所以 HTML 和 CSS 的结构偶尔就没有了.
Dealing with similar components
上面的方式还不错了. 但是遇到类似的组件就完了.
作者提了 3 个方案
1. duplicate style (这个是开玩笑的)
2. @extend, @include
3. 抽象 class 这个是大部分 library 封装的方式 (可以参考 Swiper.js 的封装和调用)
到这里就是一个关键的转捩点了. 之前都是先有 HTML 然后 CSS makeup style (HTML 在前, 它不需要 "认识" CSS, 相反 CSS 在后需要 "认识" HTML)
但现在 CSS Style 要被复用了. 对于第二个 HTML 来说, CSS 反而在前, HTML 在后了.
下面这段讲出了重点
"Separation of concerns" is a straw man
HTML 和 CSS 的关系是很密切的. 强行分开它们是不正确的思路.
反而要理解它们的依赖关系.
1. CSS that depends on HTML
这个就是上面提到的, 先写 HTML 然后 CSS makeup HTML
2. HTML that depends on CSS
这个就是上面提到的, CSS 复用, 先 CSS 然后 HTML 配合.
不同的人会选择不同的路线.
Phase 3: Content-agnostic CSS components
作者倾向于第 2 条路, 让 CSS 复用.
后来他意识到, 组件越完整越难被复用.
于是就加入了 atom css 的概念. 后续的我就不写了, 大致上就是加入了更多其它 "价值" 推出了 Tailwind CSS.
对 Youtube – 7 ways to deal with CSS 的整理
同时参考了: CSS 模块化方案探讨(BEM、OOCSS、CSS Modules、CSS-in-JS ...)
1. pure css (selector 流派), 问题撞名字
2. BEM 引入 namespace 名字就不撞了.
3. CSS Modules 用 pre-compile 自动做 namespace
4. CSS in JS 用 JS 来写 CSS. React 用 JS 写 HTML 后得到了启发, 也可以用 JS 去写 CSS 这样甚至取代了 Sass 的作用.
5. styled-components 是 CSS in JS 的实现库.
6. Tailwind CSS atom CSS 的框架. 同时它也有取代 Sass 部分能力的能力.
总结
方案真的是好多啊. 我个人的经验是这样的.
1. Angular
Angular 自带 Component styles, 所以不需要搞 BEM, CSS Modules 那套
它是用 Sass 的, 所以也不搞 CSS in JS 那套.
至于你要不要用 Tailwind CSS 这个倒是可以考虑的.
2. React / Vue
据说 CSS in JS 就有十几个 library 在做. 这就是生态繁荣吧. php 框架也是有几十个.
如果喜欢做选择, 那么可以都尝试看看
3. ASP.NET Core 传统 way
Sass + BEM 或者 only Tailwind CSS 也是不错的做法.
我目前的做法 (ASP.NET Core 项目):
我目前的做法是 Phase 1: "Semantic" CSS
步骤是这样的.
1. 写 HTML
2. 写 CSS class (CSS 结构和 HTML 完全一致, 只是添加了 class 命名)
3. class 命名没有用 BEM, 用的是 RSCSS Descendant / Child selector 的方式, 加一点自己的规范, 防止撞名的同时尽量确保 class name 短 (我不能接受 class name 长长...)
4. 遇到复用的 Styling 用 "或者" selector 和 @extend @include 就解决.
5. 遇到组件复用的话, 会把 HTML, CSS, JS 做成组件. 类似 Swiper library 加上 Razor 的 View Component.