模块化 CSS
模块化 CSS
在样式开发过程中,存在以下问题:
全局污染
CSS 使用全局选择器机制来设置样式,优点是方便重写样式。缺点是所有的样式都是全局生效,样式可能被错误覆盖,为了提高样式权重会应用
!important
、行内样式
或者 复杂的选择器权重进行处理。Web Components 标准中的Shadow DOM
能彻底解决这个问题,但它的做法有点极端,样式彻底局部化,造成外部无法重写样式,损失了灵活性。命名混乱
多人协同开发时为了避免样式冲突,选择器越来越复杂,容易形成不同的命名风格,很难统一。样式变多后,命名将更加混乱。
依赖管理不彻底
组件应该相互独立,引入一个组件时,应该只引入它所需要的 CSS 样式。Saas/Less 很难实现对每个组件都编译出单独的 CSS,引入所有模块的 CSS 又造成浪费。使用 JS 的模块化来管理 CSS 依赖是很好的解决办法,Webpack 的
css-loader
提供了这种能力。无法共享变量
复杂组件要使用 JS 和 CSS 来共同处理样式,就会造成有些变量在 JS 和 CSS 中冗余,Sass/PostCSS/CSS 等都不提供跨 JS 和 CSS 共享变量这种能力。
代码压缩不彻底
为了解决如上问题, CSS 模块化应运而生。对于 React 使用 CSS 模块化主要有两种方式:
CSS Modules
:依赖于 webpack 构建和css-loader
等 loader 处理,将 css 交给 js 来动态加载。对每个类名(非:global
声明的)按照一定规则进行转换,保证它的唯一性。CSS IN JS
:用 JavaScript 对象方式编写 CSS 。
CSS Modules
CSS Modules ,使得项目中可以像加载 js 模块一样加载 css ,本质上通过一定自定义的命名规则生成唯一性的 css 类名,从根本上解决 css 全局污染,样式覆盖的问题。
// webpack.config.js
// webpack 使用 css-loader 启用 CSS 模块
module.exports = {
module: {
rules: [
{
test: /\.css$/i, // 匹配 .css 资源
use: ['css-loader?modules'],
},
],
},
}
/* index.module.css */
.text_color {
color: blue;
}
// index.jsx
import styles from './index.module.css'
function CSSModules() {
return <div className={styles.text_color}> CSS Modules</div>
}
<div class="src-components-CSSModules-index-module__text_color--bdxf5">
CSS Modules
</div>
自定义命名规则
// webpack 使用 css-loader 处理 CSS Modules 的基础配置
module.exports = {
module: {
rules: [
{
test: /\.css$/i, // 匹配 .css 资源
use: [
{
loader: 'css-loader',
options: {
modules: {
// 自定义类名命名规则
// > 开发环境使用 [path][name]__[local]
// > 生产环境使用 [hash:base64]
localIdentName: '[path][name]__[local]--[hash:base64:5]',
// 其他配置项如下:
// mode: 'local', // 控制应用于输入样式的编译级别
// auto: true, // 当 modules 配置项为对象时允许基于文件名自动启用 CSS 模块或者 ICSS
// exportGlobals: true, // 允许 css-loader 从全局类或 ID 导出名称
// localIdentName: '[path][name]__[local]--[hash:base64:5]', // 允许配置生成的本地标识符(ident)
// localIdentContext: path.resolve(__dirname, 'src'), // 允许为本地标识符名称重新定义基本的 loader 上下文
// localIdentHashSalt: 'my-custom-hash', // 允许添加自定义哈希值以生成更多唯一类
// namedExport: true, // 本地环境启用 / 禁用 export 的 ES 模块
// exportLocalsConvention: 'camelCase', // 导出的类名称的样式
// exportOnlyLocals: false, // 仅导出局部环境
},
},
},
],
},
],
},
}
局部作用域
CSS 的规则都是全局的,任何一个组件的样式规则,都对整个页面有效。
CSS Modules 的做法是产生局部作用域的唯一方法,就是使用一个独一无二的 class 的名字,不会与其他选择器重名。
/* index.module.css */
.text_color {
color: blue;
}
/* ===== 等价于 ===== */
:local(.color) {
color: blue;
}
// index.jsx
import styles from './index.module.css'
function CSSModules() {
return <div className={styles.text_color}> CSS Modules</div>
}
<div class="src-components-CSSModules-index-module__text_color--bdxf5">
CSS Modules
</div>
全局作用域
CSS Modules 允许使用 :global(.className)
的语法,声明一个全局规则。凡是这样声明的 class,都不会被编译成哈希字符串。
/* index.module.css */
:global(.text_color) {
color: #008000;
}
.text_color {
color: blue;
}
.text_bg {
background-color: red;
}
// index.jsx
import styles from './index.module.css'
function CSSModules() {
return (
<div>
{/* 全局作用域样式,直接使用样式名 */}
<div className="text_color"> CSS Modules - :global(.className) </div>
<div className={'text_color ' + styles.text_color}> CSS Modules</div>
</div>
)
}
<div>
<div
class="text_color src-components-CSSModules-index-module__text_color--bdxf5"
>
CSS Modules
</div>
<div class="text_color">CSS Modules - :global(.className)</div>
</div>
组合样式
在 CSS Modules 中,一个选择器可以继承另一个选择器的规则,称为 组合(composition)
。
/* index.module.css */
.text_bg {
background-color: red;
}
.plain_text {
/* 继承其他模块中对应样式的规则 */
composes: other_text_color from './other.modules.css';
/* 继承本模块中 .text_bg 样式的规则 */
composes: text_bg;
}
/* other.module.css */
.other_text_color {
color: yellow;
}
// index.jsx
import styles from './index.module.css'
function CSSModules() {
return <div className={styles.plain_text}> CSS Modules - composes </div>
}
使用变量
/* color.module.css */
@value blue: #0c77f8;
@value red: #ff0000;
@value green: #aaf200;
/* index.module.css */
@value colors: "./colors.css";
@value blue, red, green from colors;
.plain_text {
color: red;
background-color: blue;
}
// index.jsx
import styles from './index.module.css'
function CSSModules() {
return <div className={styles.plain_text}> CSS Modules - 使用变量 </div>
}
配置 less 和 sass
// webpack 使用 css-loader 处理 CSS Modules 的基础配置
module.exports = {
module: {
rules: [
{
test: /\.css$/i, // 匹配 .css 资源
use: [
{
loader: 'css-loader',
options: {
modules: {
// 自定义类名命名规则
// > 开发环境使用 [path][name]__[local]
// > 生产环境使用 [hash:base64]
localIdentName: '[path][name]__[local]--[hash:base64:5]',
// 其他配置项如下:
// mode: 'local', // 控制应用于输入样式的编译级别
// auto: true, // 当 modules 配置项为对象时允许基于文件名自动启用 CSS 模块或者 ICSS
// exportGlobals: true, // 允许 css-loader 从全局类或 ID 导出名称
// localIdentName: '[path][name]__[local]--[hash:base64:5]', // 允许配置生成的本地标识符(ident)
// localIdentContext: path.resolve(__dirname, 'src'), // 允许为本地标识符名称重新定义基本的 loader 上下文
// localIdentHashSalt: 'my-custom-hash', // 允许添加自定义哈希值以生成更多唯一类
// namedExport: true, // 本地环境启用 / 禁用 export 的 ES 模块
// exportLocalsConvention: 'camelCase', // 导出的类名称的样式
// exportOnlyLocals: false, // 仅导出局部环境
},
},
},
{
// ... 使用其他 loader
},
'less-loader',
],
},
],
},
}
组合方案
React 项目可能在使用 CSS 处理样式之外,还可以使用 scss 或者 less 进行预处理。
- 可以约定对于全局样式或者是公共组件样式,可以使用
.css
文件 ,不需要做 CSS Modules 处理,这样就不需要写:global
等繁琐语法。 - 对于项目中开发的页面和业务组件,统一用 scss 或者 less 等做 CSS Module,即 CSS 全局样式 + less / scss CSS Modules 方案。
动态添加 class
CSS Modules 可以配合 classNames 库
实现更灵活的动态添加类名。
/* index.module.css */
.plain_text {
font-size: 16px;
}
.dark {
color: #fff;
background: #000;
}
.light {
color: #000;
background: #fff;
}
import classnames from 'classnames'
import styles from './index.module.css'
function CSSModules() {
const [theme, setTheme] = React.useState('light')
const changeTheme = () => {
setTheme(theme === 'light' ? 'dark' : 'light')
}
return (
<div>
<div
className={classnames(
styles.plain_text,
theme === 'light' ? styles.light : styles.dark
)}
>
CSS Modules - classnames 动态添加 class
</div>
<button onClick={changeTheme}>切换主题</button>
</div>
)
}
使用技巧
CSS Modules 是对现有的 CSS 做减法。为了追求简单可控,作者建议遵循如下原则:
- 不使用选择器,只使用
class
名来定义样式 - 不层叠多个
class
,只使用一个class
把所有样式定义好 - 所有样式通过
composes
组合来实现复用 - 不嵌套
CSS-in-JS
CSS-in-JS 的实现原理,以 styled-components 为例:
- 通过 styled-components,可以使用 ES6 的标签模板字符串语法(Tagged Templates)为需要 styled 的 Component 定义一系列 CSS 属性。
- 当该组件的 JS 代码被解析执行的时候,styled-components 会动态生成一个 CSS 选择器,并把对应的 CSS 样式通过 style 标签的形式插入到 head 标签里面。
- 动态生成的 CSS 选择器会有一小段哈希值来保证全局唯一性来避免样式发生冲突。
- 这种模式下,本质上是动态生成 style 标签。
CSS-in-JS 特点:
- CSS-in-JS 本质上放弃了 css ,变成了 css in line 形式,所以根本上解决了全局污染,样式混乱等问题。
- 运用起来灵活,可以运用 js 特性,更灵活地实现样式继承,动态添加样式等场景。
- 由于编译器对 js 模块化支持度更高,使得可以在项目中更快地找到 style.js 样式文件,以及快捷引入文件中的样式常量。
- 无须 webpack 额外配置 css,less 等文件类型。
styled-components
基础用法
import styled from 'styled-components'
const Button = styled.button`
background: #6a8bad;
color: #fff;
min-width: 96px;
height: 36px;
border: none;
border-radius: 18px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin-left: 20px !important;
`
export default function CSSINJS() {
return (
<div>
<Button>CSS-in-JS</Button>
</div>
)
}
基于 props 动态添加样式
import styled from 'styled-components'
const Button = styled.button`
background: ${props => (props.theme ? props.theme : '#6a8bad')};
color: #fff;
min-width: 96px;
height: 36px;
border: none;
border-radius: 18px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin-left: 20px !important;
`
export default function CSSINJS() {
return (
<div>
<Button theme={'#fc4838'}>props主题按钮</Button>
</div>
)
}
继承样式
const Button = styled.button`
background: #6a8bad;
color: #fff;
min-width: 96px;
height: 36px;
border: none;
border-radius: 18px;
font-size: 14px;
font-weight: 500;
cursor: pointer;
margin-left: 20px !important;
`
const NewButton = styled(Button)`
background: #fc4838;
color: white;
`
export default function CSSINJS() {
return (
<div>
<NewButton> 继承按钮</NewButton>
</div>
)
}