组件仍在补充, 欢迎加群交流哦,微信: a2298613245
headless ui
✕
组件还在补充中,希望能够帮助你有一个亮点项目放入简历!
全局方案
按钮 Button
图标 Icon
布局 Grid
间距 Space
输入框 Input
弹窗 Modal
弹出框 Popover
消息 Toast
警告 Alert
单选框 Radio
复选框 Checkbox
标签 Tag
其它组件
换肤方案
CSS 换肤方案
Tailwind 换肤方案
Next.js 换肤方案

css 换肤方案

孟祥_成都 🔥前端技术专家,全栈开发工程师
button

介绍

其实现在社区主流的 css 换肤方案一般分为两种:css in js 和 css 变量,但 css in js 某种程度也需要配置 css 变量。

在讨论 css 主题切换功能设计之前,我们不得不提一下目前很多组件库的困局,就是样式很难拓展,因为只要不是 headless 组件库(只包含 js 和 html 逻辑,不包含样式的组件库),都难免跟 css 有耦合之处,尤其是 在你们的项目对于样式修改粒度要求很细的层次时,只要你做过大项目,那个痛苦程度是很深的。这也是为什么国外现在非常流行 headless 组件库的原因。大家对定制化组件库样式的需求非常强烈。

那么我们来看看之前 ant design 4 版本之前, material ui 等主流组件库之前的样式为什么难以定制化?

ant design 的困局

在 ant design 5之前,你知道为什么用以下方式导入 ant 组件吗?并且这样的导入方式为什么连 css 也一并导入进来了?

1import { message } from 'antd';

是因为 ant 为了实现 css 的按需加载(就是我使用了 message 组件,我只加载 message 组件的 css 样式,不加载别的组件的 css),使用 babel-plugin-import,使用方法如下

官方文档这样描述:

1npm install babel-plugin-import -D

然后在 .babelrc 配置文件中,举个例子可以添加以下配置代码:

1{ 2 "plugins": [ 3 ["import", { 4 "libraryName": "antd", 5 "libraryDirectory": "libs", 6 "style": "css" 7 }] 8 ] 9}

原理是什么呢?

其实是 babel 在编译你的代码的时候,会自动帮你引入对应组件的 css,也就是帮你自动插入 css

可是成也 babel-plugin-import,败也 babel-plugin-import,这样确实很方便,对于新手用户而言。但问题又很明显,很多人用 ant-deisgn 都不知道为什么像我们上面那样导入组件, 怎么 css 也导入进来了,这样的话你根本没办法对 ant 做样式上的定制化改造。

所以很多项目,包括大厂很多项目,甚至我在用飞书的时候,飞书最开始也存在的问题,就是 css 覆盖,大家普遍的思路都是用新的,同名的 css 去覆盖 ant 的样式。

问题也很明显,css 维护起来很糟心,尤其是大型项目,因为每个模块可能都是不同的项目组在维护,很可能一个组覆盖的 css 影响了另一个组。

正确的解决办法是什么呢?(因为很多国内组件库都是这样的写法,例如字节的 arco design, 腾讯的 t-design 等等,所以介绍如何正确改造也比较重要)。

首先,不要引入 babel-plugin-import,然后单独封装每个组件的 css,举个例子,我们有一个 ant4 版本的 Button 组件,我们这样改造:

1// Button组件 2import { Button } from 'antd' ; 3// 这里引入的是 css,当然你的项目用 sass 或者 less,改成相应的就行了 4import './style/index.css'

一般你只有用 ant 的 less(当然其它的库也有对应 css 的预处理器),改造成本才比较小。

1/** 这里复制粘贴 ant button 的css 然后改其中的 css 样式*/

当然,如果你也可以用

1import './style/index.css'

改为

1import './style/index.less'

但还需要把 less 里的公共变量也需要一起改了,这个就更复杂一些。

所以你可以看到,ant design 在更改样式这块很不容易,这也是 ant5 为什么放弃 less,转向了 css-in-js,因为 css-in-js 相当于把 css 全部交给 js 去处理,这样我们在 js 里修改变量相当于修改了 css。

css-in-js 真的好用吗?

css-in-js 相当于给你开放一些css变量让你动态设置,我们看下 ant design 5 的文档是怎么使用的。

通过在 ConfigProvider 中传入 theme 变量,可以配置主题。在升级 v5 后,将默认使用 v5 的主题,以下是将配置主题示例:

1import { Button, ConfigProvider } from 'antd'; 2 3const App: React.FC = () => ( 4 5<ConfigProvider 6theme={{ 7 token: { 8 colorPrimary: 'red', 9 }, 10}} 11> 12<Button>按钮</Button> 13</ConfigProvider> 14); 15 16export default App;

这将会得到以 #00b96b 为主色的主题的 Button。

也就是我们其实也不是随心所欲的去修改 css,也是在人家开放接口的范围内修改。如果你想修改的 css 样式不在其提供的修改样式的接口里,你还是不能定制化自己的样式。

所以 css-in-js 并没有想象中那么完美,任何事情没有好坏只有适不适合,如果的业务定制性没有那么高,其实 css-in-js 也能接受(如果定制性很弱,其实 ant4 的 css 方案也挺好的),但如果定制性非常强,一定是选择 headless 组件库是更好的。

如果我们用 less 或者 sass,其实也能做到类似 css-in-js 的功能,怎么做呢,休息喝口水,咋们接着看。

css 变量换肤方案

