当前位置: 首页 > 科技观察

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
中我们会设置的样式:祖先组件的字体相对于根元素html发生变化,所以使用rem单位;parent和child的字体单位需要相对于组件的基础字体(即祖先)改变,所以使用em单位。.ancestor{font-size:1rem;}.parent{font-size:1.5em;}.child{font-size:2em;}这样当我们需要根据设备调整字体大小时,只需要调整根元素html字体大小,然后页面上的其他元素将自行调整。而如果我们只想调整局部样式,只需要调整.ancestor的字体大小,不影响其他元素。不难看出,样式很难写对是因为太容易影响其他组件,太容易被其他组件影响。大多数人遇到的问题是:我以为我修改的是A组件的样式,但无形中影响了B组件;组件A同时受到几套样式的影响,无论单独修改谁都达不到最终的效果。这个问题的解决方案早就为人所知,那就是样式的隔离。例如,在Angular中,它是通过给元素添加随机属性和给样式附加属性选择器来实现的。例如,您同时创建一个page-title组件和一个section-title组件。它们都有h1元素的样式,但是在Thecssyouseeaftercompiledare:h1[_ngcontent-kkb-c18]{background:yellow;}h1[_ngcontent-kkb-c19]{background:blue;}这样所有h1元素样式不会相互影响。实现预处理器中的问题无论您主观上多么想避免上述所有问题,都应为样式提供一个干净整洁的结构。在实施的过程中,我们还是会不小心落入工具的陷阱。回到我们上面提到的弹窗样式:.popup{width:100px;高度:30px;背景:蓝色;color:white;}如果你发现{background:blue;白颜色;}作为一种常用样式频繁出现,希望复用它,在使用Sass编程的前提下,显然这时候你有两个选择:@mixin或者@extend。如果使用mixin,代码如下:@mixincommon{background:blue;颜色:白色;}.popup{@includecommon;}如果使用extend:.common{background:blue;颜色:白色;}.popup{@extend.common;第一个问题是无论选择哪种模式,你都很难分辨开发者是故意依赖抽象还是依赖实现。我们可以将@mixincommon和.common理解为一种抽象封装,但很有可能后续的消费者只是想复用背景和颜色。一旦出现这种情况,公共模块就变得难以修改,因为对任何属性的任何修改都会影响未知模块。在SASS中,虽然我们可以在类名中加上参数,作为参数传递给对方,但是和我们实际编程中的变量、函数是不一样的:对于JavaScript中的函数,我们往往只关心它的输入输出,只是定义一个函数不会影响程序的结果。而当你定义样式类的时候,它可能已经对页面产生了影响,里面的每一个属性都会产生影响。如果你听说过“组合胜于继承”,相信你会对这一点有更深的体会。大家可以回忆一下继承体系中的副作用,比如继承打破了超类的封装,子类不能减少超类的接口等等,类似的影子在SASS中的这种复用关系中都能找到。extend比mixin更危险的是它打破了我们通常组织模块的方式。比如已经有一个pagepage,它有一组page-title样式:.page{.page-title{.icon{width:10px;}.label{宽度:100px;}}}现在card-title要通过extend重用:.card-title{@extend.page-title;}那么编译出来的结果会很奇怪:.page.page-title.icon,.page.card-title.icon{width:10px;}.page.page-title.label,.page.card-title.label{width:100px;}即使您从未听说过BEM,您的编程经验也应该告诉您页面和卡片的样式应该属于不同的模块。但实际上,编译后的结果更像是优先复用,从横截面上强行将两者耦合在一起。而如果你尝试将常见的标题样式抽象成一个mixin,然后在page-title和card-title中重用它:@mixintitle{.icon{width:10px;}.label{宽度:100px;}}.page{.page-title{@includetitle}}.card-title{@includetitle}编译结果如下:.page.page-title.icon{width:10px;}.page.page-title.label{width:100px;}.card-title.icon{width:10px;}.card-title.label{width:100px;}显然,page和card的风格更加鲜明。必要的恶如果你问我是否会遵守我上面写的每一个原则,我的回答是否定的。在实际开发中,我倾向于以方便性换取可维护性。编程领域唯一不变的就是变化本身。无论你的面向对象设计多么准确,在敲击键盘之前拆分组件多么恰当,任何业务变更都可能导致你所有的设计被推翻并重新开始。因此,为了保证代码能够准确反馈业务知识的合理性,我们需要不时地重新设计代码。你可以想象,整个过程需要重新审视架构,从头阅读理解代码,修改后验证。执行这一系列步骤需要大量成本,这还不包括所涉及的反复试验,以及因重构而浪费的添加新功能的机会。更重要的是,成本是有的,收益却不明显。如果你的样式代码是基于设计系统的,那么你的改动成本会更高。因为你更不可能站在个人的角度去随心所欲地去改代码,而是要用整个产品的设计语言来衡量从上到下修改的合理性。另一个更实际的问题是代码从不由个人维护。当这套理论在团队内部没有达成共识,或者当大家只停留在理论层面上理解而在实践中并不关心时,少数人的精心努力最终会付诸东流。理想情况下,代码应该最大程度地摒弃“人”的因素,成为流水线上的工业化产品。所以当我发现一个只需要人们阅读几十页最佳实践相关文档就可以写出符合官方标准的好代码的框架时,那么好代码出现在实际工作中的概率基本为0——在规范输出代码中事实上,一个有效的eslint规则比十页文档更强大。本章所描述的原则属于后者。但是css代码写的乱七八糟怎么办?产品坏了是肯定的,但是相对于其他bug,有趣的是发现样式问题的概率比脚本高,所见即所得;造成的损害比脚本函数小,产品在问题下仍然可以使用;修复问题的成本低,甚至不需要阅读源码就可以有针对性的快速修复。基于以上三点,再考虑到目前技术栈复杂,学习成本高,脚本开发工作量大,交付压力大,样式架构的正确性自然是被牺牲的.最后,我想重申,我不鼓励这种行为。这只是屈服于现实压力的一种可能。如果您在一个项目中拥有大量资源并且有人致力于把事情做好,那也很好。FunctionalCSS在我看来,还有一种实践是在上述体系之外的,比如tailwind和tachyons。之所以称为“函数式”样式,是因为这些框架不提供组件化和语义化的样式,如.card、.btn,而是提供“实用类”,如.overflow-auto,.box-content,它们就像函数式编程中没有副作用的纯函数。当你需要给你的元素添加样式时,只需要给这个元素添加对应的类名:之所以这个做法是免费的它在上述系统之外,因为它打破了我上面所说的前提:样式和DOM结构之间存在依赖关系。在这种编程模式下,由于不再存在“级联”关系,每个元素的样式都是独立的,互不影响。这样一来,这个模式简直就是天堂,本文提到的所有问题都可以避免:父元素依赖、角色耦合、预处理器中纠结的重用。但是仔细想想,这种做法是不是很像内联样式呢?使用内联样式也可以解决我们上面提到的所有问题。我们回到起点了吗?我之所以不给出除上述之外的进一步建议或反对意见,一方面是因为这种做法具有很大的争议性。另一方面,我缺乏使用此类框架的经验。这里判断体验的标准不是“是否用过”,而是“是否长期投入大型多人协作项目”——关键词“长期”、“多”-人”和“大规模”非常重要。因为我们在做技术选型的时候,更多的是考虑和现有项目的契合度,团队的适配成本,评估长期来看能不能给我们带来巨大的收益,能不能抵消更换它的成本。这些经验是我所缺乏的。

猜你喜欢