likes
comments
collection
share

前端架构设计之CSS篇​(3万字修订版)

作者站长头像
站长
· 阅读数 18

引言

前端架构设计之CSS篇​(3万字修订版) 前端工程师主要使用JavaScript、CSS、HTML这三种语言,其中CSS最简单的部分,CSS与其编程语言不同,她没有循环、逻辑和其他概念,它只是声明式的语言,因此,CSS很容易上手。也许正是因为如此,它才获得了简单的美誉。在 "不复杂 "的意义上,它是简单的,但这并不意味着它很容易。把 "简单 "误认为是 “容易”,只会让人心痛”。

CSS 主要面临“CSS Hug file“难题,就是随着项目逐渐增大,其维护成本激增。

前端架构设计之CSS篇​(3万字修订版)

而且随着前端的发展,前端的涉及领域日益增长,巨石应用越来越多,在年度CSS调查中,大家对CSS的满意度确实大幅度下降。

前端架构设计之CSS篇​(3万字修订版)

小编从前端工作也有一些年头了,最近也一直打算对自己的所学进行梳理沉淀,将软件开发过程中优秀的方法、理论、实践进行梳理,形成一套完整的实践架构。

本文主要讲述 CSS 领域中如何从前端架构角度去设计代码工程,包括代码规范、命名规范、编译套件、自动补全套件等等,力求从实际的软件开发角度去分解这个过程,搭建高质量的前端工程,保持全产品周期的高效率开发和维护,进而解决上面的难题。

本文分为4个 part。

  • CSS 标准化过程 主要介绍 CSS 发展史上重要的三次版本发布和其内在驱动逻辑,不感兴趣的朋友请直接跳到感兴趣的章节。
  • CSS 设计范式 主要介绍代码编写 CSS 的组织管理规范,包括命名规范、组织方式、可扩展设计。主要是帮助大家选择合适的设计范式减少命名冲突、可读、可复用,用我自己的体验来说就是减少记忆负担,无脑开发不出错。
  • CSS 架构设计 主要介绍不同 CSS 设计范式下的具体实践。业内比较出名的有原子化 CSS、CSS 预处理器(Less、PostCss)、CSS in JS等架构设计和实践,在此章节会重点介绍我在这些架构上的使用经验,也介绍我自己梳理出来的最佳实践,仅供大家参考。
  • CSS 百宝箱 顾名思义就是将 CSS 领域内其他优秀的设计和应用进行汇总,组成一个丰富的“百宝箱”,给大家开箱即用,当然肯定没有一百这么多,但是我会逐渐补充完善它。

通过这四个章节,给大家介绍下完整的 CSS 领域的生态和工程化实践,希望对处于前端学习瓶颈的朋友有所启发。

CSS 标准化过程

这个章节会比较枯燥一点,但是对初入前端领域的同学还是有必要简单介绍下历史背景,开头我先用简单的语言给大家总结下核心内容,资深的同学可以跳过这一部分。 CSS 是什么?

前端架构设计之CSS篇​(3万字修订版)

  • C(Cascade):指的是层叠,在CSS中编写样式规则是一个一个排列下来,可以简单的理解为先后顺序
  • S(Style) : 第一个S,它指的是样式规则,比如 body{color: red}
  • S(Sheets) :第二个S,它指的是样式表,就是我们常说的 .css 文件,CSS的代码会放置在样式表里

CSS(层叠样式表)是一种用于网页布局与设计的标记语言,通过对网页元素进行样式描述和定位控制来实现网页的美化、排版和交互效果。社区这些年一直在不断的迭代设计,丰富完整 CSS 生态,得益于一套严谨的规范管理机制。

第一个W3C网页。 前端架构设计之CSS篇​(3万字修订版)

刀耕火种的CSS1

前端架构设计之CSS篇​(3万字修订版)

渐入佳境CSS2

前端架构设计之CSS篇​(3万字修订版)

重视交互体验的CSS3,更容易地创建更复杂、更丰富的效果和交互。

前端架构设计之CSS篇​(3万字修订版)

CSS标准化的过程可以分为以下几个阶段:

  1. 工作草案(WD, Working Draft):由W3C联盟的工作组(Working Group)提出初始的规范方案,作为一个可进行讨论和验证的版本,通过不断迭代修改并达成共识。
  2. 建议推荐(CR, Candidate Recommendation):目前被大部分标准委员会人员认同的草案阶段,已经比较接近最终的规范。在进入CR阶段之前,需先对性能、兼容性等进行适当测试、考察,以确保标准实现的情形足够广泛。
  3. 建议确定(PR, Proposed Recommendation):已经是一份相对稳定的规范,提供给W3C理事会进行投票正式批准,同时也有机会向外公开征求意见来优化这个候选规范。
  4. 正式规范(REC, Recommendation):经过上述各个阶段的评审、反复打磨之后,最终被制定为一个正式的标准规范。

在每个阶段都要经过各类浏览器或社区的实验测试,通过支持的版本号来为这份规范提供资料,并在这个过程中更新及修改规范,至此该标准的定义才真正意义上开始得到普及和使用。

前端架构设计之CSS篇​(3万字修订版)

目前CSS迭代了3个大版本,其中CSS1是第一个被制定为正式标准的CSS版本,于1996年12月公布。

CSS1

CSS1对网页设计和开发起到了重要的推动作用,它将各种HTML元素、文本、颜色和布局等方面进行了规范、分类,并引入了Cascading Style Sheets的基本概念。

CSS1主要包含以下核心功能:

  1. 字体:定义字体类型、大小、样式、粗细以及颜色等。
  2. 背景:定义背景颜色、以图方式或背景位置等相关属性。
  3. 文本:添加文本装饰、调整行高和文本间隔等。
  4. 颜色:提供颜色设定,覆盖HTML早期颜色定义中的字面值。
  5. 大小定位:定义文本和布局元素的大小、位置、外边距和内边距等。

