React中CSS Modules的使用
最近项目开始换React的工程,感觉好多东西都得重新开始,特别在撸CSS的时候。说实话和Vue的工程相比,体感差完了。在Vue的工程中除了Modules之外还可以使用CSS的作用域scoped
的概念。用久了Vue的同学,在这方面的感觉爽死了,但是突然切到React的工程体系之下,这方面的感觉突然不要不要。拿个实例来说吧(可能我做得不到位),组件的CSS是全局的,有时覆盖起来,除了蛋疼之外,而费时,费成本。
React项目中写CSS的姿势
比如最简单的一个按钮组件,居然要这么撸:
虽然为每个组件创建一个单一的.scss
文件,并在入口引入相应的样式文件,发现React中的CSS没有域的概念,是全域的。
很多时候需要去覆盖组件初始样式,不得不重新定义样式类或提高选择器权重来处理。着实的蛋疼。除此之外,项目是多人开发,各种各样的类名都有,未统一起来,从覆盖上也增加了不少工作量。另外,还会碰到一些常见的CSS问题,比如:
- 全局污染
- 命名混乱
- 依赖关系复杂
- 无法共享变量
- 代码冗余,难维护
所以最近在重新考虑如何在React项目中编写CSS。以便找到一条更适合自己甚至团队编写、维护CSS的方式:
- 行内样式(在JS中写CSS,最终样式编译到标签元素的
style
内) - CSS-in-JS,较为流行的有
styled-components
、styled-jsx
、react-style
- CSS功能模块(Functional CSS),比如
tailwindcss
、tachyons
等(怎么看都有点类似早期的OOCSS) - CSS Modules
初步对比了React中编写CSS的几种方式,我个人更趋向于CSS Modules,不过在React项目中配置CSS Modules要比在PostCSS或者Vue中配置复杂的多(主要还是自己对Webpack太弱)。这次在配置的时候踩了一些坑,特意梳理一下,以备后用。
我要的目标
习惯了Vue的开发,开始撸React还真不习惯。而我想要的目标是:
React中编写CSS能不能像Vue一样,有作用域的概念,组件的CSS只作用于相应的组件,并不会影响其他组件。
为什么选择CSS Modules
时至今日都在提模块化管理,而前面提到的在React中处理样式的方案都有模块化的身影。而CSS模块化的解决方案主要分为:
- 彻底抛弃CSS,使用JavaScript或JSON来写,比如这两年聊得多的CSS-in-JS。其优点是能给CSS提供JavaScript同样强大的模块化能力;缺点是不能使用CSS处理器以及较难处理伪类选择器的样式
- 依旧使用CSS,但借助JavaScript来管理样式的依赖关系,比如我们今天要聊的CSS Modules。其最大的特点就是最大化的结合了CSS生态和JavaScript模块化能力
不管是哪种CSS模块化,其主要目的是解决:
- CSS样式的导入和导出:灵活的按需导入以便最大化的复用代码;导出时能隐藏内部作用域,以免造成不必要的全局污染
- 解决CSS的编程能力:虽然CSS的处理器(比如Sass、LESS、Stylus和PostCSS)赋予了CSS一些编程能力,但还有有鸡肋之处,它们依旧无法解决模块化最重要的问题
而在React中编写CSS时,有关于这些方面所涉及到的问题表现的更为真切,比如上面提到的:
全局污染
众所周知,CSS是没有作用域名的概念(虽然CSS自定义属性的出现,解决了一些作用域的问题),因此写的样式都是一个全局的。很多时候要去覆盖这些样式,也因此会造成样式可能被错误覆盖。搞不好,你会看到好多样式中会有!important
这样的关键词,甚至更为离谱的是,在行内样式中还会有!important
身影。
另一个更为复杂的是CSS选择器权重,更易于让CSSer犯错,从而也提高了样式代码覆盖的成本。仅管Web Components中的Shadow DOM能彻底解决这个问题,但它的做法有点极端,样式彻底局部化,造成样式重写难度,从而损失了灵活性。
命名混乱
由于全局污染的问题,加上多人协同开发,最易于造成的问题就是样式冲突。为了避免样式冲突,对于元素的命名就有着更高的要求,虽然很多CSS的方法论(比如BEM、OOCSS、Atomic、ITCSS等)可以让我们在编写CSS时尽可能的避免命名的冲突(样式混乱),但并没有彻底解决问题(特别是在一些新生团队),在写CSS的时选择器会非常的复杂(复杂到有超六、七层的嵌套,而最佳的是不超三层),而且命名风格还很难统一。
工程越大,人员越多,命名越乱,选择器越复杂,样式越难覆盖 —— 死循环。
依赖管理不彻底
编写组件最理想的状态 —— 应该相互独立。在引入一个组件时,应该只引入组件自己所需的样式。但现在的做法是除了要引入JavaScript之外,还要引入它的CSS(而CSS处理器以很难做到每个组件编译出单独的CSS)。而在独立的页面中引入所有CSS又会造成不必要的浪费(这也是我不喜欢而没选择Functional CSS原因之一)。
虽然JavaScript模块化已经非常成熟了。比如借助Webpack的css-loader
的能力,可以帮助我们管理CSS依赖。这也是目前较好的方案之一。
无法共享变量
复杂组件要使用JavaScript和CSS来共同处理样式(特别是一些强交互的组件),就会造成有些变量在JavaScript和CSS中冗余。而CSS的处理器是无法提供跨JavaScript和CSS共享变量的这种能力。
值得庆幸的是,CSS的自定义属性可以让我们在JavaScript和CSS共享变量的能力(注意,其实不是变量,是CSS自定义属性)。
代码压缩不彻底
很多压缩工具对于较常的class
类名压缩却无能为力。
事实上,上面提到的这几点都是CSS中一直以来存在的,而又难以解决的。不过借助JavaScript的能力来管理CSS的话,问题就会变得简单的多。这也是CSS-in-JS流行的主要原因,也就出现前面提到的现象,以对象的方式在JavaScript中撸CSS,从而也让不少的同学难以接受这种方式。但CSS Modules的出现,既可以借助JavaScript能力来管理CSS,也方便了CSSer撸代码的习惯,可以说一举两得。这也是为什么选择CSS Modules主要原因。
另外一点,CSS经过这么多年的发展,从SMACSS到OOCSS,再到BEM,可谓是不断的在优化和改进。而CSS Modules与实践单一职责原则的Web组件非常匹配。
我更为好奇的是CSS Modules在设计前端系统中的可能性。CSS Modules基本上是CSS文件,默认情况下类名的作用域名是局部的(本地的)。在任何语言中,全局作用域都被认为是一种不好的实践,而CSS却又是这样的一种模式,只不过这么多年来,大家都无耐的接受了这样的一个现实,也已经学会了在CSS中如何使用它。有了CSS Modules(和其他一些模式),我们就可以跳出CSS的全局作用域,构建模块化系统。
很多同学一直觉得CSS非常简单。事实上,CSS一开始的确很简单,但随着项目的增长,CSS会变得越来越复杂,越来越难以编写和维护,即使是专业的开发人员(CSS大神)也会发现很难在复杂的大型系统中维护和组织样式。(事实上,我也非常的害怕,特别是和一些不太了解CSS的同学一起开发项目)。
CSS Modules的出现,其主要目的就是解决CSS的问题:
通过封装CSS的组件作为闭包,从而遵循组件单一的职责。
如何在工程体系中构建CSS Modules
既然CSS Modules有这么大的优势,那么我们就需要在工程中构建CSS Modules。那么问题来了,如何构建CSS Modules呢?这是当下我们要去解决的问题。
使用Webpack来构建CSS Modules,这也是目前主流方式之一。
而在不同的工程中构建CSS Modules方式是不一样的,比如PostCSS、Vue、React等不同的工程体系中,构建的方式都不一样。除了PostCSS之外借助PostCSS相关插件,其他的工程体系(不管是Vue还是React),都可以借助Webpack来构建。而使用Webpack在React项目中构建CSS Modules又不是一件难事。
对于我这样不太了解Webpack工作机制的人来说,还是一件难事。最起码我觉得比Vue环境下难得了。这个时候我需要一位Webpack高级配置工程师和我一起来构建CSS Modules。
抛开所有JavaScript框架而言,不管是在哪处框架底下,都可以通过Webpack下的css-loader
在配置文件中的加载器选项来启用它。当然,如果你还依赖Sass或PostCSS这样的开发套件的话,那么会相对的增加配置难度。不过,只要一步一步来,一切都不是太大的问题。接下来的内容就来看如何在React的开发环境上构建CSS Modules。
- 如果你是在Vue环境下开发,需要构建CSS Modules的话可以阅读《Vue中的作用域CSS和CSS模块的差异》一文
- 如果你不借助任何JavaScript框架进行开发,但会使用PostCSS来构建开发体系,那么要构建CSS Modules的话可以阅读《PostCSS-modules: 让CSS变得更强大》一文
接下来的内容主要来看如何在React环境下配置CSS Modules以及如何使用CSS Modules。
CSS Modules在create-react-app休系下的的使用
React社区中有一个构建React开发体系的工具Create React App,俗称create-react-app
,有点类似于Vue社区中的Vue-CLI。
在这里不会阐述Create React App如何使用,更多的是会聊聊CSS Modules在Create React App构建的工程体系下如何工作。为了更好的用示例向大家演示CSS Modules的使用,我在Github上创建了一个仓库,在不同的分支下能看到相应的Demo效果。
使用Create React APP创建项目
你可以直接从Github上克隆我创建的示例:
git clone git@github.com:airen/css-modules.git
cd css-modules
cd react-modules
安装工程所需要的包:
npm i
npm start
这样工程就可以跑起来了。或者你使用:
npx create-react app <project-name> // 我这里创建的项目名称react-modules
cd project-name
npm start
你将看到的初始效果如下:
上面的这一切都并不重要,重要的是下面的内容 —— React中CSS Modules的使用。
注意,我写使用案例时的基本环境是
node: v10.9.0
、npm: v6.9.0
、Create React APP(v2)
。不同环境,估计效果略有差异,最终以你本地电脑运行结果为准。
使用Create React App 第二版本构建的React项目,在使用CSS Modules时,不需要做任何的配置。只需要创建.css
、.sass
或.sass
文件时有相应的要求,即**使用 [name].module.css
文件命名约定支持 CSS Modules 和常规 CSS 。 CSS Modules 允许通过自动创建 [filename]\_[classname]\_\_[hash]
格式的唯一 classname
来确定 CSS 的作用域。同样的,如是要是.sass
或.scss
的话,文件名格式应该是[name].module.sass
或 [name].module.scss
。
或许你会好奇,不需要配置就具备CSS Modules运行环境吗?事实上的确如此。当然,Create React APP环境默认配置了一些功能,如果这些功能达不到你工程所需的要求,那就需要手动进行配置。只不过Create React APP的配置文件隐藏的较深,需要执行不可逆转的命令:
npm run eject
执行完上面的命令之后,会新增两个目录script/
和config/
。
在config/
目录下可以看到两个配置文件webpack.config.js
和webpackDevServer.config.js
。如果你需要配置所需的功能,可以在webpack.config.js
中添加配置。如果你需要将项目打包输出的话,还得配置webpack.config.prod.js
。具体如何配置,这里不说了。因为太复杂了。在后面的内容,我们会聊聊Webpack中怎么配置CSS Modules(纯Webpack环境之下,即不依赖Create React APP构建的项目工程)。
CSS Modules的基本使用
前面提到过了,Create React APP默认具备了CSS Modules的功能:
在接下来的内容把重点放在CSS Modules的使用上。不管是在哪种环境之下使用,CSS Modules的使用都不会有太大差异,只会稍微的细节上的差异。
CSS Modules中类名的使用
将分支切换到
demo1
查看示例代码。
类名的使用是CSS中最基础的部分,那么CSS Modules中类名又是如何使用呢?第一个示例就向大家演示CSS Modules中类名的使用。
首先创建了一个基本组件Button
:
|--src/
|----Button/
|------Button.js // 组件模板在这个文件中构建
|------Button.module.css // CSS代码在这个文件中书写
前面提到过了,Create React APP中使用默认的CSS Modules功能,创建.css
文件时需要以[name].module.css
格式创建,正如上面示例中的Button.module.css
所示。
这个组件非常简单,就是一个按钮:
// Button.js
import React from 'react';
import styles from './Button.module.css';
class Button extends React.Component {
render() {
return (
<div className={styles.button} role="button">Click Me</div>
)
}
}
export default Button;
Button.module.css
中的代码也非常的简单:
// Button.module.css
.button {
--primary: #fe90af;
--color: #fff;
background: var(--primary);
color: var(--color);
padding: 5px 10px;
border-radius: 4px;
margin: 5px;
}
调用Button
组件之后,编译出来的HTML会像下面这样:
<div class="Button_button__1o_YA" role="button">Click Me</div>
对应的CSS的选择器.button
编译成了Button_button__1o_YA
,而CSS样式编译成:
.Button_button__1o_YA {
--primary: #fe90af;
--color: #fff;
background: var(--primary);
color: var(--color);
padding: 5px 10px;
border-radius: 4px;
margin: 5px;
}
效果如下:
CSS Modules将本地名.button
编译成全局名.Button_button__1o_YA
。在组件中(Button
)可以使用像.button
的名称来声明类名,而不必担心类名的冲突。如果你不信,可以写一个简单的小示例,假设在App.js
中使用一个带.button
类名的元素,看看是否会受组件Button
中的.buttonn
的影响:
<div className="button">我是一个带button类名的元素</div>
渲染出来的结果告诉我们,组件中的类名不会影响别的组件中的同类名。这已经达到我们想要的两个目的 —— 解决CSS的命名冲突 和 不会污染全局。
顺便说一下,.Button_button__1o_YA
中的__1o_YA
是一个随机的hash
值,以确保具有相同名称的多个CSS Modules的唯一性。
在Webpack的配置文件
webpack.config.dev.js
中可以,配置CSS Modules编译器如何重写类名,并且hash
值是可选的。
在继续往下介绍CSS Modules中类的使用前先打断一下,我们在引用Button.module.css
是以JavaScript对象的方式引入:
import styles from './Button.module.css';
其中styles
变量是一个JavaScript对象,如果将styles
打印出来,你会看到像下面这样的结果(就Button
组件的代码为例):
console.log(styles)
你或许习惯了
import './App.css';
这种姿势引入CSS文件,但使用CSS Modules时,还是需要稍作改变。不过有技术手段达到您想要的方式,稍后会聊到这方面的知识。
不少同学平时写类名喜欢采用BEM的方式,或者喜欢使用带中折号-
或--
方式。为了验证我们所说的,把App.css
更换成CSS Modules可以识别的文件格式App.module.css
,并在文件中使用带中折号声明的样式,在调用的时候会报错:
.App-header {}
<header className={styles.App-header}></header>
这个时候在编译的时候将会报错:
稍后的方法中我们会介绍,怎么处理这种方式,让其生效。
默认情况之下,使用中折号声明的类名在CSS Modules中是不生效的。这个时候需要将其更换成驼峰的书写格式,即将.App-header
换成.AppHeader
:
.AppHeader { }
<header className={styles.AppHeader}></header>
这个时候CSS Modules能正常的编译:
虽然不能使用带有中折号-
或双中折号--
命名的类名,但可以接受下划线命名的类名(_
或__
):
.App_logo { //... }
.App__header { //... }
<header className={styles.App__header}>
<img src={logo} className={styles.App_logo} alt="logo" />
</header>
另外需要注意一点,如果使用import styles from '[name].module.css'
方式引入CSS(即开启了CSS Modules方式),在模板中(JSX)中未使用styles.[classname]
,那么编译的时候会被忽略(哪怕是使用className
引用),比如:
<div className="App"></div>
这样调用,类名.App
并未生效:
值得注意的是,如果你在CSS文件中编写了一些CSS,而在模板中没有使用它们(或错误的使用它们),Webpack将跳过生成输出任何未使用的CSS,从而能减少代码大小。
CSS Modules中多类名的使用
请把分支切换到
demo2
查看示例代码。
很多时候,在写代码的时候,会使用多个类名来控制元素的样式。同样拿Button
组件来举例。我们可以借助ES6的相关特性来给元素调用多个类名:
<div className={`${styles.button} ${styles.buttonPrimary}`} role="button">Button</div>
前面提到过了,styles
是一个JavaScript的对象,所以使用上面那种方式引用多个类名,可以正常的工作。上面的代码编译之后的结果是:
<!-- 编译出来的HTML -->
<div class="Button_button__1o_YA Button_buttonPrimary__2NC-O" role="button">Button</div>
// 编译出来的CSS
.Button_button__1o_YA {
--primary: #f36;
--color: #fff;
padding: 5px 10px;
font-size: 1rem;
margin: 5px;
border: 1px solid currentColor;
border-radius: 5px;
cursor: pointer;
transition: all .28s ease;
}
.Button_buttonPrimary__2NC-O {
background: var(--primary);
color: var(--color);
}
.Button_buttonPrimary__2NC-O:hover {
box-shadow: 0 0 3px 5px var(--primary);
}
上面示例演示的效果非常的简单,.buttonPrimary
是一个主色按钮,在<div>
上同时引用.button
和.buttonPrimary
两个类名的主要作用是让.buttonPrimary
同时具备按钮的基本样式.button
。
如果你不习惯上面那种设置多类名的方式的话,可以使用CSS Modules中的另一个特性 —— 组合特性composes
。那么上面的示例,我们可以修改成像下面这种方式:
// Button.module.css
.button {
//...
}
.buttonPrimary {
composes: button;
background: var(--primary);
color: var(--color);
}
.buttonPrimary:hover {
box-shadow: 0 0 3px 5px var(--primary);
}
<!-- Button.js -->
<div className={styles.buttonPrimary} role="button">Button</div>
编译出来的结果:
<!-- HTML -->
<div class="Button_buttonPrimary__2NC-O Button_button__1o_YA" role="button">Button</div>
<!-- style -->
.Button_button__1o_YA {
--primary: #f36;
--color: #fff;
padding: 5px 10px;
font-size: 1rem;
margin: 5px;
border: 1px solid currentColor;
border-radius: 5px;
cursor: pointer;
transition: all .28s ease;
}
.Button_buttonPrimary__2NC-O {
background: var(--primary);
color: var(--color);
}
.Button_buttonPrimary__2NC-O:hover {
box-shadow: 0 0 3px 5px var(--primary);
}
最终得到的效果是一样的。
CSS Modules最大的特色之一就是“反正全局CSS,每个CSS代码都是应该用于特定的组件中,并且与项目的其他部分是隔离的”。也正因为这个特性,刚接触CSS Modules的同学可能会担心,如果多个组件中要使用相同的CSS时是不是要写多个CSS(做着相同的事情)。事实上大家一点都不用担心,CSS Modules的composes
特性可以用来跨文件引用相同的样式(上面演示的是同一文件引用类样式)。在上面的示例上稍作调整。比如我们在App.module.css
中要引用Button.module.css
中的.button
中的样式代码,我们就可以像下面这样来写:
<!-- App.module.css -->
.composeClass {
composes: button from './Button/Button.module.css';
composes: buttonPrimary from './Button/Button.module.css';
}
<!-- Button.module.css -->
.button {}
.buttonPrimary {}
.buttonPrimary:hover {}
<!-- App.js -->
<div className={styles.composeClass}>合并别的文件中的类</div>
编译出来的结果:
<!-- HTML -->
<div class="App_composeClass__23fyv Button_button__1o_YA Button_buttonPrimary__2NC-O">合并别的文件中的类</div>
<!-- Style -->
.App_composeClass__23fyv {
}
.Button_buttonPrimary__2NC-O {
background: var(--primary);
color: var(--color);
}
.Button_button__1o_YA {
--primary: #f36;
--color: #fff;
padding: 5px 10px;
font-size: 1rem;
margin: 5px;
border: 1px solid currentColor;
border-radius: 5px;
cursor: pointer;
transition: all .28s ease;
}
最终的效果是一样的:
CSS Modules中的composes
特性有点类似于Sass中的@extend
,可以扩展@mixins
和%placeholder
中声明的样式。
如此一来,可以把各个组件共用的样式放置在一个独立的文件中,比如common.css
。然后在需要的地方调用。来看一个小示例:
// common.css
.primary {
--primary: #f36;
}
.white {
--color: #fff;
}
.p10 {
padding: 10px;
}
// Button.module.css
.buttonPrimary {
composes: primary from '../common.css';
composes: white from '../common.css';
background: var(--primary);
color: var(--color);
}
// App.module.css
.composeClass {
composes: primary from './common.css';
composes: white from './common.css';
composes: button from './Button/Button.module.css';
}
<!-- Button.js -->
<div className={styles.buttonPrimary} role="button">Button</div>
<!-- App.js -->
<div className={styles.composeClass}>合并别的文件中的类</div>
编译出来的结果:
<!-- HTML -->
<div class="Button_buttonPrimary__2NC-O Button_button__1o_YA" role="button">Button</div>
<div class="App_composeClass__23fyv common_primary__3GgzT common_white__1wVkU Button_button__1o_YA">合并别的文件中的类</div>
<!-- Style -->
<style type="text/css">
.common_primary__3GgzT {
--primary: #f36;
}
.common_white__1wVkU {
--color: #fff;
}
.common_p10__1huS7 {
padding: 10px;
}
</style>
<style type="text/css">
.Button_button__1o_YA {
--primary: #f36;
--color: #fff;
padding: 5px 10px;
font-size: 1rem;
margin: 5px;
border: 1px solid currentColor;
border-radius: 5px;
cursor: pointer;
transition: all .28s ease;
}
.Button_buttonPrimary__2NC-O {
background: var(--primary);
color: var(--color);
}
.Button_buttonPrimary__2NC-O:hover {
box-shadow: 0 0 3px 5px var(--primary);
}
</style>
<style type="text/css">
.App_App__16ZpL {
text-align: center;
}
.App_AppLogo__2NrNP {
animation: App_App-logo-spin__1e7sv infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
.App_AppHeader__2Hhu3 {
background-color: #282c34;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
font-size: calc(10px + 2vmin);
color: white;
}
.App_AppHink__3RXs- {
color: #61dafb;
}
@keyframes App_App-logo-spin__1e7sv {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
.App_composeClass__23fyv {
}
</style>
在使用CSS Modules时,composes
是非常强大的,可以让你跳出CSS Modules局部作用域名的限制,在组件中(局部作用域)调用共用样式(全局作用域)。这一点非常有用,可以让我们很好地在CSS Module中管理共用样式。比如你想使用Bootstrap中的CSS代码,或者你想使用tailwindcss(Functional CSS典型的作品之一)时,就可以采用该特性。
继续加到CSS Modules中多类名的使用中来。虽然composes
可以达到
className={`${styles.primary} ${styles.white}`}
类似的功能。但对于习惯于class="primay white"
的开发同学来说,或多或少有所不习惯。其实除了上面这两种方法之外,还可以借助classnames
模块,让多类名的使用变得更为轻便。
如果需要使用classnames
的话,需要先执行下面的命令来安装该功能模块:
npm install classnames --save
并在需要使用该功能的.js
中像下面这样引入:
// App.js
import cn from 'classnames';
那么就可以像下面这样在元素上同时使用多个类名:
<!-- App.js的模板中 -->
<div className={cn(styles.border, styles.box)}>classnames模块</div>
// App.module.css
.border {
border: 2px solid #f25;
}
.box {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
color: #fff;
border-radius: 4px;
margin: 5px;
}
编译出来的结果如下:
<!-- HTML -->
<div class="App_border__3Ekwc App_box__aqO3i">classnames模块</div>
<!-- Style -->
.App_box__aqO3i {
display: flex;
justify-content: center;
align-items: center;
padding: 10px;
color: #fff;
border-radius: 4px;
margin: 5px;
}
.App_border__3Ekwc {
border: 2px solid #f25;
}
有关于
classnames
更详细的介绍和使用可以查阅官方文档。
CSS Modules中的组合选择器的使用
请将分支切换到
demo3
查看示例代码。
CSS中组合选择器的使用是非常常见的。那么在CSS Modules中的组合选择器怎么使用呢?咱们先来看后代选择器(也常称嵌套选择器)。
// App.module.css
.button {
padding: .5rem;
margin-top: .5rem;
border: 1px solid #2F79AD;
border-radius: 4px;
background-color: #6DB9EE;
}
.fun .button {
font-weight: bold;
background: linear-gradient(
90deg,
#ff0000, #ffff00,
#00ff00, #00ffff,
#ff00ff, #ff0000
);
}
<!-- App.js -->
<button className={styles.button}>Regular Button</button>
<div className={styles.fun}>
<button className={styles.button}>FUN BUTTON</button>
</div>
编译出来的结果如下:
<!-- HTML -->
<button class="App_button__13pio">Regular Button</button>
<div class="App_fun__1wmPx">
<button class="App_button__13pio">FUN BUTTON</button>
</div>
<!-- Style -->
<style type="text/css">
/* .... */
.App_button__13pio {
padding: .5rem;
margin-top: .5rem;
border: 1px solid #2F79AD;
border-radius: 4px;
background-color: #6DB9EE;
}
.App_fun__1wmPx .App_button__13pio {
font-weight: bold;
background: linear-gradient(
90deg,
#ff0000, #ffff00,
#00ff00, #00ffff,
#ff00ff, #ff0000
);
}
</style>
效果如下:
再来看一个子选择器的示例:
// App.module.css
.box > .button {
background: linear-gradient(to right, #f36, #f90);
color: #fff;
}
// App.js
<div className={styles.box}>
<button className={styles.button}>BOX BUTTON</button>
</div>
编译出来的结果如下:
<!-- HTML -->
<div class="App_box__aqO3i">
<button class="App_button__13pio">BOX BUTTON</button>
</div>
<!-- Style -->
<style type="text/css">
/* ... */
.App_button__13pio {
padding: .5rem;
margin-top: .5rem;
border: 1px solid #2F79AD;
border-radius: 4px;
background-color: #6DB9EE;
}
.App_box__aqO3i > .App_button__13pio {
background: linear-gradient(to right, #f36, #f90);
color: #fff;
}
</style>
至于组合选择器,其实和前面提到的多类名的使用有点类似。只是稍微在细节上有所不同。比如基于上面的.button
再加一个.disabled
的样式,而且使用的是组合选择器:
// App.module.css
.button.disabled {
background-color: #aaa;
border-color: #999;
}
// App.js
<button className={`${styles.button} ${styles.disabled}`}>Disabled Button</button>
编译出来的结果我想你也能猜得到一二:
<!-- HTML -->
<button class="App_button__13pio App_disabled__sQeTY">Disabled Button</button>
<!-- Style -->
<style type="text/css">
/* ... */
.App_button__13pio {
padding: .5rem;
margin-top: .5rem;
border: 1px solid #2F79AD;
border-radius: 4px;
background-color: #6DB9EE;
}
.App_button__13pio.App_disabled__sQeTY {
background-color: #aaa;
border-color: #999;
}
</style>
至于其他的组合选择器的示例就不再演示了。感兴趣的可以自己尝试一下。
CSS Modules中选择器的作用域
请把分支切换到
demo4
查看示例代码。
通过上面内容,应该都了解到CSS Modules中的选择器默认都是局部作用域名的。其实在CSS Modules中还有两个标识符可以用来区别局部作用域名和全局作用域。
:local
:局部作用域名,相当于组件中的本地选择器:global
:全局作用域名
我们来看一个小示例,假设我们在App.module.css
中的某个选择器前面使用:global
来标识该选择器是全局的:
// App.module.css
.App {
text-align: center;
}
:global .App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
// App.js
<div className={styles.App}>
<header className={styles.AppHeader}>
<img src={logo} className="App-logo" alt="logo" />
</header>
</div>
编译出来的结果如下:
<!-- HTML -->
<div class="App_App__16ZpL">
<header class="App_AppHeader__2Hhu3">
<img src="/static/media/logo.5d5d9eef.svg" class="App-logo" alt="logo">
</header>
</div>
<!-- Style -->
<style type="text/css">
.App_App__16ZpL {
text-align: center;
}
.App-logo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
/* ... */
</style>
不知道大家是否有留意到,采用:global
声明的全局选择器,和我们平时声明类名无差异,而且也不需要遵循CSS Modules的类名命名规则,并且在模板中调用的时候,也不需要使用styles.xxx
这样的方式,可以直接在className
中引用类名。比如上例中的className="App-logo"
。而且还可以跨组件的使用,比如我们在Button
组件的Button.module.css
中使用:global
声明了一个全局的.button
样式:
// Button.module.css
:global .button {
padding: .5rem;
margin-top: .5rem;
border: 1px solid #2F79AD;
border-radius: 4px;
background-color: #6DB9EE
}
同时我们在App
组件中也有一个Button
,同时共用了Button
组件中的.button
类:
// App.js
<div className={`${styles.primary} button`} role="button">Primary Button</div>
// App.module.css
.primary {
background: linear-gradient(to right, #f35, #d0f);
color: #fff;
}
编译出来的结果如下:
<!-- HTML -->
<div class="App_primary__2eHgs button" role="button">Primary Button</div>
<!-- Style -->
.button {
padding: .5rem;
margin-top: .5rem;
border: 1px solid #2F79AD;
border-radius: 4px;
background-color: #6DB9EE;
}
.App_primary__2eHgs {
background: linear-gradient(to right, #f35, #d0f);
color: #fff;
}
从编译出来的结果可以看到,跨组件中使用:global
标识符声明的类名可以全局中使用。
这种场景也是非常有用的。比如使用JavaScript生成的类,它是一个全局作用域的,而不是局部作用域。比如你需要给当前的菜单项添加一个.active
类名时(由于使用JavaScript或React自己身的方法生成的类都是全局作用域的),:global
声明的类就非常有用了,比如:
:globa .active {
color: #fff;
border-bottom: 1px solid #f3450a;
}
CSS Modules中的@规则的使用
请把分支切换到
demo5
查看示例代码。
在CSS中写样的的时候,我们会遇到使用@
规则的样式,比如媒体查询@media
、条件CSS判断@supports
以及动画关键帧@keyframes
等。那么在CSS Modules中会有什么不同吗?我们来看两个有关于@
规则的使用,一个是媒体查询@media
,另一个是@keyframes
。
比如,在App.module.css
中使用@keyframes
创建了一个App-logo-spin
的动画:
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
并且该动画运用于.AppLogo
上:
.AppLogo {
animation: App-logo-spin infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
最后编译出的代码如下:
<!-- HTML -->
<img src="/static/media/logo.5d5d9eef.svg" class="App_AppLogo__2NrNP" alt="logo">
<!-- Style -->
<style type="text/css">
.App_AppLogo__2NrNP {
animation: App_App-logo-spin__1e7sv infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
@keyframes App_App-logo-spin__1e7sv {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
</style>
动画名称App-logo-spin
会自动添加相应的hash
值,变成了App_App-logo-spin__1e7sv
,并且运用于相应的元素之上。
再来看一个@media
的运用:
// App.module.css
@media (max-width: 600px) {
.AppLogo {
opacity: .6;
}
}
编译出来的代码如下:
@media (max-width: 600px){
.App_AppLogo__2NrNP {
opacity: .6;
}
}
.App_AppLogo__2NrNP {
animation: App_App-logo-spin__1e7sv infinite 20s linear;
height: 40vmin;
pointer-events: none;
}
CSS Modules中标签元素和属性选择器的使用
在CSS Modles中,类名是被限定在定义它的组件中使用(除非使用了:global
标识的类),但对于HTML的标签元素和属性选择器是不受影响的。比如,在Button
组件中声明了下面的样式代码:
// Button.module.css
[href^="https:"]{
color: salmon;
border-bottom: 3px double currentColor;
padding-bottom: 5px;
}
a{
text-decoration: none;
}
虽然属性选择器和标签元素是在Button
组件中声明的,但其还是成为全局的样式,被运用到了App
组件中的带有href="https://"
开头的<a>
标签上。来看看编译出来的代码:
<!-- HTML -->
<a class="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a>
<!-- Style -->
<style type="text/css">
[href^="https:"]{
color: salmon;
border-bottom: 3px double currentColor;
padding-bottom: 5px;
}
a{
text-decoration: none;
}
</style>
效果如下:
如果你不想让标签元素和属性选择器对应的样式被作用于全局的话,可能使用局部作用域名的类名来稍加限制,比如:
.panelBody [href^="https:"]{
color: salmon;
border-bottom: 3px double currentColor;
padding-bottom: 5px;
}
这样只会作用于.panelBody
容器下带有href="https:"
和元素。
另外,在CSS Modules中使用id
声明的样式不会被运用到元素上,比如:
// Button.module.css
#button {
padding: .5rem;
margin-top: .5rem;
border: 1px solid #2F79AD;
border-radius: 4px;
background-color: #6DB9EE;
}
// Button.js
<div role="button" id="button">Button</div>
虽然JSX模板中的div
显式声明了id
名称,并且和样式中的#id
相同,但样式并没有运用到相应的元素上。如果我们把styles
对象打印出来,看到有值输出:
顺便再来看一下伪元素或伪类选择器在CSS Modules中的使用。比如:
// Button.module.css
.btn::after{
content: '外部链接';
}
如果组件中显式调用了.btn
类,伪元素::after
也会相应的编译出来:
CSS Modules和全局样式共存
CSS Modules中声明的样式仅运用于相应的组件范围内,帮助我们解决了CSS中很多痛苦的问题。而事实上,我们很多时候可能会引用于第三方库的样式,比如前面提到的Bootstrap的样式。甚至很多时候希望要一些共用样式,比如基础样式normalize.css
。如果需要让CSS Modules和全局样式共存的话,就需要借助Webpack的能力,在配置上做相应的设置。比如:
rules: [
{
test: /\.css$/,
include: '/src/app',
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
sourceMap: true,
modules: true,
localIdentName: '[local]___[hash:base64:5]'
}
}
],
},
{
test: /\.css$/,
include: '/src/styles',
use: [
{
loader: 'style-loader',
},
{
loader: 'css-loader',
options: {
sourceMap: true
}
}
],
}]
比如上面所示的配置,在/src/app/
目录下的CSS将会遵循CSS Modules的能力,将相应的类名编译成随机的hash
值,而在/src/styles/
目录下是一些全局样式文件,不会被编译。
CSS Modules中样式覆盖
请把分支切换到
demo7
查看示例代码。
CSS Modules虽然随机生成了带有hash
值的类名,在多人协作的时候再也不用担心类名的冲突。但随之也来了新的问题,我们始终无法预知最终生成的类名是什么。而且同一个组件有可能会在不同的地方调用,在样式如果有细节上的差异,就难免需要组件原有样式进行覆盖。那么在CSS Modules环境之下,又如何来覆盖原有的样式呢?
比如,我们在Button
组件中在.button
中定义了一个样式:
// Button.module.css
.button {
padding: .5rem;
margin-top: .5rem;
border: 1px solid #2F79AD;
border-radius: 4px;
background-color: #6DB9EE;
}
虽然在App
组件中调用了Button
组件,并且也使用了className
调用一个新的类,比如primary
:
<Button className={styles.primary}></Button>
同时在App.module.css
重新定义了样式:
.primary {
background: linear-gradient(to right, #f36, #90a);
color: #fff;
}
你是不是和我一样认为能覆盖呢?事实上并非如此,未能如愿。主要是className={styles.primary}
并未编译到Button
组件的按钮标签中:
<div class="Button_button__1o_YA" role="button">Button</div>
这问题出于组件设计不够好,无法透传别的类名。暂时先不表,后面再来聊这个透传类名的事情。
那么我们有没有别的办法可以进行覆盖呢? 想过使用组合选择器,可是和前面一样,找不到区别按钮的标识符。那再换过一种方式,在调用组件的时候,显式声明一个data-*
的自定义属性,比如:
// App.js
<Button data-primary-button="primary"></Button>
// App.module.css
[data-primary-button="primary"] {
background: linear-gradient(to right, #f36, #90a);
color: #fff;
}
事实上还是未生效。再想个曲线救国,牺牲HTML结构:
// App.js
<div data-primary-button="primary">
<Button />
</div>
<div className={styles.primary}>
<Button />
</div>
// App.module.css
.primary > div,
[data-primary-button="primary"] > div{
background: linear-gradient(to right, #f36, #90a);
color: #fff;
}
样式覆盖成功了:
重新创建一个Button1
组件,定义一个data-type
的自定义属性,并且能动态传值:
// Button1.js
import React from 'react';
import styles from './Button1.module.css';
console.log(styles);
class Button1 extends React.Component {
constructor(props) {
super(props);
this.state = {}
}
render() {
const {type} = this.props;
console.log(this.type)
return (
<div className={styles.button} data-type={`${type}`} role="button" >Button</div>
)
}
}
export default Button1;
在App
组件中引用Button1
组件,并且通过动态传的type
值来覆盖样式:
// App.js
<Button1 type="success" />
同时在App.module.css
中设置需要覆盖按钮的样式:
[data-type="success"] {
background: linear-gradient(to bottom, #f09, #adf);
}
注意属性选择器在CSS Modules是全局的,其样式将会覆盖Button1
组件中的.button
样式:
刚撸React,上面透传参数的案例有可能写得不到位,欢迎路过的大婶能指正其中不对之处。
相对而言,CSS Modules中要覆盖组件中的样式会比较蛋疼。在设计组件的时候,千万要注意这个细节,并全需要留好口子。当然,或许还有其他的方式,只是我自己还没有领略到其中的奥秘之处。
CSS Modules中CSS和JavaScript共享变量
CSS Modules另一个优势是能够从CSS文件中导出变量用于JavaScript中。在CSS Modules中可以导出Sass、LESS中的变量或者任何CSS的属性。比如:
// var.scss
$bule: #45fdf3;
$base-font-size: 14px;
:export {
brandColor: $blue;
baseFontSize: $base-font-size;
}
在JavaScript文件中可以像下面这样获取到相应的变量值:
// App.js
import styles from './var.scss';
console.log(styles.brandColor); // -> #45fdf3
console.log(styles.baseFontSize); // -> 14px
其实我们可以借助CSS的自定义属性相关的特性,会让事情变得更为简单。感兴趣的同学不妨一试。
CSS Modules使用小技巧
花了一定的篇幅向大家演示了如何来写CSS Modules,现在简单的小结一些写CSS Modules应该注意的一些小细节。
- 尽量使用
class
来定义样式 - 在CSS Modules中
id
声明的样式会被忽略,正好应了那句,少在代码中使用id
- CSS Modules最大特色是作用于本地(局部域),只用单个类来定义样式最佳
- 尽量避免组合选择器的运用,在CSS Modules中也没必要这么使用,有利于提高选择器性能
- 可以借助
:global
和属性选择器的小技巧来声明全局样式 - 借助Webpack的能力,让全局样式和局部样式共存(文件结构需要组织好)
- 通过CSS Modules的
composes
特性来实现样式复用,可以将共用样式单独放置在一个文件中,通过composes
来复用 - 使用
class
命名时,尽量考虑BEM的模式(尽量避免中折-
或--
,应该尽量选择_
或__
连接) - 尽量借助PostCSS能力,辅助你快速编写CSS代码
- 尽量借助CSS的自定义属性来替代CSS处理器的变量,虽然CSS Modules中引用的变量可以CSS和JavaScript共用
- 在设计组件时,需要预留覆盖组件样式的入口,个人建议采用
data-xxx
这样的自定义属性来做为覆盖样式的入口选择器
上面这几点只是我自己使用CSS Modules的体验,仅是建议。既然是建议,大家可以根据自己的习惯来做相应的调整。另外在使用CSS Modules时尽量避免:
- 在同一个元素中声明多个类:其实没必要,CSS Modules的
composes
可以复用本地或其他组件中相同的类的样式,好比Sass的@extend
可以扩展@mixin
和%placeholder
样式 - 不在同一个样式文件中使用相同的类名:不管是CSS Modules还是CSS中,我们都应该尽量避免在同一个样式文件中使用相同的类名(CSS的选择器权重和顺序决定选用哪个样式)。哪果你在同一相样式中使用了相同的类名,在编译时并不会出错,编译出来的类名是相同的,只不过哪个使用,决取于其位置所在,越靠后越会被选用(假设权重相同)
- 尽量不要在样式中使用标签或属性选择器声明样式:在CSS Modules中,使用标签元素和属性选择器声明的样式是全局的
- 尽量避免使用
:global
标识声明样式:如果需要使用:global
标识符声明样式,应该将这该样式放置到公用样式文件中,以供全局使用,这样做利于代码维护 - 尽量避免
id
选择器声明样式:虽然CSS Modules会对样式中的id
选择器转换成相应的hash
值,但并不会运用到带有指定id
的元素上(标签元素id
属性的值不不会像class
一样被编译与样式表中编译后的值相匹配)
这些只是自己在React项目中初次使用CSS Modules的体感所记,如果有不对之处还请大神拍正。如果您有这方面更多的使用经验和更好的建议,欢迎在下面的评论中与我们一起分享。
改变Create React App体系下默认使用CSS Modules的模式
前面提到了,使用create-react-app
创建的React项目,默认就具备了CSS Modules的能力。不同的是在创建样式文件时对命名有一定的要求,需要以[name].module.css
(或[name].module.scss
、[name].module.sass
、[name].module.less
等),而且在.js
中引用样式文件需要像下面这样来引用:
import styles from './Button.module.css'
如果你去阅读create-react-app
构建体系中的Webpack相关配置(webpack.config.js
)时不难发现,默认只对cssModuleRegex
和sassModuleRegex
中开启了CSS Modules。
如果要查看Webpack相关的配置,需要先执行
npm run eject
命令,在config/
目录下可以找到对应的webpack.config.js
文件。请接着把分支切换到demo8
查看源码。
执行完npm run eject
命令之后,在config/
目录可以找到webpack.config.js
文件,就可以查看代码了(这个时候我又要感叹了,昵玛Webpack的配置真复杂,我需要一位Webpack高级配置专家帮我),截取其中的一部分代码:
// style files regexes
const cssRegex = /\.css$/;
const cssModuleRegex = /\.module\.css$/;
const sassRegex = /\.(scss|sass)$/;
const sassModuleRegex = /\.module\.(scss|sass)$/;
配置中对.css
、.module.css
、.scss|sass
以及.module.scss|sass
等样式文件做了一个正则匹配。关键代码是默认之下只在cssModuleRegex
和sassModuleRegex
开启了CSS Modules的能力:
既然如此,是不是依葫芦画瓢,也可以在cssRegex
和sassRegex
的getStyleLoaders()
中添加下面两行的配置呢:
modules: true,
getLocalIdent: getCSSModuleLocalIdent,
想不如动,直接添加保存:
重新一下工程,测试一下是否会对.css
中的选择器做hash
转换:
编译出来的结果如下:
是不是离我们的习惯又更进一步了。接着继续往下撸,让我们在CSS Modules环境下撸CSS更接近我们平时的手撸姿势。
React CSS Modules带来的优势
React CSS Modules是一个npm
包,其实现了自动化映射CSS Modules。每个CSS类都赋予一个带有全局唯一名字的本地标识符。CSS Modules实现了模块化和复用性。同时CSS Modules是一个可以被多种方法实现的规范。React CSS Modules利用Webpack的css-loader
所提从的功能启用了现有的CSS Modules的集成。
简单地说,CSS Modules 和 React CSS Modules都是通过Webpack的css-loader
来做编译的,不同的是React CSS Modules在现有的CSS Modules做了一些集成和优化。如果大家有动手写过CSS Modules的代码,不难发现现有的CSS Modules给我们编写CSS时带来几个不便之处:
- 类名的命名有较强的限制,比如说需要采用驼峰式命名,或采用下划线
_
(或__
)连接的命名 - 无论何时构建一个
className
都必须使用styles
对象 - 混合类的使用带来诸多的不便
- 全局类不够灵活
- 引用一个未定义的CSS模块时解析结果为
undefined
,但并无相关警告或报错提示
而React CSS Modules改进了上述的问题,并提供了更好的实现方式:
- 类的命名方式不再受限制(可以使用你平时较为喜欢的命名类方式)
- 不必每次使用一个CSS模块时还要引用
styles
对象 - 全局CSS和CSS Modules之间有一个明显的区别,比如
<div className="button" styleName="button-primary">
- 当
styleName
引用一个未定义的CSS模块时,会得到一个警告信息(需要开启handleNotFoundStyleName
选项的配置) - 可以为每个元素(
ReactElement
)只使用单独的CSS Module,也可以使用多个CSS Module(取决由配置项allowMultiple
决定)
接下来,还是基于Create React APP创建的项目环境下来向大家演示React CSS Modules的使用。
先把分支切换到
demo9
查看示例源码。
首先在项目根目录下执行:
npm i react-css-modules --save
那么你的组件可以会变成下面这种方式使用:
// App.js
import React from 'react';
import CSSModules from "react-css-modules";
import logo from './logo.svg';
import styles from './App.module.css';
console.log(styles);
function App() {
return (
<div className="App">
<header styleName="App-header">
<img src={logo} className="App-logo" alt="logo" />
<p>Edit <code>src/App.js</code> and save to reload.</p>
<a
className="App-link" styleName="underline"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default CSSModules(App, styles);
// 或者
// export default CSSModules(App, styles, options)
注意,在组件中需要调用react-css-modules
,像下面这样:
import CSSModules from "react-css-modules";
并且export
组件的时候像下面这样:
export default CSSModules(App, styles);
或者带有参数的方式导出组件:
export default CSSModules(App, styles, options)
在React CSS Modules的官方文档中有options
设置的详细描述。比如:
allowMultiple
选项设置为true
时,可以在styleName
中同时设置多个类名,比如styleName="a b"
,如果设置为false
只能在styleName
设置一个类名handleNotFoundStyleName
,选项设置调用未声明的模块报不报错等
使用了React CSS Modules之后,我们在JSX模板中时,可以同时使用className
和styleName
。其中className
是一个全局的样式,而styleName
是一个局部的样式。比如我们前面提到的Button
组件,.button
是公用样式,那么就可以使用className="button"
来引用,而button-primary
是组件样式,那就可以通过styleName="button-primary"
来引用:
// Button/Button.js
import React from 'react';
import CSSModules from 'react-css-modules';
import styles from './Button.module.css'
const Button = () => {
return (
<div className="button" styleName="button-primary" role="button">Primary Butotn</div>
)
}
export default CSSModules(Button, styles, {allowMultiple: true, handleNotFoundStyleName: 'throw'})
在介绍CSS Modules的时候,我们提到过,在CSS Modules中要覆盖样式是件麻烦的事情,那么在React CSS Modules中,事件会相对来说简单一点。我们可以使用styles
属性来覆盖组默认的styles
对象。比如我们想要在App.js
中覆盖组件Button
的样式,可以像下面这样来做:
// button-cover-css.module.css
.button-primary {
background: linear-gradient(to bottom, #09a, #f09);
color: #fff;
}
// App.js
import buttonCoverStyles from './button-cover-css.module.css;
<Button styles={ buttonCoverStyles }>
包装过的组件继承了styles
属性,该属性描述了CSS Mdodules和CSS类之间的映射关系。比如,styleName='foo'
和className={this.props.styles.foo}
是等价的。
使用React CSS Modules还有一个方便之处。在某些情况下,我们希望给组件同时导入多份CSS样式文件,毕竟在写组件时还是会用到一公用样式的嘛,这些公用样式又不想在每个组件的方重撸一回。那么我们就可以像下面这样使用:
// Panel.js
import React from "react";
import CSSModules from "react-css-modules";
import style from './panel.css'
import sharedStyle from './shared.scss'
const Panel = () => {
return (
<div styleName="panel panel-default">
<div styleName="panel-body">A Basic Panel</div>
</div>
)
}
export default CSSModules(Panel,{...style, ...shared},{});
// Shared.css
.panel{
background-color: #eee;
border-radius: 4px;
padding: 20px 30px;
}
// Panel.css
.panel-default{
composes: panel from './panel.css";
max-width: 640px;
margin-bottom: 24px;
}
上面只是演示了React CSS Modules中的一小部分特性,如果你对React CSS Modules更详细的内容感兴趣的话,可以花点时间阅读官方提供的文档。
如果你对React CSS Modules的文档不是很了解,也没关系,他并不会太影响你在React中怎么使用CSS Modules。只需要知道怎么安装,使用调用React CSS Modules即可,其使用具有CSS Modules所有特性(因为他是CSS Modules的集合),只是在CSS Modules上做了一些使用上的优化。
你可能发现了,上面的示例中
import
的styles
时,文件格式还是.module.css
。如果你还是喜欢.css
的方式,请按上一节的方式,在Create React App的工程体系下开启相关的配置。
使用 babel-plugin-react-css-modules实现CSS Modules
如果你打开过React CSS Modules的官网,可以看到作者在最前面就推荐大家使用babel-plugin-react-css-modules
来替代它。而两者主要差异是:
babel-plugin-react-css-module
是预先发生,可以免去运行时,处理styleName
对应问题,提升生产环境下的性能。
不过babel-plugin-react-css-modules
的配置真是件非常痛苦的事情。要比react-css-modules
复杂的多。跟着官方文档操作一波也未能如愿,网上搜索了大量的相关文章也并没有实际上解决配置问题(主要还是自己太弱,继续需要一位Webpack配置导师来指点我)。另外到目前为止,babel-plugin-react-css-modules
还有很多Issues(44个)未修复。是不是有点感觉在坑爹呀。
如果你不所被坑,那就请继续往下阅读,看看如何使用babel-plugin-react-css-module
实现CSS Modules。
请把分支切换到
demo10
查看源码。
目前我们还是基于Create React App的构建体系上来操作的。请先执行npm run eject
命令再继续后面的配置和操作。
在命令终端执行下面命令:
npm i babel-plugin-react-css-modules --save
此处有个细节,安装的时候,带的后缀是
--save
,而不是--save-dev
。
在webpack.config.js
中开启.css|.scss|.sass
的CSS Modules的能力。同时在你项目的根目录下创建.babelrc
文件,并在该文件中添加有关于babel-plugin-react-css-modules
相关的配置:
// .babelrc
{
"presets": [
"@babel/preset-env",
"@babel/preset-react"
],
"plugins": [
"@babel/plugin-transform-react-jsx-source",
[
"babel-plugin-react-css-modules",
{
"context": "./",
"generateScopedName": "[local]___[hash:base64:5]",
"autoResolveMultipleImports": true,
"webpackHotModuleReloading": true,
"exclude": "node_modules",
"handleMissingStyleName": "warn"
}
]
]
}
结应的webpack.config.js
也需要做相应的调整。因为babel-plugin-creat-css-modules
中的generateScopedName
规则要和css-loader
中的localIdentName
规则保持一致。
// webpack.config.js 部分代码
const getCSSModuleLocalIdent = "[local]___[hash:base64:5]"; // 需要和.babelrc中generateScopedName保持一样
oneOf: [
// ...
{
test: cssRegex,
exclude: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
// getLocalIdent: getCSSModuleLocalIdent,
localIdentName: getCSSModuleLocalIdent,
}),
sideEffects: true,
},
{
test: cssModuleRegex,
use: getStyleLoaders({
importLoaders: 1,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
// getLocalIdent: getCSSModuleLocalIdent,
localIdentName: getCSSModuleLocalIdent,
}),
},
{
test: sassRegex,
exclude: sassModuleRegex,
use: getStyleLoaders({
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
// getLocalIdent: getCSSModuleLocalIdent,
localIdentName: getCSSModuleLocalIdent,
},'sass-loader'),
sideEffects: true,
},
{
test: sassModuleRegex,
use: getStyleLoaders({
importLoaders: 2,
sourceMap: isEnvProduction && shouldUseSourceMap,
modules: true,
// getLocalIdent: getCSSModuleLocalIdent,
localIdentName: getCSSModuleLocalIdent,
},'sass-loader'),
},
],
如果package.json
中有关于babelrc
的相关配置的话,需要把它去除掉,不然会和新创建的.babelrc
相冲突。保存上面所有的修改,重新运行你的项目。如果你能看到React的Logo,表示你已经离成功更近一步了。
使用起来也非常的简单,和CSS Modules以及React CSS Modules只是细节上的差异,详细的操作可以阅读官方提供的示例,这里只用一个简单的示例演示一下babel-plugin-react-css-modules
下的CSS Modules的基本使用:
// App.js
import React from 'react';
import logo from './logo.svg';
import './App.css';
function App() {
return (
<div styleName="App">
<header styleName="App-header">
<img src={logo} styleName="App-logo" alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
styleName="App-link"
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
使用该插件操作CSS Modules时,在引入CSS文件时,不再需要将其赋值给一个对象,可以直接import
导入需要的样式文件:
// babel-plugin-react-css-modules
import './App.css';
// CSS Modules 或 React CSS Modules下
import style from './App.css'
在JSX中使用styleName
替代了className
。如果使用className
引入的为名,虽然样式中的代码会编译,但在JSX中className
不会做hash
编译,比如下面这个小例示:
// App.css
.active {
color: red;
padding-bottom: 5px;
border-bottom: 3px double currentColor
}
// App.js
<a styleName="App-link" className="active">Learn React</a>
编译出来的结果:
<!-- Style -->
.App-link___13fTZ {
color: #61dafb;
}
.active___1y60p {
color: red;
padding-bottom: 5px;
border-bottom: 3px double currentColor
}
<!-- HTML -->
<a href="https://reactjs.org" target="_blank" rel="noopener noreferrer" class="active App-link___13fTZ">Learn React</a>
如果在组件中同时引入两个不同的CSS文件,并且也有相同的类名时,想分清楚使用的是哪个文件中的类名时,可以像下面这样操作:
// App.js
import appStyle from './App.css';
import fooStyle from './foo.css';
<div styleName="appStyle.active">active class in App.css</div>
<div styleName="fooStyle.active">active class in foo.css</div>
编译出来的结果:
<!-- HTML -->
<div class="active___1y60p">active class in App.css</div>
<div class="active___R5w4I">active class in foo.css</div>
<!-- Style -->
// foo.css
.active___R5w4I {
color: red;
}
// App.css
.active___1y60p {
color: green;
padding-bottom: 5px;
border-bottom: 3px double currentColor;
}
为了更好的区分类名来自哪个文件,我们在做
hash
配置的时候,可以把[name]
参数加上,比如"[name]__[local]___[hash:base64:5]"
。
更多的示例不再演示了,感兴趣的同学可以点击官网查阅。另外推荐两篇有关于babel-plugin-react-css-modules
相关的两篇文章:
小结
该文主要踩了一遍React下面CSS Modules的使用,虽然大环境下是基于Create React App构建体系展开的,但有关于CSS Modules在业务中如何使用,以及需要的注意的细节都有阐述过,另外为了更好的改善大家在React项目中编写、维护和扩展CSS的能力,还介绍了react-css-modules
和babel-plugin-react-css-module
怎么来增强React环境下CSS Modules的能力。
那么自这篇文章之后,有关于CSS Modules在不同环境下的运用就基本上全了(PostCSS下CSS Modules、Vue环境下的CSS Modules),接下来,再来踩一回纯Webpack下的CSS Modules如何配置,以及有关于其他增加CSS能力和提高开发效率的工具,在Webpack体系下如何构建,感兴趣的同学可以关注后续的相关更新。