整理项目 CSS 存在的问题,如何减轻样式编写痛感,以及如何实现动态换肤。
当前困境
整理了一下当前项目中前端样式编写的困境
- 使用了开源的 antd 组件库,但设计师多少想改组件默认的样式
- 自己编写的组件,即使在定义了 CSS 变量的情况下,仍然不是很规范,比如有变量但未使用,或设计稿本身没有按照规范
关于样式配置
- 主要分为两部分
- 基础样式
- 组件样式
- 基础样式:当修改基础样式的配置时,对全局样式有着强烈的影响
- 基础样式主要包括 Arco Design 设计系统中定义的一系列的粒子变量,如颜色,字体,尺寸等,是设计同学和前端同学约定的一套规范。他们作为基础变量,被 Button,Checkbox 等基础组件所引用。而 Checkbox,Button 等基础组件又是 Modal,Tree 等复杂组件的一部分。所以对基础变量的修改会直接影响到全部组件的设计风格。
- 组件样式配置:细粒度的定制单个组件的样式
- 开发者模式:针对无法通过配置项实现的需求
对于第一个问题,首先就是要接受这种现象,UI 设计师和开源组件库有着难以调和的矛盾。比如我接触的最近三位设计师,要我实施定制的部分就各不相同。所以那我们就只能一直改改改吗?当然不是,这样只会影响我们的开发效率,因此当我们碰到这种情况时,需要多走一步,需要推进把修改变成规范,使之变成一次性工作,而不是反复,具体方式有:
- 最优解:通过组件库暴露的变量进行修改(antVarOverrides.js)
- 次解:通过样式覆盖的方式(antd.less)
警惕对于可交互对象的样式修改,由于其通常有如下四种状态,如果单修改其中某个颜色,会导致和其他状态搭配起来很尬,同时注意不要滥用 !important
属性,从而导致失去交互效果。
- 正常态
- 悬浮态
- 激活态
- 禁用态
我们思考一下哪些规范是至关重要的呢,从而先解决最重要的问题?我们可以从使用场景出发,比如我们设计一套组件库的时候,首先要考虑颜色、字体、边框、图标这些基础元素的设计,它们是构建各个组件的基石。或者我们可以将来变化的角度出发,比如需要实现暗黑主题或紧凑主题,我们最关心的 CSS 变量就会是
- 颜色变量
- 文本颜色
- 背景色
- 边框、分割线、图标颜色
- 间距变量
当然不是说其他的就不重要,还有如图所示的基础定义也是很重要的,只是说影响范围相对没有那么大。
如果项目中早已定义好相关变量,且各组件中是严格按照规范开发的,这样实现暗黑或紧凑主题就会轻松。
总结了一下当前项目中样式的重灾区。正好指向的就是我们关心的颜色和间距这两部分,相对而言比较没有规范。
- 关于品牌色和功能色这一块争议不大。主要是背景色最为严重,比文本和边框色严重的多
- 间距这一块,padding 和 margin 类似,使用了大量不在规范里的数值
表现较好的有:
- 字号由于之前定义了规范,仅存在少量没使用变量的情况,但是属于规范里数值
- 圆角由于使用较少,问题同字号
CSS 审查
本次 CSS 审查过程中发现团队编写 CSS 中存在如下问题
跟规范相关问题
- 未跟推进规范建设相关:不思考对于组件的样式覆盖是否可以规范化,只是一味的去覆盖,甚至同一个组件,相同的覆盖方式被不知不觉实现了多次
- 设计师犯的小错误,导致错误就被实现了,常见于相近的间距或相近的颜色
- 使用 style 属性写样式,从而无法使用 less 变量,应该尽量避免
- 错误的写法相关,导致出现了很多规范之外的事情
- 有时候是百分比或对齐问题,而不是一个离谱的不符合规范的间距值
- ……
工程师容易犯的错误
- 面向自身浏览器编程,不考虑不同分辨率下情况:max-height: 600px、最多 15 组参数
- 面向当下编程,只关注当下使用场景,导致 CSS 完全没有伸缩性:width: 216px、height: 82px
- 只看现象,不看结构:margin 的奇淫技巧
打破困境
正确上述问题,我们该如何打破困境
- 最重要的一步:和设计师对于规范的理解达成一致,并严格执行
- 为避免变量名冲突,所有 variable 下定义的名称使用
<shorthand-company>
前缀
- 为避免变量名冲突,所有 variable 下定义的名称使用
- 工程师要主动推动规范建设,不要只关注当下实现
- 当你发现你需要疯狂去覆盖组件库自身样式时,就要开始考虑是不是该统一规范了
- 人工 ai 自动修正规范误差,存疑时,主动和设计沟通,而不是一味实现
- 提高工程师 CSS 素养
定制规范从来不是一件困难的事情,难的在于一个团队之间,如何保持意识形态统一并执行。否则导致规范流于形式,最终草草收场,然后一直反复!
动态换肤
先了解下常见的 CSS 变量解决方案
- css: var(--variable)
- less: @variable
- sass: $variable
less 和 sass 是在编译期进行变量替换,这时候用于实现动态换肤就有些捉襟见肘了,虽然支持客户端编译。
由于 css variable 是运行时方式,使用 less 函数对 css variable 进行计算是不形,但你搭配 less 变量进行计算后赋值给 css variable 是 OK,如下
@pre-primary-color: #f00;
body {
--primary-color: tint(@pre-primary-color, 50%);
}
@primary: var(--primary-color);
为什么要使用 CSS 变量
- 在构建大型站点时,作者通常会面对可维护性的挑战。在这些网页中,所使用的 CSS 的数量是非常庞大的,并且在许多场合大量的信息会重复使用。例如,在网页中维护一个配色方案,意味着一些颜色在 CSS 文件中多次出现,并被重复使用。当你修改配色方案时,不论是调整某个颜色或完全修改整个配色,都会成为一个复杂的问题,不容出错,而单纯查找替换是远远不够的。
- 名称本身就包含了语义的信息。CSS 文件变得易读和理解。main-text-color 比文档中的 #00ff00 更容易理解,特别是同样的颜色出现在不同的文件中的时候。
动态主题换肤方案
- 定义颜色变量:可能有兼容性问题、组件库可能不支持
- 通过覆盖样式实现:开发效率低、维护成本高
- antd menu theme 就是这么实现的
- 生成多套 CSS 皮肤,切换不同的 CSS 文件实现换肤
实现动态换肤方案总结
了解到这里,发现 antd v4 版本对于动态换肤的支持度是不足的,在 v5 计划中有提及,但谁知道啥时候发布呢?
场景换肤
分主题渲染,较复杂,会影响到开发效率,暂不考虑。
一些经验
搜索技巧
- 有些 antd 组件没有暴露对应的 className,仅提供了 style 属性,这时候会导致有些没有使用 less 变量,这时候可以通过搜索
style=
词元搜索 - 有些虽然使用了变量,但作用在 antd 组件上的,审查
:global
的使用情况判断是不是该推进规范 - 某些绝对定位元素表达是通过 top/left/bottom/right 表达的,也需要看看
- 检查
!important
是否有必要,使用!important
可能导致其他状态,如 hover 失效
一些问题解决方案
- storybook 如何一次引入样式变量:直接在
.storybook/preview.js
中引入即可
Less 函数
Less 常用函数
- fade 添加透明度
- darken 减少颜色的亮度,HSL 颜色表示法的亮度位
- lighten 减少颜色的亮度,HSL 颜色表示法的亮度位
- tint 混入白色
- shade 混入黑色
- color 解析颜色,使字符串变成颜色
- ceil/floor/max/round 同编程语言
Less 函数入参不能是 css variable,因为一个编译时,一个运行时
扩展:antd v5
关于 Ant Design v5
- 动机
- 更灵活的设计风格,作为设计系统,我们应该考虑约束与自由的平衡。
- 微前端会导致样式污染,意味着当使用多个 antd 版本时,会存在样式冲突
- 计划
- 新的设计系统
- 使用 css-in-js 替换 less
- 支持 React-18
- 放弃 IE
- 替换 moment.js
- API 升级
聊聊 css-in-js
- 最原始的 React 支持的 style 属性就是 css-in-js 的实现,但由于 CSS 的封装非常弱,导致了一系列的第三方库,用来加强 React 的 CSS 操作。它们统称为 css-in-js,意思就是使用 JS 语言写 CSS。
- CSS in JS 实现方式大致分为两种,css in js playground,可以快速尝试不同 css-in-js 实现的网站
- 唯一 CSS 选择器,以 styled-components 为代表,其他还有 jss、emotion、stitches 等。原理:当组件的 JS 代码被解析的时候,styled-components 会动态生成一个 CSS 选择器,并把对应的 CSS 样式通过 style 标签插入到 head 标签里面
- 内联样式
- 对比图
- 带来的优势
- less 在动态主题和微前端上表现出劣势
- 使用原生 js 的语法,无需编译,将会减少编译时间
- 更好的支持 tree-shaking,且不再需要 babel-plugin-import
- javascript 显式的变量定义、模块引用等语言特性跟踪样式的使用情况,大大方便了对样式代码的重构
- 缺点
- 运行时消耗
- 可读性降低
由于 HTML 代码和 CSS 样式之间没有显式的一一对应关系,很难辨认出项目中哪些是有用的,哪些是无用的,这就导致了我们不敢轻易删除代码中可能是无用的样式。这样随着时间的推移,项目中的 CSS 样式只会增加而不会减少。无用的样式代码堆积会导致以下这些问题:
- 项目变得越来越重量级,加载到浏览器的 CSS 样式会越来越多,会造成一定的性能影响。
- 开发者发现他们很难理解项目中的样式代码,甚至可能被大量的样式代码吓到,这就导致了开发效率的降低以及一些奇奇怪怪的样式问题的出现。
antd 动态换肤
还挺折磨人的,并没有找到非常优雅的方案。
换肤血泪史,踩的那些坑
- antd-theme-webpack-plugin 插件的 varFile 默认值并不适用我项目,因此必须提供,否则提示找不到文件
- 使用 antd-theme-webpack-plugin 插件,配置完后直接启动成功,但并没有效果,原因是编译时其实报错了,但仅仅是 catch 住进行了打印,项目启动时会刷新控制台,从而看不到报错信息,手动将源码改成将错误抛出
- 报错找不到 @{root-entry-name} 对应的文件,这是 antd@4.17 引入的问题,一开始找到魔改源码的方式解决,但后来发现 antd-theme-generator 包作者已经解决了,更新新版即可
- 提供 ceil argument should be a number,一开始以为是 less 版本的问题,因为确实 less@4.x 版本有破坏更新,但我的版本是 3.9,各种锁版本,配置 math: "always" 参数都无法解决,但官方示例确实是 OK 的。最终只能定位报错的代码,将代码输出到文件,二分法自行通过 less.render 进行编译,最后锁定不是 less 版本的问题,而是在文件末尾,追加了一堆变量,将类型于 @font-size-base 等值,修改成了颜色
最终实施了不太满意的解决方案
- 去掉 babel-plugin-import 按需加载功能,采用全量加载
- 将自身组件库移除 import 插件后,导致 storybook 样式也丢失,需要对 storybook 的启动脚本进行单独配置
- 由于我们需要对一些 Less 变量进行定制,因此采取手动将 antd.less 和 antd.dark.less,调用 less 的 api 手动编译出对应的 css 资源
- 通过切换 css 的方式完成样式的切换
- 存在的问题
- 可能导致加载了很多不需要的样式
- 没有 minifyCss,css 资源体积可能过大,可解决
- 当前没有启用 postcss,可能存在兼容性问题,可解决
- 并没有对资源加 hash 值,可能导致缓存问题,可解决
了解到一个非常快捷实现暗黑模式的方案 darkreader
- 原理:智能通过白色主题推演出黑色主题
- 优点:使用非常简单,无侵入性
- 缺点:无法进行精细化的控制