虽然开发工具早已经从 preprocessor 进化到了 styled component 甚至是 functional css,但在我看来新的工具并没有让我们的样式代码写的更好,只是更快——也可能会让代码坏的更快。工具的繁荣并没有让那些导致代码难以维护的根本问题烟消云散,而是更易让我们对其视而不见。这篇文章旨在回答一个问题:为什么样式代码难以写对,它的陷阱究竟在哪里?
如果一本正经的聊架构,套路多半是按照某些重要的特征依次展开讲解。但这些所谓的重要特征其实在编程领域中是放之四海而皆准的,例如“扩展性”、“可复用”、“可维护性”等等,按这种思路聊,空谈大于应用。所以我们不如通过解决某个具体的样式问题,来审视样式代码应该如何编写和组织
下图是一个非常简单的 popup 组件,我们会以它的样式开发过程串起整篇的内容。
我们首先以一种简单粗暴的方式来实现它,直觉上看,蓝狮注册开户实现这个 popup 只需要三个元素即可:div 是最外面的容器,h1 用于包裹 “Success” 文案,button 用来实现按钮
SuccessOK
我不会完整的写出它的完整样式,只大概列出其中一些关键属性
.popup {
display: flex;
justify-content: space-around;
padding: 20px;
width: 200px;
height: 200px;
div {
margin: 10px;
font-size: 24px;
}
button {
background: orange;
font-size: 16px;
margin: 10px;
}
}
第一版实现即完成了。目前看来并没有什么不妥。
问题不在于实现而是在于维护。接下来我就以一些常见的实际需求变更来看看上面的代码存在怎样的问题。
对 dom 元素的依赖
假设现在需要在“Success”下方新增一个元素用于展示成功的具体信息
想当然的我们需要新增一个 div 标签。但如果这样的话上面样式中的 .popup div 样式就会同时对这两个 div 产生同样的效果,这并不是我们希望的,很明显这两个元素的样式是不同的。OK,如果你坚持使用标签作为选择器的话,你可以使用伪类选择器 nth-child 来区分样式:
.popup {
div:nth-child(1) {
margin: 10px;
font-size: 24px;
}
div:nth-child(2) {
margin: 5px;
font-size: 16px;
}
但如果某一天你认为”Success”应该使用 h1 而非 div 封装更为恰当的话,那么修改的成本则是:
将 div 改为 h1,
将 div:nth-child(1) 样式改为 h1 所属,
将 div:nth-child(2) 还原为 div 样式
但如果你一开始就能给 button 和 div 一个确切的 class 名称,那么当你修改 DOM 元素时也仅仅需要修改 DOM 元素,而无需修改样式文件了
上面举得这个例子是水平拓展的情况,也就是说我在某一元素的同一级新增一个元素。纵向拓展也会出现同样的问题,你可以完全想象的出类似于这样的选择器:
.popup div > div > h1 > span {
}
.popup {
div {
div {
span {}
}
}
}
无论是上面代码中的哪一种情况,样式是否生效都极度依赖于 DOM 结构。在一连串的 DOM 标签的层级关系中,哪怕只有一个元素出现了问题(可能是元素标签类型发生了修改,还有可能是在它之上新增了一个元素)都会导致样式大面积失效。同时这样的做法也会让你复用样式难上加难,如果你希望复用 .popup div > div > h1 > 的样式,你不得不将 DOM 结构也拷贝到想要复用的地方。
所以这里我们至少能得出一个结论:CSS 不应该过分的依赖 html 结构
而之所以加上“过分”二字,是因为样式完全无法脱离结构独立存在,例如 .popup .title .icon 这样的的依赖关系背后就暗示了 HTML 结构的大致轮廓。
所以我们可以继续将上面的原则稍作更正:CSS 应该拥有对 HTML 的最小知识。理想情况下一个 .button 样式无论应用在任何元素上看上去都应该像同一个立体的可点击按钮。
父元素依赖
上一节中我们开发完毕的组件通常会在页面上被多处引用,但总存在个别场景需要你对组件稍作修改才得以适配。假设有一个需求是希望把这个 popup 应用在他们的移动端网站上,但为了适配动设备,某些元素的有关尺寸例如长宽内外边距等都要缩小,你会怎么实现?
我见过的 90% 的解决方案都是以添加父元素的依赖进行实现,也就是判断该组件是否在某个特定的 class 下,如果是的话则修改样式:
body.mobile {
.popup {
padding: 10px;
width: 100px;
height: 100px;
}
}
但如果此时你需要给平板设备添加一个新的样式,我猜你可能会再添加一个 body.tablet { .popup {} } 代码。又如果移动端网站有两处需要使用 popup ,那么你的代码很最终会变成这样:
body.mobile {
.sidebar {
.popup
}
.content {
.popup
}
}
这样的代码依然是难以复用的。如果某位开发者看到了移动端网站 popup 打开的样式很喜欢,然后想移植到另一处,那么单纯引入 popup 组件是不够的,他还需要找到真正的生效的代码,将样式和 DOM 层级都复制粘贴过去。
在一个组件自身已经拥有样式的情况下,过分的依赖父组件间接的调整样式,是一种 case by case 的编码行为,本质上这架空了 popup 自带样式。假设 popup 自带 box-shadow 的样式属性,但在有的用例里,box-shadow 可能会被加重,而在有的用例里,box-shadow 又可能会消失,那么它自带的 box-shadow 根部本就没有意义了,因为它永远不会生效。
架空违背了“最小惊讶原则”,给后续的维护者带来了“惊喜”。如果此时 popup 的设计稿发生了修改,阴影需要减少,则修改它自身的样式是不会生效的,或者说无法在每一处生效。而至于还有哪些地方无法生效,为什么它们无法生效,维护者并不知道,他同样需要 case by case 的去查看代码。这么做无疑增加了修改代码的成本.
解决这个问题并不像解决 DOM 依赖问题那么简单,需要我们多管齐下。
样式角色的分离
想提高代码的可维护性,分离关注点永远是屡试不爽的手段。纵观现有的各类组织样式的方法论,比如 SMASS 或者是 ITCSS,对样式进行适当的角色划分是它们的核心思想之一。
我们以一个完整的 popup 样式为例:
.popup {
width: 100px;
height: 30px;
background: blue;
color: white;
border: 1px solid gary;
display: flex;
justify-content: center;
}
在这一组样式中,我们看到
有与布局相关的 width, height
与视觉样式相关的 background, color
自身的布局样式 flex
其他样式比如 border
根据这些特点和常见的规范,可以考虑从下面几个维度对样式进行分离:
布局(Layout)和尺寸(size) : 一个组件在不同的父组件下拥有不同的尺寸是再正常不过的事情。与其定义一个被架空随时会被覆盖的尺寸,不如将布局的工作交由专职的组件处理。反过来说,该组件自生并不拥有尺寸,例如它可以选择总是以 100% 的宽和高充满包裹它的容器。
从表面上看,这种行为只是将样式(尺寸)从一个组件转移到另一个组件(容器)上,但却从根本上解决了我们上面提到的父元素依赖的困恼。任何想使用 popup 的其他组件,蓝狮注册登陆不用再设法关心 popup 组件的尺寸是如何实现的,它只需要关自己。
进一步从深层次上说,它消灭了 依赖 。你可能没有注意到,flex 布局的样式配置遵循的就是这种模式:当你想让你孩子元素按照某种规则布局的话,你只需要修改父元素和 flex 布局样式属性即可,完全不用再在孩子元素的样式上做出修改。
我个人认为另一个反模式的例子是 text-overflow: ellipsis 属性,单一的该样式属性是不足以自动省略容器内的文字,容器还需要满足 1) 宽度必须是 px 像素为单位 2) 元素必须拥有 overflow:hidden 和 white-space:nowrap 两组样式。也就是说当你想实现 A 功能时,必须依赖 B 和 C 功能的实现。
0 Comments