之前做过一个名为 @mx-design 的组件库,我觉得这样的 css 主题色设计方案还挺好用的。

我们拿 button组件为例,最终打包的组件库,我会生成一个 index.css,这个 css 中包含了一些 css 变量,例如

1.mx-base-button { 2 display: inline-flex; 3 position: relative; 4 align-items: center; 5 justify-content: center; 6 outline: none; 7 padding: var(--btn-padding); 8 height: var(--btn-height); 9 appearance: none; 10 user-select: none; 11 cursor: pointer; 12 white-space: nowrap; 13 transition: all 0.2s var(--transition-timing-function-standard); 14 box-sizing: border-box; 15 line-height: 1.5715; 16 border-radius: var(--btn-radius); 17}

然后,对外暴露一个修改 css 变量的方法,例如:

1import { isObject } from './is'; 2/** 3* 更换css变量的方法 4*/ 5export function setCssVariables(variables: Record<string, any>, root = document.body) { 6 if (variables && isObject(variables)) { 7 Object.keys(variables).forEach((themKey) => { 8 root.style.setProperty(themKey, variables[themKey]); 9 }); 10 } 11}

假设你有两份文件,分别是 light.ts 和 dark.ts,两份文件里的变量名都是一样的,只是值不一样,我们可以这样使用:

1import { setCssVariables } from './css'; 2import light from './light'; 3import dark from './dark'; 4 5// 切换到亮色主题 6setCssVariables(light); 7 8// 切换到暗色主题 9setCssVariables(dark);

light.ts 如下(dark.ts 跟 light.ts 的变量名一样,从而达到换肤的效果,同时也支持更多的皮肤):

1export const lightTheme = { 2 // base color 3 '--brand-color-1': '#f2f3ff', 4 '--brand-color-2': '#d9e1ff', 5 '--brand-color-3': '#b5c7ff', 6 '--brand-color-4': '#8eabff', 7 '--brand-color-5': '#618dff', 8 '--brand-color-6': '#366ef4', 9 // ... other variable 10 // brand color status 11 '--brand-color': 'var(--brand-color-7)', 12 '--brand-color-hover': 'var(--brand-color-6)', 13 '--brand-color-focus': 'var(--brand-color-2)', 14 '--brand-color-active': 'var(--brand-color-8)', 15 '--brand-color-disabled': 'var(--brand-color-3)', 16 // error color 17 // warning color 18 // success color 19 // ... 20 ’other‘: 'var(--other-color)', 21}

那么全局样式改变我们已经完成了,但一个好的 css 换肤方案一定要兼顾全局和局部的样式改变,也就是我只想改一个 Button 的样式,而不要一改变量牵扯到所有的 Button 样式不在其提供的修改样式的接口里,你还是不能定制化自己的样式。

我们的实现的目标为:

1、局部样式改变

比如所有的 button 默认都是蓝色,但是某个 button 我想是黄色,允许单独给 button 传递主题色。

2、全局样式改变

比如我想对所有的 button 的主色都设为绿色,提供一个全局更换颜色配置的入口。

全局的之前讲过了,我们来讲讲局部样式更改。

局部更换主题色

最开始我是想向 css-in-js 那样,传递参数,然后用 style 来设置样式,但是毫无疑问不能这样做,这样会影响 css 优先级,有些同学更改了 class 可能因为没有 style 优先级高而导致样式不生效,这就造成奇怪的体验。

当然这也是普通 css 不如 css-in-js 的地方,可以像传入 js 变量一样更改 css。但是我们也有办法。之前说了,这样独立样式的 button 毕竟不是常见的需求,因为一般大家的 ui 都有一套设计规范,我们在全局更换主题色即可。

这样单独修改的需求可以将从打包好的css样式中提取出对应组件的css,把样式单独更改后,和 js 一起导出。

也可以采取覆盖局部样式的方式。(不推荐)

现在想来,局部更换主题色似乎不太好做,还好,原生 css 变量支持 css 变量的作用域,什么意思呢?在 mdn 中,这种作用域被称之为继承性。以下转自 mdn 对继承性的解释和案例:

自定义属性会继承。这意味着如果在一个给定的元素上,没有为这个自定义属性设置值,在其父元素上的值会被使用。看这一段 HTML:

1<div class="one"> 2<div class="two"> 3 <div class="three"></div> 4 <div class="four"></div> 5</div> 6</div>

配套的 CSS:

1.two { 2--test: 10px; 3} 4.three { 5--test: 2em; 6}

在这个情况下, var(--test) 的结果分别是:

对于元素 class="two" :10px 对于元素 class="three" :2em 对于元素 class="four" :10px (继承自父属性) 啥意思呢,就是我的 button 组件,我可以默认用全局变量的样式,比如我设置在 body 上,然后 button 组件包裹一个 div, div 上也有同名的一个变量,那么 button 组件会优先使用 div 上的变量。

基于以上原理我可以在 button 组件里,直接在 style 中用来设置 css 变量,代码如下

1const localBtnTheme = { 2 "--btn-color": "red", 3 "--btn-width": 12 4}; 5 6<div style={{ style, ...localBtnTheme }} >

其中 style 是正常外面传给组件的 style, localBtnTheme 是指 css 变量

如果你有其他组件库主题切换的方案,欢迎在评论区,或者我的 github 上、微信组件库讨论群一起讨论哦

介绍
ant design 的困局
css-in-js 真的好用吗?
css 变量换肤方案
局部更换主题色