在CSS1中,使用了24个属性,如color、background-color、font-size和padding等等,为后来的CSS2、CSS3的发展奠定了基础。

CSS2

CSS2是第二个被制定为正式标准的CSS版本,于1998年5月公布。相对于CSS1,CSS2增加了更多的样式属性和选择器,并提供了更细致、更精确的控制元素显示的能力。

CSS2的主要特点可以归纳为以下几点:

  1. 定位与元素特征:新增了绝对/相对定位、浮动以及display属性等,并引入了:first-child、:hover、:active等伪类选择器来描述元素特征。
  2. 布局:增加了table-layout和position属性,使开发者能够更好地控制页面布局。
  3. 贴近文档结构:强调清零所有默认值,把不需要样式的HTML元素作为一个独立的模块去考虑。
  4. 字体:相较于CSS1,CSS2引入字体风格(font-style)、文字间距(letter-spacing)以及字符间距等新功能。
  5. 其它:还包括盒子模型(Box Model)的完善、文本处理与呈现的扩展、生成内容样式等等。

CSS3

CSS3是CSS的第三个版本,于1999年开始制定,并在2011年6月正式公布。与前两个版本相比,CSS3引入了更多的样式和选择器,让开发者可以更容易地创建更复杂、更丰富的效果和交互。

CSS3引进的一些新特性包括:

  1. 选择器:新增了属性选择器(attribute selectors)、伪类选择器(pseudo-class selectors)和伪元素选择器(pseudo-element selectors),增强了选择元素的能力。
  2. 盒模型:让CSS针对盒子模型进行更精确、更准确的计算。
  3. 边框和背景:新增了多重背景、圆角边框、边框阴影等。
  4. 文本效果:新增了文字阴影(text-shadow)、文字渐变、多列文本布局等。
  5. 转换与过渡:新增了2D/3D转换、过渡动画等样式属性,使得开发者可以实现更加生动的页面展示效果。
  6. 网格和弹性布局:新增了网格布局(以行和列的方式排列元素)和弹性盒布局(以父容器/子元素的关系为基础)等。

未来的 CSS 发展方向主要体现在以下几个方面:

  1. 更智能化的样式规则:未来的 CSS 将更加智能化,能够更好地识别 DOM 中元素之间的关系,并自动应用相应的样式。例如,可视化样式编辑器,可以通过图形界面操作选中元素,并直接生成对应的 CSS 代码。
  2. 支持更多的用户交互效果:CSS 的 user-select 属性已经允许开发人员控制哪些元素可以选择和拖动,未来还会支持更多用户交互效果,例如滚动、折叠、拖放等操作。
  3. 响应式设计优化:随着移动设备和其他非传统设备的增多,未来的 CSS 将继续优化响应式布局,在不同设备上以适当的方式呈现,以提高用户体验。

随着浏览器的性能和开发程度的提升,CSS 能够实现的功能也将逐渐提升。

CSS设计范式

CSS设计范式出现的初衷是为了解决Huge CSS file难题。随着项目逐渐变得更加庞大、更加复杂之后,就需要维护庞大的 CSS 文件,它们难以阅读、存在命名冲突、DOM映射困难,工程质量变得越来越脆弱,样式出现不可控的错误情况越来越多。

1、OOCSS

全称-Object Oriented CSS, 主要目的是获取页面中使用和重复的元素作为对象,并在必要的地方使用它们。

OOCSS 有两个主要原则

  • 将结构与皮肤分离
  • 将容器与内容分离

在早期的前端开发中,结构往往是指代“width, height, padding, margin , position”这些会影响布局的样式;皮肤则是指代“border, color, font, background”这些单纯的样式。原则一就是让结构和皮肤进行分离,看下下面的错误案例。

前端架构设计之CSS篇​(3万字修订版) box1和box2的都具有相同的皮肤设计,因此需要将皮肤分离出来。

前端架构设计之CSS篇​(3万字修订版) 相应的类名也要调整到对应层级的设计。

前端架构设计之CSS篇​(3万字修订版) 原则二则是希望减少使用子类选择器。下图中h2标签会在多处使用,子类选择器“.sidebar h2 { ... }”的使用就有悖于原则二。

前端架构设计之CSS篇​(3万字修订版) 应用 OOCSS 范式将 h2.sidebar-title 从容器 sidebar 中独立出来,就可以在多处复用,也不用重复这段 CSS。

前端架构设计之CSS篇​(3万字修订版)

2、SMACSS

SMACSS(Scalable and Modular Architecture for CSS)是一种面向模块化的CSS代码组织方法论。它将CSS代码按照功能和层次进行分割,使得代码易于维护、扩展和重用。

SMACSS基于五个核心概念:基础(Base)、布局(Layout)、模块(Module)、状态(State)和主题(Theme)。

  • 基础包括标准的HTML元素和基本的样式,如reset.css。
  • 布局关注的是页面的框架结构,比如定宽布局和流动布局等。
  • 模块则指可重复使用的独立单元,如头部、导航栏、侧边栏和按钮等。
  • 状态考虑到使用交互时产生的变化效果,比如焦点状态或者隐藏状态等。
  • 主题则指与设计风格相关的CSS代码,例如颜色、背景图案、字体大小等。

通过这些概念的选择性组合,设计者可以实现结构解构分离,让代码更为灵活。同时也遵循着一些约定俗成的样式书写规范,确保团队协作时代码的一致性和清晰度。

命名规则

  • 使用前三个层级名称作为类名的前缀: .module-xxx,例如属于布局的元素,采用 l- 作为前缀
  • 状态命名规则,采用is/not-state规则.l-inline.is-collapsed {}
  • 主题的命名和状态命名保持一致
