中后台 CSS Modules 最佳实践
工作中发现前端 CSS 的使用五花八门,有用 Sass,Less 这种预处理语言,还有 CSS in JS 这种奇葩玩法,还有 TailWindCSS 这种原子化的 CSS 方案,还有 CSS Modules 这种专注解决局部作用域和模块依赖问题的单纯技术。这么多种类,我们该怎么选呢,下面我介绍一种在现在微前端趋势下,在中后台项目中最好用的,开发体验最佳组合方式。
我们的这个最佳实践是以 CSS Modules 为基础的,为什么要选择他呢?在真实的工作中,我们遇到最痛的问题,就是样式的隔离,尤其是在微前端框架下,子应用之间,子应用和主应用之间,甚至同一个项目的不同页面之间都会有样式的覆盖,即使各种微前端框架都试图去解决样式隔离问题,不论是通过工程化加命名空间,还是 shadow DOM 的方式,都无法一劳永逸的解决,都有其弊端,相比于 Less ,Sass 这个技术,都要在每个页面或者组件上人为的想一个命名空间,这个过程没有技术上的约束,单靠人之间的口头规范是没有用的,但 CSS Modules 无疑是一种彻底解决样式冲突问题的方法。
CSS Modules 的文档相当简单,10 分钟内就能学会,而且基本主流的工程化工具和脚手架都是支持的,比如 vite 默认支持,CRA 也是天然支持,不需要任何额外的配置。
CSS Modules 开发体验极佳,写 CSS 从未如此丝滑,后面会详细介绍。
CSS Modules 由于他非常的单纯,因此 module.css 文件,依然是遵循 CSS 文件的规范的,因此不能写嵌套。为了解决这个问题,我们引入 Less,也就是使用 module.less 的文件格式,这样我们就可以借助 Less 的能力,写嵌套的代码了。
为什么不用 Sass 呢?其实 Sass 和 Less 本质上没有太多区别,也没有什么好坏之分,我选择 Less 的原因是,我的项目中大量使用 antd 的组件库,而 antd 使用的是 Less 的方案,而且如果要定制 antd 的主题,就必须用 Less。
有了 Less 以后就可以有效的弥补,CSS Modules 的很多不足,尤其是嵌套,比如下面的代码。
.container {
.header {
color: red;
}
}
Less、CSS Modules 都支持变量的定义和使用,我们挨个看看是怎么用的:
// 定义 common.less
@width: 10px;
@height: @width + 10px;
// 使用
@import './common.less';
.header {
width: @width;
height: @height;
}
// 定义 colors.css
@value blue: #0c77f8;
@value red: #ff0000;
@value green: #aaf200;
// 使用
@value colors: "./colors.css";
@value blue, red, green from colors;
.title {
color: red;
background-color: blue;
}
这两种方式在定义和使用上,都比较麻烦,尤其是在使用的时候,需要显式的导入,而我推荐的是另一种方式:就是 CSS 原生支持的方式。使用文档查看:MDN CSS Variables 基本使用方式如下:
// 定义全局变量
:root {
--main-color: #fff;
}
// 定义局部变量
.container {
--main-color: #000;
}
// 使用变量
.component {
color: var(--main-color);
}
我们可以看到,变量有明确的 -- 前缀,比较容易区分,而且使用方便不需要导入,而且很容易做覆盖。如果我们看最新版本的 antd-mobile 的组件库中,就大量使用这种原生的方式做主题的定制和样式的覆盖。
至于兼容性这块,在中后台场景下,Chrome 的支持是非常好的,基本不需要考虑。
在 Less 中有基于 extend 和 Mixins 的继承方式,但我觉得都没有 CSS Modules 的继承方式更方便,尤其是 Mixins 这种反常识的使用方式,一旦写不好代码就很容易散、并且不便于维护、新手难以理解。使用 CSS Modules 的 composes 的方式如下:
// 定义
.container {
color: #fff;
}
// 相同文件下调用
.component {
composes: container;
}
// 不同文件下调用
.component {
composes: container from './index.module.less';
color: #000;
}
如上述的代码,最终会被编译成 <div class="_container_i32us _component_iw22a"/>
且最终生效的 color 是 #000。
我们在平时的编码中经常会去覆盖第三方组件的样式,比如我们使用了 antd 中 Button 的样式,在 module.less 中,我们可以使用 :global
关键字,只要使用他的地方都不会在编译时自动添加 Hash,而且这种方式下,也可以给他设定唯一的父元素的 class ,这样你改变的第三方组件的样式就不会影响别的也同样引用该组件的地方的样式。
.container {
:global(.ant-button) {
color: var(--main-color);
}
}
如果一个组件的 class 可能需要多个,或者有可能需要一定的计算,传统的 CSS Modules 的使用方式是比较丑陋的,因此我们使用一种更为优雅的方式来解决,就是借助第三方 NPM 包,classnames 的能力。如下:
// 当 className 需要多个 class 的时候,我们直接使用 classnames 传多个参数的方式
<div className={classnames(style.container1, style.container2)} />
// 最终会编译成 <div class="_contianer1_i323u _container2_i889k" />
// 如果某个 class 是需要一定的逻辑判断的,可以把一个对象传入,用 value 的 false 或者 true
// 来控制 class 的有无
<div className={classnames({ [style.container1]: true, [style.container2]: false })} />
// 这种方式,是上面两种方式的组合,classnames 可以接收多参数,对象,甚至是数组
<div className={classnames('body', {[style.container1]: true, [style.container2]: false })} />
传统写 css 是很难通过编辑器在 JSX 的 div className 上,按住 cmd + 点击快速显示或者定位到样式代码的,但如果我们使用了 CSS Modules ,并且在安装了 VSCode CSS Modules 扩展以后。
如下图所示:我们就可以轻松实现定位和显示,甚至不需要切换到 Less 文件里。
当时真正使用的时候就知道有多爽了。
当然,使用 CSS Modules 还有一个巨大且显而易见的好处是,我们不需要纠结 class 的命名,不同组件内我们甚至可以定义相同的名字,比如:
import style from './index.module.less';
const Login = () => (
<div className={style.container}>
<div className={style.header}>登录</div>
</div>);
const Register = () => (
<div className={style.container}>
<div className={style.header}>注册</div>
</div>);
我们看到,Login 和 Register 组件,我们都使用了 container 和 header 两个 class ,而不需要在前面加组件的前缀。这样更有利于代码的复用,而且可以很好的表达页面的结构。
CSS Modules 用在项目的业务代码里是没有问题的,但如果我们想把一些组件做成 NPM 包给别人使用,如果我们用了 CSS Modules ,编译后的 NPM 包,也会把 class 上都加上 Hash 的,是动态变化的。因此当别人想覆盖你的样式的时候,就非常困难了。这个问题怎么解决呢?
确实,社区给出了一些答案,可以看看下面的文档:customizing-components
这里面提出了两个观点,一个是妄图去覆盖别人组件的样式,这本身就是一种 Hack 的行为,我们应该使用更优雅的方式实现,应该让 NPM 组件提供对应的 API 让外部调用修改,第二就是社区提供了一个工具包,react-css-themr,每个 NPM组件接受外部传 theme 参数(css module 对象),用来定义所有样式。示例如下:
import React from 'react';
import { AppBar } from 'react-toolbox/lib/app_bar';
import theme from './PurpleAppBar.css';
const PurpleAppBar = (props) => (
<AppBar {...props} theme={theme} />
);
export default PurpleAppBar;
上述最佳实践经过本人的多年验证,真实有效,童叟无欺,如果大家喜欢或者不喜欢都可以尝试用起来,早用早享受,晚用晚开心。
关注「Alibaba F2E」微信公众号把握阿里巴巴前端新动向
微信扫码关注该文公众号作者