前端架构设计之CSS篇(万字手疼)
小编从前端工作也有一些年头了,最近也一直打算对自己的所学进行梳理沉淀,将软件开发过程中优秀的方法、理论、实践进行梳理,形成一套完整的实践架构。本文主要讲述 CSS 领域中如何从前端架构角度去设计代码工程,包括代码规范、命名规范、编译套件、自动补全套件等等,力求从实际的软件开发角度去分解这个过程,搭建高质量的前端工程,保持全产品周期的高效率开发和维护。当然在这个过程中也会穿插一些 CSS 领域的背景知识,方便大家理解业内一些CSS设计的初衷。
本文分为4个 part。
- CSS 标准化过程 主要介绍 CSS 发展史上重要的三次版本发布和其内在驱动逻辑,不感兴趣的朋友请直接跳到感兴趣的章节。
- CSS 设计范式 主要介绍代码编写 CSS 的组织管理规范,包括命名规范、组织方式、可扩展设计。主要是帮助大家选择合适的设计范式减少命名冲突、可读、可复用,用我自己的体验来说就是减少记忆负担,无脑开发不出错。
- CSS 架构设计 主要介绍不同 CSS 设计范式下的具体实践。业内比较出名的有原子化 CSS、CSS 预处理器(Less、PostCss)、CSS in JS等架构设计和实践,在此章节会重点介绍我在这些架构上的使用经验,也介绍我自己梳理出来的最佳实践,仅供大家参考。
- CSS 百宝箱 顾名思义就是将 CSS 领域内其他优秀的设计和应用进行汇总,组成一个丰富的“百宝箱”,给大家开箱即用,当然肯定没有一百这么多,但是我会逐渐补充完善它。
通过这四个章节,给大家介绍下完整的 CSS 领域的生态和工程化实践,希望对处于前端学习瓶颈的朋友有所启发。
CSS 标准化过程
这个章节会比较枯燥一点,但是对初入前端领域的同学还是有必要简单介绍下历史背景,开头我先用简单的语言给大家总结下核心内容,资深的同学可以跳过这一部分。
- 刀耕火种的CSS1,只有可怜的24个属性,各个浏览器设计混乱。
- 版图扩张的CSS2,脱离了传统的枯燥的页面瀑布流的境地,开发对页面控制力大大增强。
- 重视交互体验的CSS3,更容易地创建更复杂、更丰富的效果和交互。
CSS(层叠样式表)是一种用于网页布局与设计的标记语言,通过对网页元素进行样式描述和定位控制来实现网页的美化、排版和交互效果。社区这些年一直在不断的迭代设计,丰富完整 CSS 生态,得益于一套严谨的规范管理机制。
CSS标准化的过程可以分为以下几个阶段:
- 工作草案(WD, Working Draft):由W3C联盟的工作组(Working Group)提出初始的规范方案,作为一个可进行讨论和验证的版本,通过不断迭代修改并达成共识。
- 建议推荐(CR, Candidate Recommendation):目前被大部分标准委员会人员认同的草案阶段,已经比较接近最终的规范。在进入CR阶段之前,需先对性能、兼容性等进行适当测试、考察,以确保标准实现的情形足够广泛。
- 建议确定(PR, Proposed Recommendation):已经是一份相对稳定的规范,提供给W3C理事会进行投票正式批准,同时也有机会向外公开征求意见来优化这个候选规范。
- 正式规范(REC, Recommendation):经过上述各个阶段的评审、反复打磨之后,最终被制定为一个正式的标准规范。
在每个阶段都要经过各类浏览器或社区的实验测试,通过支持的版本号来为这份规范提供资料,并在这个过程中更新及修改规范,至此该标准的定义才真正意义上开始得到普及和使用。由此可见 CSS 作为标记语言是有严格的规范流程的,请不要轻视它,利用的好可以释放巨大的开发效率的。
目前CSS迭代了3个大版本,其中CSS1是第一个被制定为正式标准的CSS版本,于1996年12月公布。
CSS1
CSS1对网页设计和开发起到了重要的推动作用,它将各种HTML元素、文本、颜色和布局等方面进行了规范、分类,并引入了Cascading Style Sheets的基本概念。
CSS1主要包含以下核心功能:
- 字体:定义字体类型、大小、样式、粗细以及颜色等。
- 背景:定义背景颜色、以图方式或背景位置等相关属性。
- 文本:添加文本装饰、调整行高和文本间隔等。
- 颜色:提供颜色设定,覆盖HTML早期颜色定义中的字面值。
- 大小定位:定义文本和布局元素的大小、位置、外边距和内边距等。
在CSS1中,使用了24个属性,如color、background-color、font-size和padding等等,为后来的CSS2、CSS3的发展奠定了基础。
CSS2
CSS2是第二个被制定为正式标准的CSS版本,于1998年5月公布。相对于CSS1,CSS2增加了更多的样式属性和选择器,并提供了更细致、更精确的控制元素显示的能力。
CSS2的主要特点可以归纳为以下几点:
- 定位与元素特征:新增了绝对/相对定位、浮动以及display属性等,并引入了:first-child、:hover、:active等伪类选择器来描述元素特征。
- 布局:增加了table-layout和position属性,使开发者能够更好地控制页面布局。
- 贴近文档结构:强调清零所有默认值,把不需要样式的HTML元素作为一个独立的模块去考虑。
- 字体:相较于CSS1,CSS2引入字体风格(font-style)、文字间距(letter-spacing)以及字符间距等新功能。
- 其它:还包括盒子模型(Box Model)的完善、文本处理与呈现的扩展、生成内容样式等等。
CSS3
CSS3是CSS的第三个版本,于1999年开始制定,并在2011年6月正式公布。与前两个版本相比,CSS3引入了更多的样式和选择器,让开发者可以更容易地创建更复杂、更丰富的效果和交互。
CSS3引进的一些新特性包括:
- 选择器:新增了属性选择器(attribute selectors)、伪类选择器(pseudo-class selectors)和伪元素选择器(pseudo-element selectors),增强了选择元素的能力。
- 盒模型:让CSS针对盒子模型进行更精确、更准确的计算。
- 边框和背景:新增了多重背景、圆角边框、边框阴影等。
- 文本效果:新增了文字阴影(text-shadow)、文字渐变、多列文本布局等。
- 转换与过渡:新增了2D/3D转换、过渡动画等样式属性,使得开发者可以实现更加生动的页面展示效果。
- 网格和弹性布局:新增了网格布局(以行和列的方式排列元素)和弹性盒布局(以父容器/子元素的关系为基础)等。
未来的 CSS 发展方向主要体现在以下几个方面:
- 更智能化的样式规则:未来的 CSS 将更加智能化,能够更好地识别 DOM 中元素之间的关系,并自动应用相应的样式。例如,可视化样式编辑器,可以通过图形界面操作选中元素,并直接生成对应的 CSS 代码。
- 支持更多的用户交互效果:CSS 的 user-select 属性已经允许开发人员控制哪些元素可以选择和拖动,未来还会支持更多用户交互效果,例如滚动、折叠、拖放等操作。
- 响应式设计优化:随着移动设备和其他非传统设备的增多,未来的 CSS 将继续优化响应式布局,在不同设备上以适当的方式呈现,以提高用户体验。
随着浏览器的性能和开发程度的提升,CSS 能够实现的功能也将逐渐提升。
CSS设计范式
CSS设计范式出现的初衷是为了解决Huge CSS file难题。随着项目逐渐变得更加庞大、更加复杂之后,就需要维护庞大的 CSS 文件,它们难以阅读、存在命名冲突、DOM映射困难,工程质量变得越来越脆弱,样式出现不可控的错误情况越来越多。
1、OOCSS
全称-Object Oriented CSS, 主要目的是获取页面中使用和重复的元素作为对象,并在必要的地方使用它们。
OOCSS 有两个主要原则
- 将结构与皮肤分离
- 将容器与内容分离
在早期的前端开发中,结构往往是指代“width, height, padding, margin , position”这些会影响布局的样式;皮肤则是指代“border, color, font, background”这些单纯的样式。原则一就是让结构和皮肤进行分离,看下下面的错误案例。
box1和box2的都具有相同的皮肤设计,因此需要将皮肤分离出来。
相应的类名也要调整到对应层级的设计。
原则二则是希望减少使用子类选择器。下图中h2标签会在多处使用,子类选择器“.sidebar h2 { ... }”的使用就有悖于原则二。
应用 OOCSS 范式将 h2.sidebar-title 从容器 sidebar 中独立出来,就可以在多处复用,也不用重复这段 CSS。
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 连接,以-连接单词。
一个典型案例:三种不同状态的按钮。
// 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(SUIT是“Simple, Scalable, and Modular”三个词的缩写)是一种基于组件和命名约定的CSS框架。它提供了一套规范的类名和样式命名方式,旨在降低大型项目中CSS的复杂度和难度。
SUIT CSS的编写风格非常规范化,主要包括三个部分:
-
基础:SUIT CSS使用 normalize.css 来重置和标准化CSS属性,小编在项目中是经常见到的,不过当时不懂其中的奥妙。
-
组件:每个组件对应一个独立的CSS文件,并且包含唯一的类名,比如.snackbar、.modal等。
-
命名约定:SUIT CSS采用BEM风格(Block Element Modifier)的命名方式,可以通过类名完整地描述组件结构和状态。
例如,按钮组件的HTML可以这样写。
<button class="Button Button--primary">Submit</button>
其中,.Button代表按钮组件的Block,--primary是它的Modifier,用于明确其为主要按钮。
- 工具类: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 架构设计
说起架构设计大家可能会觉得比较玄学,像隔着一层毛玻璃一样,看不真切。实际大家设计和七巧板玩具组装建筑的过程特别相近,每个七巧板代表一个代码库、又或者一个开发者工具,又或者一个开源服务,各个七巧板相互配合,最终形成一件精美的艺术品。在项目的启动阶段,我们往往要选取一套模版,包含了各个七巧板的位置(仓库)、数量(需求依赖)、编号(执行计划),然后召集多个小伙伴一起分工组装,而模板往往包含了实现预期、整体质量、抽象层次的实现规划等等,设计这个模版的过程就是所谓的架构设计。好的架构设计往往具有前瞻性,能够为未来的扩展保留足够的弹性;往往具有高效性,是实现预期功能的最短路径;往往具有良好的独立性,各个功能模块可以用极小的改动即可独立服务于客户。
CSS 架构设计则是希望设计和拟合一系列开发范式、开发套件、功能设计来满足产品对交互、视觉效果的要求,同时具有可维护、可扩展、高效率的工程特性。本章节会先介绍CSS领域内的常规范式和套件,然后给大家推荐一套本人比较喜欢的架构,以作参考。
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 官网。
这里我比较喜欢使用 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的作用域概念、屏蔽浏览器差异等,对于不同的浏览器和同一种浏览器的不同版本实现了自动兼容,极大解放了前端生产力。
下面列举下常见的 CSS 预处理器:
- PostCSS:2013/11/04 发布
- Less:2009 发布
- SASS:2006/11/28 发布
- Stylus:2010/12/29 发布
这四种架构都有自己的拥护者,其高阶的语法真的各有特色,特别是 mixin、继承、变量、条件语句。需要注意的 PostCSS 会将 CSS 代码解析抽象成抽象语法树结构AST,因此也就有 Webpack、 Grunt、Gulp 编译平台进行继承,利用插件可以对 CSS 代码进行定制。
以 SAAS 为例,其转换过程如下图所示。
以上都是都是在 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 的严格校验,避免无用类和样式漏写的尴尬。
下面介绍这套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>
);
}
且最终的DOM结构也是十分精简。
对于普通业务组件,它的设计往往没有那么精简,当你希望单独复用样式文件或者DOM结构时,就需要 Less + CSS module + 类名推导校验 这套体系了。这套体系即将DOM + 代码逻辑和样式进行了文件抽取方便复用,也提供了类名自动补全,错误类名校验来降低记忆负担和出错率。
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性能提升要比预期的大。无论使用预处理器Less,还是 CSS in JS 最终给到浏览器的CSS文件还是一层层嵌套的,繁复的样式文件,最终体积依然很大,只是开发不用再去写这些恶心代码了而已。一旦 CSS 嵌套语法被全面支持,那边编译出来的样式文件体积会被大大压缩,甚至有可能直接书写.css文件。
CSS 现代选择器
现代 CSS 选择器是指在 CSS3 标准中引入的一些新的选择器,它们提供了更强大、更灵活的选择元素的方式。以下是一些常见的现代 CSS 选择器:
- 属性选择器(Attribute Selectors):通过元素的属性进行选择,有多种写法,如
[attr]
选择所有具有该属性的元素,[attr=value]
选择属性值等于指定值的元素。 - 伪类选择器(Pseudo-class Selectors):用于选择元素的特殊状态或位置,例如
:hover
选择鼠标悬停的元素,:nth-child(n)
选择第 n 个子元素等。 - 伪元素选择器(Pseudo-element Selectors):用于向选中的元素的特定部分应用样式,例如
::before
在元素内容前插入生成的内容,::after
在元素内容后插入生成的内容等。 - 结构性伪类选择器(Structural Pseudo-class Selectors):根据元素在 DOM 结构中的位置进行选择,如
:nth-child(n)
选择其父元素的第 n 个子元素,:first-child
选择其父元素的第一个子元素等。 - 否定伪类选择器(Negation Pseudo-class Selectors):选择不符合给定选择器的元素,例如
:not(selector)
选择不匹配指定选择器的元素。 - 相邻兄弟选择器(Adjacent Sibling Selectors):选择某个元素后紧接着的兄弟元素,例如
element + sibling
选择紧跟在 element 后面的 sibling 元素。 - 通用兄弟选择器(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 结构是完全保持一致的。
这里大家可以重点关注下 :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 结合起来,实现更强大的交互效果和动态样式控制。然而,需要注意的是,由于该特性尚处于实验阶段,并不是所有浏览器都完全支持,因此在使用时需谨慎考虑兼容性。
关键部分的代码
@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 的加持后,动态主题的能力也得到了加强,包括但不限于:
- 支持动态切换主题;
- 支持同时存在多个主题;
- 支持针对某个/某些组件修改主题变量;
- ...
这里提到的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>,
);
}
最终生成了的页面样式如下
这里面的设计核心就是 token 的机制,例如上图中的 css-l0aeht 和 css-w8mne 就是两个 token,利用现代选择器 :where 来增加自定义样式的作用于,通过类名的优先级和添加删除来动态控制全局样式,不得不说这是一个很酷炫的设计,性能开销也可以控制在一个可以接受范围。
结语
都坚持到这里了,请给个点赞吧! 希望这篇文章能够帮助你的日常开发,帮助设计高效率的、维护简便的、合作同事乐于接受的前端架构。
转载自:https://juejin.cn/post/7249595460289134647