CSS_0
时间:2023-03-18 01:09:41
科技观察
CleanArchitecture作者|李光义在列举技术进步的代价时,弗洛伊德所遵循的路线让人感到压抑。他同意Tamms的评论,即我们的发明只是手段的改进,而没有目的的改进。——NeilPostman《技术垄断》虽然开发工具已经从预处理器进化到样式化组件甚至函数式css,但在我看来,新的工具并没有让我们的样式代码变得更好,而是更快——也可能让代码破坏得更快.工具的繁荣并没有使代码难以维护的底层问题消失,反而更容易被忽视。本文旨在回答一个问题:为什么样式代码这么难写正确,它的陷阱在哪里?如果采用严肃的聊天结构,大部分套路会根据某些重要特征依次进行讲解。但这些所谓的重要特性在编程领域其实是普遍存在的,比如“可扩展性”、“可重用性”、“可维护性”等等。按照这种思路,空谈大于应用。所以我们不妨通过解决一个具体的样式问题来考察样式代码应该如何编写和组织。下图是一个非常简单的弹窗组件,我们将通过它的样式开发流程将整个内容串起来。我们先简单粗暴的实现一下。直观上,实现这个popup只需要三个元素:div是最外层的容器,h1用来包裹“Success”文案,button用来实现按钮:Success
我就不把它的完整样式写出来,只列出一些关键属性:.弹出{显示:flex;证明内容:空间周围;填充:20px;宽度:200px;高度:200px;div{边距:10px;字体大小:24px;}按钮{背景:橙色;字体大小:16px;margin:10px;}}第一个实现完成。到目前为止似乎没有任何问题。问题不在于实施,而在于维护。接下来,我将通过一些常见的实际需求改动,看看上面的代码存在哪些问题。对DOM元素的依赖假设现在我们需要在“Success”下添加一个元素来显示成功的具体信息。当然我们需要添加一个div标签。但是如果这样的话,上面样式中的.popupdiv样式会同时对两个div产生相同的效果,这不是我们想要的。很明显,这两个元素的风格是不一样的。OK,如果你坚持使用标签作为选择器,可以使用伪类选择器nth-child来区分样式:.popup{div:nth-child(1){margin:10px;font-size:24px;}div:nth-child(2){margin:5px;font-size:16px;}但是如果哪天你觉得《成功》应该用h1而不是div包装更合适,那么修改的代价就是:把div改成forh1,改div:nth-child(1)style到h1所属的位置,将div:nth-child(2)还原为div样式,但是如果你可以首先给button和div一个确切的类名,那么当你修改DOM元素时,你只需要修改DOM元素,无需修改样式文件。上面的例子是水平扩展的情况,也就是说我在一个元素的同一层级添加一个元素。垂直展开也会出现同样的问题,你完全可以想象一个类似这样的选择器:.popupdiv>div>h1>span{}.popup{div{div{span{}}}}是否是上面代码中的任一个case,样式是否生效很大程度上取决于DOM结构。在一系列DOM标签的层级关系中,即使只有一个元素出现问题(可能修改了元素标签类型,或者在其上方添加了新元素),也会造成大面积的失败的风格。同时,这种做法也会让你更难复用样式。如果要复用.popupdiv>div>h1>样式,则必须将DOM结构复制到要复用的地方。所以到这里我们至少可以得出一个结论:CSS不应该过多的依赖HTML的结构。之所以加“过多”二字,是因为样式不能脱离结构独立存在,比如.popup.title.icon。这些关系背后隐含了HTML结构的粗略轮廓。所以我们可以继续应用上面的原则并稍微修正一下:CSS应该对HTML有最少的了解。理想情况下,无论应用于什么元素,.button样式都应该看起来像相同的三维可点击按钮。父元素依赖我们上一节开发的组件,通常会在页面的多个地方被引用,但总会有个别场景需要你对组件进行微调以适配。假设他们的移动网站需要应用这个popup,但是为了适配移动设备,必须缩小某些元素的相关尺寸,比如长、宽、内外边距等。你将如何实施它?我看到的解决方案90%都是通过添加父元素依赖来实现的,即判断组件是否在特定类下,如果是则修改样式:body.mobile{.popup{padding:10px;宽度:100px;height:100px;}}但是如果此时你需要给平板设备添加一个新的样式,我猜你可能会添加另一个body.tablet{.popup{}}代码。而如果移动网站有两个地方需要用到popup,那么你的代码最终会变成这样:body.mobile{.sidebar{.popup}.content{.popup}}这样的代码还是很难复用。如果开发者在移动端看到弹窗样式,想移植到别的地方,仅仅引入弹窗组件是不够的。他还需要找到真正有效的代码来集成样式和DOM层次结构。复制粘贴过去。当一个组件已经有自己的样式时,过度依赖父组件间接调整样式是一种casebycase的编码行为,本质上是清空了popup自身的样式。假设popup有自己的box-shadow样式属性,但是在某些用例中,box-shadow可能会被强调,而在某些用例中,box-shadow可能会消失,那么它自己的box-shadowroot是没有意义的,因为它永远不会工作。overhead违反了“最少惊喜原则”,给后续的维护者带来了“惊喜”。如果此时修改了popup的设计稿,需要缩小阴影,那么修改自己的样式是不会生效的,或者是处处都不会生效。至于还有什么不能生效,为什么不能生效,维护者就不知道了,还需要具体看代码。这样做无疑会增加修改代码的成本。解决这个问题并不像解决DOM依赖问题那么简单,我们需要多管齐下。样式角色分离想要提高代码的可维护性,关注点分离永远是一个久经考验的方法。纵观现有的各种组织风格的方法论,如SMASS或ITCSS,对风格进行适当的角色划分是其核心思想之一。我们以一个完整的弹窗样式为例:.popup{width:100px;高度:30px;背景:蓝色;白颜色;边框:1px实心加里;显示:弹性;justify-content:center;}在这组样式中,我们看到有布局相关的宽度、高度和视觉样式相关的背景、颜色和自身的布局样式flexborder等其他样式,根据这些特点和通用specifications,我们可以考虑从以下几个维度分离样式:布局(Layout)和大小(size)一个组件在不同的父组件下有不同的大小是很正常的。与其定义一个开销很大、随时会被覆盖的大小,不如把布局的工作交给一个专职的组件。相反,组件本身没有尺寸,例如它可以选择始终以100%的宽度和高度填充其包装容器。从表面上看,这种行为只是将样式(大小)从一个组件转移到另一个组件(容器),但它从根本上解决了我们上面提到的父元素依赖问题。任何其他想要使用弹出窗口的组件都不必尝试关心弹出窗口的大小是如何实现的,它只需要关闭自己即可。在更深层次上,它消除了依赖性。你可能没有注意到flex布局的样式配置遵循这样的模式:当你想让你的子元素按照一定的规则布局时,你只需要修改父元素和flex布局的样式属性,而不必需要使用对子元素的样式进行更改。我个人认为反模式的另一个例子是text-overflow:ellipsis属性。单个样式属性不足以自动忽略容器中的文本。容器还需要满足1)宽度必须以px像素为单位2)元素必须有overflow:hidden和white-space:nowrap两套样式。也就是说,当你要实现A的功能时,必须依赖于B和C的功能实现。至于布局功能元素是与父元素相同的元素还是独立的元素,我更喜欢后者。毕竟几个标记代码不会给我们增加太多的负担,但是明确的职责分工可以给我们带来日后的维护。来了很多方便。在这个前提下,给popup添加任何布局样式,其实都意味着你添加了隐式依赖,因为你实际上是在暗示它在父容器下的margin值看起来刚刚好。ModifierSOLID原则中的open-closed告诉我们关闭修改,扩展开发的样式代码也是如此。通常我们不仅仅需要一个单一样式的按钮,我们可能还需要一个红底白字的错误样式按钮,以及一个黄底白字警告样式的按钮。这个用例的通用解决方案不是创建N种不同的按钮样式,例如primary-button、error-button(所以肯定有很多通用按钮代码),而是在一个按钮样式的基础上,通过提供样式“修改”类以达到最终目的。比如基本按钮的类名是button,如果你想让它有警告样式,只需要同时使用error的类名。
从本质上讲,这也是一种关注点分离,只是从这个角度来说,它关心的是“变化”和“不变性”。我们将所有“变量”转移到“装饰”类中。但是这种方案在实施的时候会遇到很多问题。首先是装饰类的设计。比如我在定义error、primary、warning这些装饰类的时候,哪些样式属性我可以重写,哪些不可以,这些必须事先约定好。否则,当有人写出错误的样式时,他可能会盲目地覆盖原来按钮上的样式,直到看起来满意为止。它依赖于抽象,但是糟糕的抽象比没有抽象更难维护。模块化随着组件模块化的普遍趋势,样式模块化似乎是水到渠成的事情。但如果你放眼长远,模块化并不局限于将风格逼到墙角,将它们封装起来进行集中管理。从上面的例子不难看出,通过在样式中借用父元素所依赖的特性,可以很容易地打破这种封装。组件不是封装样式的唯一单位。在一个网站中,可能还会有base、reset等全局或方面的样式属性。我理想中的模块化样式应该能够轻松实现以下目标:控制样式影响的方向性:比如全局样式可以影响组件,但组件不能影响整个世界;样式模块之间的隔离和污染:虽然A组件是B组件的子元素,但是B组件的样式不会影响A的样式。说明这两点最好的例子就是业界通用的字体大小适配解决方案响应式开发。比如下面这个组件的html结构:
parenthello