Better

Ethan的博客,欢迎访问交流

关于 UI 规范与 CSS 换肤

整理项目 CSS 存在的问题,如何减轻样式编写痛感,以及如何实现动态换肤。

当前困境

整理了一下当前项目中前端样式编写的困境

  • 使用了开源的 antd 组件库,但设计师多少想改组件默认的样式
  • 自己编写的组件,即使在定义了 CSS 变量的情况下,仍然不是很规范,比如有变量但未使用,或设计稿本身没有按照规范

关于样式配置

  • 主要分为两部分
    • 基础样式
    • 组件样式
  • 基础样式:当修改基础样式的配置时,对全局样式有着强烈的影响
    • 基础样式主要包括 Arco Design 设计系统中定义的一系列的粒子变量,如颜色,字体,尺寸等,是设计同学和前端同学约定的一套规范。他们作为基础变量,被 Button,Checkbox 等基础组件所引用。而 Checkbox,Button 等基础组件又是 Modal,Tree 等复杂组件的一部分。所以对基础变量的修改会直接影响到全部组件的设计风格。
  • 组件样式配置:细粒度的定制单个组件的样式
  • 开发者模式:针对无法通过配置项实现的需求

对于第一个问题,首先就是要接受这种现象,UI 设计师和开源组件库有着难以调和的矛盾。比如我接触的最近三位设计师,要我实施定制的部分就各不相同。所以那我们就只能一直改改改吗?当然不是,这样只会影响我们的开发效率,因此当我们碰到这种情况时,需要多走一步,需要推进把修改变成规范,使之变成一次性工作,而不是反复,具体方式有:

  • 最优解:通过组件库暴露的变量进行修改(antVarOverrides.js)
  • 次解:通过样式覆盖的方式(antd.less)

警惕对于可交互对象的样式修改,由于其通常有如下四种状态,如果单修改其中某个颜色,会导致和其他状态搭配起来很尬,同时注意不要滥用 !important 属性,从而导致失去交互效果。

  • 正常态
  • 悬浮态
  • 激活态
  • 禁用态

我们思考一下哪些规范是至关重要的呢,从而先解决最重要的问题?我们可以从使用场景出发,比如我们设计一套组件库的时候,首先要考虑颜色、字体、边框、图标这些基础元素的设计,它们是构建各个组件的基石。或者我们可以将来变化的角度出发,比如需要实现暗黑主题或紧凑主题,我们最关心的 CSS 变量就会是

  • 颜色变量
    • 文本颜色
    • 背景色
    • 边框、分割线、图标颜色
  • 间距变量

当然不是说其他的就不重要,还有如图所示的基础定义也是很重要的,只是说影响范围相对没有那么大。 css-design.png

如果项目中早已定义好相关变量,且各组件中是严格按照规范开发的,这样实现暗黑或紧凑主题就会轻松。

总结了一下当前项目中样式的重灾区。正好指向的就是我们关心的颜色和间距这两部分,相对而言比较没有规范。

  • 关于品牌色和功能色这一块争议不大。主要是背景色最为严重,比文本和边框色严重的多
  • 间距这一块,padding 和 margin 类似,使用了大量不在规范里的数值

表现较好的有:

  • 字号由于之前定义了规范,仅存在少量没使用变量的情况,但是属于规范里数值
  • 圆角由于使用较少,问题同字号

CSS 审查

本次 CSS 审查过程中发现团队编写 CSS 中存在如下问题

跟规范相关问题

  • 未跟推进规范建设相关:不思考对于组件的样式覆盖是否可以规范化,只是一味的去覆盖,甚至同一个组件,相同的覆盖方式被不知不觉实现了多次
  • 设计师犯的小错误,导致错误就被实现了,常见于相近的间距或相近的颜色
  • 使用 style 属性写样式,从而无法使用 less 变量,应该尽量避免
  • 错误的写法相关,导致出现了很多规范之外的事情
    • 有时候是百分比或对齐问题,而不是一个离谱的不符合规范的间距值
    • ……

工程师容易犯的错误

  • 面向自身浏览器编程,不考虑不同分辨率下情况:max-height: 600px、最多 15 组参数
  • 面向当下编程,只关注当下使用场景,导致 CSS 完全没有伸缩性:width: 216px、height: 82px
  • 只看现象,不看结构:margin 的奇淫技巧

打破困境

正确上述问题,我们该如何打破困境

  • 最重要的一步:和设计师对于规范的理解达成一致,并严格执行
    • 为避免变量名冲突,所有 variable 下定义的名称使用 <shorthand-company> 前缀
  • 工程师要主动推动规范建设,不要只关注当下实现
    • 当你发现你需要疯狂去覆盖组件库自身样式时,就要开始考虑是不是该统一规范了
  • 人工 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 文件实现换肤

实现动态换肤方案总结 css-theme.png

了解到这里,发现 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

  • 原理:智能通过白色主题推演出黑色主题
  • 优点:使用非常简单,无侵入性
  • 缺点:无法进行精细化的控制


留言