/* Example Module */
.example { }

/* Callout Module */
.callout { }

/* Callout Module with State */
.callout.is-collapsed { }

/* Form field module */
.field { }

/* Inline layout  */
.l-inline { }

3、BEM - Blocks,Elements and Modifiers

BEM 由三个单词的首字母组成,代表样式的三个层级,这和 React 的容器组件、展示组件、状态分离模块(大多是组件的独立状态渲染)的组件层级是保持一致,也符合 CSS module 的需求,这也是 BEM 饱受欢迎的重要原因之一。

  • Blocks

独立的实体,例如header、container、menu、checkbox

  • Elements

无法独立的绑定在独立实体上的一部分,例如menu item、list item、checkbox caption、header title

  • Modifier

是 Blocks 和 Elements 的修饰符,例如 disabled, highlighted, checked, fixed, size big, color yellow,以--和Blocks 和 Elements 连接,以-连接单词。

一个典型案例:三种不同状态的按钮。

前端架构设计之CSS篇​(3万字修订版)

// html
<button class="button">
  Normal button
</button>
<button class="button button--state-success">
  Success button
</button>
<button class="button button--state-danger">
  Danger button
</button>
.button {
        display: inline-block;
        border-radius: 3px;
        padding: 7px 12px;
        border: 1px solid #D5D5D5;
        background-image: linear-gradient(#EEE, #DDD);
        font: 700 13px/18px Helvetica, arial;
}
.button--state-success {
        color: #FFF;
        background: #569E3D linear-gradient(#79D858, #569E3D) repeat-x;
        border-color: #4A993E;
}
.button--state-danger {
        color: #900;
}

从上面的例子,可以看出BEM具有以下优点:

  • 模块化 块样式永远不依赖于页面上的其他元素,因此永远不会遇到级联问题。 您还可以将已完成的项目中的块转移到新的项目中。
  • 可重用性 以不同的方式组合独立的块,并巧妙地重用它们,减少了你必须维护的CSS代码的数量。 有了一组适当的样式指南,您可以构建一个块库,使您的CSS超级有效。
  • 结构 BEM方法为您的CSS代码提供了一个简单易懂的坚固结构,其含义明确,且不会被误解。

4、SUITCSS

SUIT CSS(Style tools for UI components)是一种基于组件和命名约定的CSS框架。它提供了一套规范的类名和样式命名方式,旨在降低大型项目中CSS的复杂度和难度。

SUIT CSS的编写风格非常规范化,主要包括三个部分:

  1. 基础:SUIT CSS使用 normalize.css 来重置和标准化CSS属性,小编在项目中是经常见到的,不过当时不懂其中的奥妙。

  2. 组件:每个组件对应一个独立的CSS文件,并且包含唯一的类名,比如.snackbar、.modal等。

  3. 命名约定:SUIT CSS采用BEM风格(Block Element Modifier)的命名方式,可以通过类名完整地描述组件结构和状态。

例如,按钮组件的HTML可以这样写。

<button class="Button Button--primary">Submit</button>

其中,.Button代表按钮组件的Block,--primary是它的Modifier,用于明确其为主要按钮。

  1. 工具类:SUIT CSS 提供了一些工具类来快速实现一些常见的样式,比如清除浮动、隐藏元素、居中显示等。

总之,SUIT CSS的核心理念是将样式与功能进行解耦,并提供一种有效和一致的方法对CSS进行管理和扩展。

5、Atomic CSS

Atomic CSS是一种CSS代码设计规范,它采用简单的类名来表示样式,且每个类名只包含一个固定的CSS属性。由于其使用了更小、更具体的类名,因此被称为“原子”级别的CSS。

Atomic CSS 鼓励将整个页面视为各种组成单元——比如按钮、输入框、边距、内填充等——而不是按功能或部件对CSS进行组织,通过选择正确的原子类,来构建出完整的样式。

下面是颜色的样式提取案例:

.color-red {
  color: red;
}

.color-green {
  color: green;
}

在 Atomic CSS 中,则可以使用样式属性前缀或者缩写-状态的方式进行命名,在上面的例子就可以命名成.c-red、.c-green。

尽管 Atomic CSS 使用过多的类名,但它能够提高代码的重用性和灵活性,减少样式文件的代码量,也能够以语义化的命名规范,方便大家阅读代码即可理解页面的整体样式情况。当然缺点也很明显,会导致全局命名污染的缺陷。

到这里一共介绍了五种CSS设计范式,这些范式出现的初衷都是类似的,都是为了解决下面的问题,只是方法和方向不一样而已。

  • 减少选择器命名和样式的冲突
  • 清晰的 CSS 整体结构
  • 去除冗余代码,减少样式的体积
  • 可重复利用,组件化的 CSS
  • 提高 CSS 代码的可读性

最近大火的 ChatGPT 对这些设计范式的冲击还是比较大,其实这些范式的好坏最终就是落到两个指标上的,一是代码量;二是易读性。ChatGPT 可以快速生成大量的样式,也能通过关联分析快速聚合类名,命名也更加通用,压缩代码量,记忆负担也能大大减少。我认为 ChatGPT 的出现具有两面性,一方面大大解放了前端的生产力,另外一方面也对前端开发提出更大的挑战,要有足够的领域知识对 ChatGPT 的推荐进行优化调整,特别是设计范式的理解。

前端架构设计之CSS篇​(3万字修订版)

CSS 架构设计

CSS 架构设计则是希望设计和拟合一系列开发范式、开发套件、功能设计来满足产品对交互、视觉效果的要求,同时具有可维护、可扩展、高效率的工程特性。本章节会先介绍CSS领域内的常规范式和套件,然后给大家推荐一套本人比较喜欢的架构,以作参考。

按照软件生命周期来看,CSS可以触及代码编写、代码编译、运行三个阶段,每个阶段应用的框架结构也各不相同。 前端架构设计之CSS篇​(3万字修订版)

Atomic CSS

Atomic CSS is the approach to CSS architecture that favors small, single-purpose classes with names based on visual function.

译文:原子化 CSS 是一种 CSS 的架构方式,它倾向于小巧且用途单一的 class,并且会以视觉效果进行命名。

这种设计模式是从开发者实际使用的场景中延伸出来的,在前端开发过程中,样式的需要也就主要集中在margin,padding,flex,height,width等基础样式上,那为何不使用独立的缩写类,直接写到内联样表中,而要单独写个样式文件这样大费周章呢?时至今日我们在主流的网页中还能看到他们的身影。例如 GitHub 官网。

前端架构设计之CSS篇​(3万字修订版)

这里我比较喜欢使用 tailwindcss 框架,tailwindcss 内置了大量工具类名,理论上可以做到 100% 使用内置方案覆盖。

tailwindcss 的命名规范很统一,具有唯一性的样式属性会直接作为对应的类名,例如 block, absolute, flex, top-0, overflow-hidden, whitespace-nowrap, border, border-black 只看名称就能唯一确定属性,符合开发直觉。

  • block -> display: block
  • absolute -> position: absolute
  • m-auto -> margin: auto
  • p-auto -> padding: auto
  • mx-2 -> margin-left: 0.5rem; margin-right: 0.5rem

对于一些 css 通用名称,tailwindcss 提供了统一的规范。例如 left, right, top, bottom, 分别对应 l, r, t, b。left-right 对应 x, top-bottom 对应 y。所以产生了下面的类名:

  • ml-[2px] -> margin-left: 2px;
  • mr-[2px] -> margin-right: 2px;
  • mx-[2px] -> margin-left: 2px; margin-right: 2px;
  • pt-2 -> padding-top: 0.5rem;
  • pb-2 -> padding-bottom: 0.5rem;
  • border-r -> border-right-width: 1px;
  • border-b -> border-bottom-width: 1px;

所以如果想写一个上边框黑色,上下外边距 2px, 上内边距 10px,可以如下:

<div class="border-t border-black my-[2px] pt-[10px] "/>

Atomic CSS 架构也存在缺陷,行内样式不支持伪类、媒体查询、现代选择器,应用场景受限。而且随着前端组件模块化概念的兴起,催生了 CSS in JS 和 CSS modules 的概念,这种比较传统CSS编写方式显然难以满足需要了。

CSS 预处理器

CSS 预处理器是一个能让你通过预处理器自己独有的语法来生成 CSS 的程序。市面上有很多 CSS 预处理器可供选择,且绝大多数 CSS 预处理器会增加一些原生 CSS 不具备的特性,例如代码混合,嵌套选择器,继承选择器等。这些特性让 CSS 的结构更加具有可读性且易于维护。 —— 《MDN / CSS 预处理器》

在前端发展的早期,CSS开发者往往被各种浏览器兼容、命名冲突、全局作用域冲等突问题所折磨,迫切的需要一个工具来屏蔽这些底层差异,CSS 预处理器便应运而生了。预处理器提供了统一的语法,嵌套式CSS结构、基于类名hash的作用域概念、屏蔽浏览器差异等,对于不同的浏览器和同一种浏览器的不同版本实现了自动兼容,极大解放了前端生产力。

这里不得不提强大的 PostCSS 插件工具,它提供了一种方式用 JavaScript 代码来处理 CSS。它负责把 CSS 代码解析成抽象语法树结构(Abstract Syntax Tree,AST),再交由插件来进行处理。插件基于 CSS 代码的 AST 所能进行的操作是多种多样的,比如可以支持变量和混入(mixin),增加浏览器相关的声明前缀,或是把使用将来的 CSS 规范的样式规则转译(transpile)成当前的 CSS 规范支持的格式。需要注意的 PostCSS 会将 CSS 代码解析抽象成抽象语法树结构AST,因此也就有 Webpack、 Grunt、Gulp 编译平台进行继承,利用插件可以对 CSS 代码进行定制。

  • PostCSS:a tool for transforming CSS with JS plugins 原理:CSS -> AST -> Modify

下面列举下常见的 CSS 预处理器:

  • Less:Less.js/动态样式语言,双端支持,CSS-like
  • SASS:Ruby环境/动态样式语言,缩排语法/{}优化缩排,语法可编程性更好,也支持所有CSS语法
  • Stylus:Node项目CSS预处理器,相对于 SASS 其可编程性更加简洁

CSS

.conditional-example-1 {
  background-color: #000;
  color: #ddd;
}
.conditional-example-2 {
  background-color: #fff;
  color: #555;
}

Less

.mixin (@a) when (lightness(@a) >= 50%) {
  background-color: black;
}
.mixin (@a) when (lightness(@a) < 50%) {
  background-color: white;
}
.mixin (@a) {
  color: @a;
}
.conditional-example-1 {
  .mixin(#ddd)
}
.conditional-example-2 {
  .mixin(#555)
}

SASS

=mixin($a)
  @if lightness($a) >= 50%
    background-color: black
  @else if lightness($a) < 50%
    background-color: white
    color: $a;
.conditional-example-1
  +mixin(#ddd)
.conditional-example-2
  +mixin(#555)

STYLUS

mixin(a)
  if lightness(a) >= 50%
    background-color black
  else if lightness(a) < 50%
    background-color white
    color a
.conditional-example-1
  mixin(#ddd)
.conditional-example-2
  mixin(#555)

这三种架构都有自己的拥护者,其高阶的语法真的各有特色,特别是 mixin、继承、变量、条件语句,在 npm trends 的统计来看,使用分化并不大。

前端架构设计之CSS篇​(3万字修订版)

以 SAAS 为例,其转换过程如下图所示。

前端架构设计之CSS篇​(3万字修订版)

以上都是都是在 CSS 的基础上进行的语法设计,由于今年来 CSS in JS 的兴起,也就有很多使用 JS 代码来编写 CSS 样式表的尝试。

CSS in JS

在 CSS 预处理器的模式下,CSS文件往往独立于HTML文件,在日常开发中这就带来了很大的记忆负担,毕竟你单独看 HTML 文件或者 CSS 文件都不能快速勾画出网页的布局状态,你往往需要在两种不同的文件里来回跳跃。

CSS in JS 顾名思义,就是在 JS 中维护样式表,抛弃那无用的样式表文件,岂不是更加符合组件化思想。最大的好处还是在于真正的避免了CSS 选择器冲突的尴尬,也就说你不用费尽心思去想名字了。 例如以本文实践的styled-components 为例。

import React, { useContext } from 'react';
import styled from 'styled-components';
import { themeContext, IThemeConfig} from '../../../store/themeContext';

export default function() {
  const { themeConfig, dispatch } = useContext(themeContext);
  const Title = styled.h1
    font-size: 16px;
    font-weight: bold;
    margin-bottom: 16px;
    color: ${themeConfig.color}
  
  return (
    <div>
    <Title>展示每个小区的匹配雷达图</Title>
    <div>主要展示交通,新旧,配套,检索符合度,学区等维度进行分析,需要相关的爬虫和算法支持</div>
    </div>
  );
}

你只需要关注这个标签的含义即可,配合BEM的命名思路,整体模板会变得更加可读,可维护。

CSS in JS 其他的好处如下:

  • CSS-in-JS 利用 JavaScript 环境的全部功能来增强CSS。
  • 真正的选择器隔离。范围选择器是不够的。CSS具有从父元素自动继承的属性(如果未明确定义)。
  • CSS 要避免选择器冲突,例如 BEM 之类的命名约定可能在一个项目中有所帮助,但在集成第三方代码时则会存在很多问题。当 JSS 将 JSON 表示形式编译为 CSS 时,默认情况下会生成唯一的类名。
  • 动态浏览器私有化前缀,使用 CSS-in-JS 可以避免臃肿的 CSS 代码。
  • 代码共享,轻松在 JS 和 CSS 之间共享常量和函数。
  • CSS-in-JS 的单元化测试。
  • TypeScript 的支持。
  • 减少项目编译的依赖,纯 JS 或 TS 项目。
  • 动态变化的主题和变量。

这部分内容会在 CSS 百宝箱 章节部分给大家详细讲讲如何使用 CSS in JS 解决主题定制中的动态主题难题。

CSS 架构实践推荐

来到本文的核心部分了,给大家介绍一套我自己用的比较舒服的 CSS 架构体系,方便大家直接在自己的项目中尝试。 采用以下技术体系:

  • tailwindcss 进行行内样式表的直接书写,尽可能减少样式表文件的产生
  • styled-components 解决组件内部样式封装和动态主题功能
  • polished CSS 工具函数,阮一峰老师推荐,配合 styled-components 减少 CSS 代码量
  • Less 使用 Less 预处理器将公共组件的样式进行提取,进行公共样式的复用。
  • typescript-plugin-css-modules 帮助 import styles from 'xxx.less' 的字段推导和Typescript 的严格校验,避免无用类和样式漏写的尴尬。
  • 推荐命名的工具-变量命名神器 codelf

下面介绍这套CSS架构使用场景。

首先介绍 tailwindcss + styled-components + polished CSS 组合,这套组合可以覆盖绝大多数组件化开发场景,你几乎不需要书写任何样式文件。因为所有内容都集中在一个文件里,更加容易理解组件内容。

import React from 'react';
import { Card } from 'antd';
import styled from 'styled-components';
import { clearFix, ellipsis, between, linearGradient } from 'polished';
function StyledComponentCard() {
  const Title = styled.div
    ${clearFix()}
    ${ellipsis('450px')}
    font-size: ${between('12px', '20px', '400px', '1000px')};
    ${linearGradient({
      colorStops: ['#00FFFF 0%', 'rgba(0, 0, 255, 0) 50%', '#0000FF 95%'],
      toDirection: 'to top right',
      fallback: '#FFF',
      })}
  ;
  return (
    <Card title="styled-component、polish.css、tailwind.css组合使用">
      <Title>展示抓取的数据,目的获取关注小区的最新售价,和挂牌价变化范围</Title>
      <div className='font-bold underline'>tailwind.css的应用-加粗下划线</div>
    </Card>
  );
}

前端架构设计之CSS篇​(3万字修订版)

且最终的DOM结构也是十分精简。

前端架构设计之CSS篇​(3万字修订版)

对于普通业务组件,它的设计往往没有那么精简,当你希望单独复用样式文件或者DOM结构时,就需要 Less + CSS module + 类名推导校验 这套体系了。这套体系即将DOM + 代码逻辑和样式进行了文件抽取方便复用,也提供了类名自动补全,错误类名校验来降低记忆负担和出错率。

前端架构设计之CSS篇​(3万字修订版)

如果团队内部想统一命名习惯,而又不引入记忆负担,建议接入VSCPDE codeIf 插件,自动生成语义化的命名。

前端架构设计之CSS篇​(3万字修订版)

回到最初的问题,这套最佳实践是否可以完美解决 CSS Hug file 难题呢?

前端架构设计之CSS篇​(3万字修订版)

这里主要介绍了如何解决性能差的问题。

复用度高是由于 Polish CSS 和 StyleComponent 实现大部分的样式复用,主要针对容器组件。

展示组件则倾向于采用 Less + style component等CSS in JS 的方案,方便组件实现动态主题,来适配不同的场景,增强组件的功能性,避免重复开发,再加上编译的文件提取可以保证CSS的文件体积不会随着项目扩张,体积持续增大。

CSS 百宝箱

webpack CSS优化

目前前端大部份的CSS的编写都是采用 Less、PostCss、SCSS 等预编译语言,通过 Webpack 编译成浏览器可以支持的 CSS 文件。在这个过程可以借助 Webpack 丰富的插件进行 CSS 的编译优化,降低 CSS 的代码体积,自动补充浏览器的兼容性差异。

首先是将CSS从各个JS文件中提取出来,放到 html head 里优先加载,避免样式闪屏问题。这里推荐使用 mini-css-extract-plugin。

// webpack.config.js
const MiniCssExtractPlugin = require('mini-css-extract-plugin')
export default {
  ...options,
  module: {
    rules: [
      {
        test: /.css$/,
        use: [MiniCssExtractPlugin.loader, 'css-loader']
       },
      {
          test: /.less$/,
          use: [MiniCssExtractPlugin.loader, 'css-loader', 'less-loader']
      },
      {
          test: /.scss$/,
          use: [MiniCssExtractPlugin.loader, 'css-loader', 'sass-loader']
      }
    ]
  },
    plugins: [
      new HtmlWebpackPlugin({
          template: './index.html'
      }),
      new MiniCssExtractPlugin({
          filename: 'css/built.css'
      })
     ],
  }
}

为了浏览器兼容,需要开启 autoprefixer 和 browsers 选项。

  • Trident内核:主要代表为IE浏览器, 前缀为-ms
  • Gecko内核:主要代表为Firefox, 前缀为-moz
  • Presto内核:主要代表为Opera, 前缀为-o
  • Webkit内核:产要代表为Chrome和Safari, 前缀为-webkit

package.json里增加浏览器的声明。

"browerslist": {
    "development": [
      "last 1 chrome version",
      "last 1 firefox version",
      "last 1 saferi version"
    ],
    "production": [
      "last 1 version",
      "> 1%",
      "IE 9"
    ]
  }

利用 "postcss-loader" 的 autoprefix 配置项可以对 CSS 增加浏览器兼容前缀,postcss.config.js 中增加具体配置。

// webpack.config.js
module.exports = {
  module: {
    rules: [
      {
        test: /.css$/,
        use: ["style-loader", "css-loader", "postcss-loader"]
      }
    ]
  }
}
// postcss.config.js
module.exports = {
  plugins: [
    require('autoprefixer')
  ]
}

其次是移除无用CSS,可以使用 purgecss-webpack-plugin。

const PurgecssPlugin = require("purgecss-webpack-plugin");
const glob = require('glob')
const PATHS = {
    src: resolve(__dirname, 'src')
}
 plugins: [
   new PurgecssPlugin({
    paths: glob.sync(${PATHS.src}/**/*,  { nodir: true }),
   })
  ]

CSS 的新特性玩法

Chrome 112 :CSS 支持嵌套语法

这个特性给大家简单演示下,下面是一个简单的Demo。

function DynamicThemeCard() {
  const {dispatch, themeConfig} = useContext(themeContext);
  const [value, setValue] = useState('rgba(0,0,0,.85)');
  return (
    <Card title="浏览器CSS嵌套">
      <Row className="formItem">
        <Col className="formItem--label" span={4}>字体颜色</Col>
        <Col className="formItem--content" span={20}>
          <Input className="formItem--content--input" value={value} onChange={(event: React.ChangeEvent<HTMLInputElement>) => { setValue(event.target.value) }} style={{ color: themeConfig.color, width: '300px' }} type='text' />
        </Col>
      </Row>
      <Button onClick={() => dispatch({ type: EThemeActionType.SET, payload: { color: value } }) }>同步字体颜色</Button>
    </Card>
  );
}

样式文件 index.css。

.formItem {
  .formItem--label {
    color: rgba(0,0,0, 0.6);
  }

  .formItem--content {
    .formItem--content--input {
      width: 300px;
    }
  }
}

当你运行起来之后,你会发现你的CSS竟然生效了。

前端架构设计之CSS篇​(3万字修订版)

这个特性虽然小,但是对于CSS性能提升要比预期的大。无论使用预处理器Less,还是 CSS in JS 最终给到浏览器的CSS文件还是一层层嵌套的,繁复的样式文件,最终体积依然很大,只是开发不用再去写这些恶心代码了而已。一旦 CSS 嵌套语法被全面支持,那边编译出来的样式文件体积会被大大压缩,甚至有可能直接书写.css文件。

CSS 现代选择器

现代 CSS 选择器是指在 CSS3 标准中引入的一些新的选择器,它们提供了更强大、更灵活的选择元素的方式。以下是一些常见的现代 CSS 选择器:

  1. 属性选择器(Attribute Selectors):通过元素的属性进行选择,有多种写法,如 [attr] 选择所有具有该属性的元素,[attr=value] 选择属性值等于指定值的元素。
  2. 伪类选择器(Pseudo-class Selectors):用于选择元素的特殊状态或位置,例如 :hover 选择鼠标悬停的元素,:nth-child(n) 选择第 n 个子元素等。
  3. 伪元素选择器(Pseudo-element Selectors):用于向选中的元素的特定部分应用样式,例如 ::before 在元素内容前插入生成的内容,::after 在元素内容后插入生成的内容等。
  4. 结构性伪类选择器(Structural Pseudo-class Selectors):根据元素在 DOM 结构中的位置进行选择,如 :nth-child(n) 选择其父元素的第 n 个子元素,:first-child 选择其父元素的第一个子元素等。
  5. 否定伪类选择器(Negation Pseudo-class Selectors):选择不符合给定选择器的元素,例如 :not(selector) 选择不匹配指定选择器的元素。
  6. 相邻兄弟选择器(Adjacent Sibling Selectors):选择某个元素后紧接着的兄弟元素,例如 element + sibling 选择紧跟在 element 后面的 sibling 元素。
  7. 通用兄弟选择器(General Sibling Selectors):选择某个元素后所有的兄弟元素,例如 element ~ sibling 选择在 element 后面的所有 sibling 元素。

这些现代 CSS 选择器提供了更灵活和便捷的方式来选择和操作网页中的元素,可以根据实际需求采用不同的选择器组合,以实现更精准的样式控制和交互效果。

这里建议大家配合 BEM 等 CSS 规范,将 DOM 结构和 CSS 结构在语法上进行配合,使之更加容易阅读,也可以降低 CSS 样式代码量。例如下面的 :where, :not, :has, :is 四个现代选择器。

.cssContainer {
  .demo {
    > div {
      margin-top: 4px;
    }
    p {
      color: black;
      margin: 0;
    }
    :is(.parent1) p {
      color: red;
    }
    :where(.parent1, .parent2) p {
      color: blue;
    }
    div:has(p) {
      position: relative;
      &::before {
        content: '包含P标签的标识';
        position: absolute;
        right: 0px;
        bottom: 0px;
        color: gray;
      }
    }
    div:has(h1) {
      position: relative;
      &::before {
        content: '包含H标签的标识';
        position: absolute;
        right: 0px;
        bottom: 0px;
        color: gray;
      }
    }
    div:not(.notBorder) {
      border: 1px dashed #888;
      border-radius: 4px;
      overflow: hidden;
    }
  }
}

最终的样式效果如下,在语义上和 DOM 结构是完全保持一致的。 前端架构设计之CSS篇​(3万字修订版)

这里大家可以重点关注下 :where 这个现代选择器,在新版 and-design 5 中动态主题 CSS in JS 实现在 CSS 主要是基于 :where(.css-token) 的机制来实现动态主题覆盖、不同组件之间的主题独立配置,其内部实现细节会在下面的章节详细介绍。兼容性方面还是存在缺陷,主流 Firefox 浏览器并不支持这些伪类选择器。

@property 在动画层面的应用

CSS @property 是 CSS Houdini 规范中引入的一个特性,它允许开发者创建自定义 CSS 属性,在样式表中定义和使用这些属性。通过 @property,开发者可以扩展 CSS,并在 JavaScript 中操作这些自定义属性。

使用 @property 定义自定义 CSS 属性的语法如下:

@property <property-name> {
  initial-value: <default-value>;
  inherits: <true | false>; /* 可选 */
  syntax: <value-type>; /* 可选 */
}
  • <property-name>: 自定义属性的名称,通过该名称在样式表中引用。
  • initial-value: 自定义属性的初始值,当未指定具体值时将使用该值。
  • inherits: 指定自定义属性是否继承父元素的值。如果设置为 true,则子元素会继承父元素的自定义属性值;如果设置为 false,则子元素不会继承。
  • syntax: 定义自定义属性的值类型。可以使用 CSS 数据类型(如 <color><length><percentage> 等),也可以使用自定义的枚举值等。

例如,定义一个名为 --highlight-color 的自定义属性,并设置其初始值为红色:

@property --highlight-color {
  initial-value: red;
}

然后,在样式表中可以使用这个自定义属性:

.selector {
  color: var(--highlight-color);
  background-color: var(--highlight-color);
}

使用 JavaScript,你可以通过 setProperty 方法动态改变这个自定义属性的值:

element.style.setProperty('--highlight-color', 'blue');

CSS @property 的引入使开发者能够创建更灵活的样式系统,并将样式与 JavaScript 结合起来,实现更强大的交互效果和动态样式控制。然而,需要注意的是,由于该特性尚处于实验阶段,并不是所有浏览器都完全支持,因此在使用时需谨慎考虑兼容性。 前端架构设计之CSS篇​(3万字修订版)

关键部分的代码

@property --s {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}
@property --m {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}
@property --ms {
  syntax: '<integer>';
  inherits: false;
  initial-value: 0;
}

@keyframes minitus {
  to {
    --m: 59
  }
}
@keyframes seconds {
  to {
    --s: 59
  }
}
@keyframes ms {
  to {
    --ms: 99
  }
}

.animationTimer {
  counter-reset: minitus var(--m) seconds var(--s) ms var(--ms); // CSS 计数器
  animation: minitus 3600s infinite steps(60, end),
  seconds 60s infinite steps(60, end),
  ms 1s infinite steps(100, end);
  border: 2px solid rgba(0,0,0,0.5);
}
.animationTimer::before {
  color: #fff;
  padding: 6px 8px;
  background-color: black;
  font-size: 16px;
  content: counter(minitus, decimal-leading-zero) ':' counter(seconds, decimal-leading-zero) ':' counter(ms, decimal-leading-zero);
}

.animationTimer {
  animation-play-state: paused;
}

.started.animationTimer {
  animation-play-state: running;
}
<Card title="CSS 数字时钟">
  <p>css property 数字变化</p>
    <span className='cssTimer'></span>
    <Divider />
    <p>css animation 数字变化</p>
    <div className="clockLayout">
    <span ref={clockRef} className={classnames({ 'animationTimer': true, started: started })}></span>
    <div className="clockAction">
      <Button style={{ marginRight: '4px' }} size="small" type="primary" onClick={handleTimeStart}>开始</Button>
      <Button style={{ marginRight: '4px' }} size="small" type="ghost" onClick={handleTimeEnd}>暂停</Button>
      <Button size="small" type="ghost" onClick={handleCurrentTime}>报时</Button>
    </div>
  </div>
  </Card>

由于是伪元素渲染,页面上看不到任何数字,也就是无法直接通过 innerText 获取当前时间,但是,我们可以借助 getComputedStyle 来得到 CSS 属性或者 setProperty 设置属性

// 获取值
getComputedStyle($0).getPropertyValue('--ms')
// 设置值
(clockRef.current as any).style.setProperty('--ms', '50');

由于在实现中用到了 CSS @property 特性,这是 CSS Houdini 的一部分,目前只有 Chrome 支持(可惜了😥)。让人惊奇的是,Safari 居然在前不久也支持了这个特性,未来可期。

总结下实现思路:

  • CSS @property 可以使 CSS 变量支持动画
  • 数字时钟的变化其实是一个 CSS 变量不断递增循环的动画
  • 计时器开始和暂停其实就是动画的运行和暂停
  • 直接将动画取消就相当于重置了整个动画

CSS variables 在主题定制中的应用

动态主题比主题定制要更进一步,要实现能够在运行阶段进行样式主题的调整,而不是定制主题的CSS编译阶段调整,因此在日常开发中可以预留一部分这块的设计。

样式定制根据组件类别的不同,方案也有所调整。例如组件库或者抽象组件的动态主题往往要依赖CSS Variables来实现(有兴趣可以看下《一文讲透CSS变量和动态主题的内在联系》)。

CSS in JS 在主题定制中的应用

这里回到前文留的坑,CSS variables 实现动态主题还是比较麻烦的,最近 ant-design 5 基于 CSS in JS 方案给出了新的方案,并且发布正式版本了。

Ant-desgin 组件库几乎是国内最成功的前端组件库,在小编开始接触组件库的时候,antd 提供了 css 和 less 两种样式引入方式,其定制主题也是基于 less 和 CSS 的变量实现,其动态主题的效果很差,甚至antd-pro的动态主题模板本地运行都会出现失效的情况。

正如文章结尾总结,基于 less 和 CSS 的变量实现的局限性太大。

就我自己的使用体验而言,相对于传统静态主题定制的方式,动态主题在实际使用过程中也存在以下局限性的。

  • 性能差,无法进行压缩、混淆,文件体积大
  • css in js 的思想有一定的冲突,无法使用 css module,实际代码开发体验不好
  • 不适用于微前端等应用场景,CSS 隔离会遇到很大的麻烦
  • 兼容性差

在 antd@5版本迎来了转机。

在 5.0 版本的 Ant Design 中,我们提供了一套全新的定制主题方案。不同于 4.x 版本的 less 和 CSS 变量,有了 CSS-in-JS 的加持后,动态主题的能力也得到了加强,包括但不限于:

  1. 支持动态切换主题;
  2. 支持同时存在多个主题;
  3. 支持针对某个/某些组件修改主题变量;
  4. ...

这里提到的CSS-in-JS就是 @ant-design/cssinjs 库,基于它,antd 实现了在 TS 中书写 CSS 样式。

// ant-design/components/card/style/index.ts
// ============================== Basic ==============================
const genCardStyle: GenerateStyle<CardToken> = (token): CSSObject => {
  const {
    componentCls,
    cardShadow,
    cardHeadPadding,
    colorBorderSecondary,
    boxShadowTertiary,
    cardPaddingBase,
  } = token;

  return {
    [componentCls]: {
      ...resetComponent(token),

      position: 'relative',
      background: token.colorBgContainer,
      borderRadius: token.borderRadiusLG,

      [`&:not(${componentCls}-bordered)`]: {
        boxShadow: boxShadowTertiary,
      },

      [`${componentCls}-head`]: genCardHeadStyle(token),

      [`${componentCls}-body`]: {
        padding: cardPaddingBase,
        borderRadius: ` 0 0 ${token.borderRadiusLG}px ${token.borderRadiusLG}px`,
        ...clearFix(),
      },
    },
    ...
  };
};
// ============================== Export ==============================
export default genComponentStyleHook('Card', (token) => {
  const cardToken = mergeToken<CardToken>(token, {
    cardShadow: token.boxShadowCard,
    cardHeadHeight: token.fontSizeLG * token.lineHeightLG + token.padding * 2,
    cardHeadHeightSM: token.fontSize * token.lineHeight + token.paddingXS * 2,
    cardHeadPadding: token.padding,
    cardPaddingBase: token.paddingLG,
    cardHeadTabsMarginBottom: -token.padding - token.lineWidth,
    cardActionsLiMargin: `${token.paddingSM}px 0`,
    cardActionsIconSize: token.fontSize,
    cardPaddingSM: 12, // Fixed padding.
  });

  return [
    // Style
    genCardStyle(cardToken),

    // Size
    genCardSizeStyle(cardToken),
  ];

在 Card 组件中则会引入这个样式生成 JS。


import useStyle from './style';
const Card = React.forwardRef<HTMLDivElement, CardProps>((props, ref) => {
    ...
    const prefixCls = getPrefixCls('card', customizePrefixCls);
    const [wrapSSR, hashId] = useStyle(prefixCls);
    return wrapSSR(
        <div ref={ref} {...divProps} className={classString}>
          {head}
          {coverDom}
          {body}
          {actionDom}
        </div>,
      );
}

最终生成了的页面样式如下

前端架构设计之CSS篇​(3万字修订版) 这里面的设计核心就是 token 的机制,例如上图中的 css-l0aeht 和 css-w8mne 就是两个 token,利用现代选择器 :where 来增加自定义样式的作用于,通过类名的优先级和添加删除来动态控制全局样式,不得不说这是一个很酷炫的设计,性能开销也可以控制在一个可以接受的范围。

结语

都坚持到这里了,请给个点赞吧! 希望这篇文章能够帮助你的日常开发,帮助设计高效率的、维护简便的、合作同事乐于接受的前端架构。

转载自:https://juejin.cn/post/7249595460289134647
评论
请登录