CSS 命名及模块化方案
CSS 根据选择器名称去全局匹配元素,它没有作用域可言,因此存在命名冲突导致样式覆盖的问题,这种影响在组件开发中尤为明显。
理想的状态下,开发一个组件时,应该可以随意的为其中的元素进行命名,只需要保证其语义性即可,而不必担心它是否与组件外的样式发生冲突。
与 JavaScript 社区中的 CommonJS、AMD、CMD、ES Modules 模块规范类似,CSS 社区也诞生了相应的模块化解决方案:BEM、OOCSS、SMACSS、ITCSS,以及 CSS Modules 和 CSS-in-JS 等。
根据这些 CSS 模块化方案的特点,可以分为三大类:
- CSS 命名方法论:通过人工的方式来约定命名规则。
- CSS Modules:一个 CSS 文件就是一个独立的模块。
- CSS-in-JS:在 JS 中写 CSS。
一、CSS 命名方法论
为了避免 CSS 选择器命名冲突的问题,以及更好的实现 CSS 模块化,CSS 社区在早期诞生了一些 CSS 命名方法论,如 BEM、OOCSS、SMACSS、ITCSS、SUITCSS、Atomic CSS 等。
它们几乎都有一个共同的特点——为选择器增加冗长的前缀或后缀,并试图通过人工的方式来生成全局唯一的命名。这无疑会增加了类命名的复杂度和维护成本,也让 HTML 标签显得臃肿。
1、BEM
BEM(Block Element Modifier)是一种典型的 CSS 命名方法论,由 Yandex 团队于 09 年提出,它的核心思想是 通过组件名的唯一性来保证选择器的唯一性,从而保证样式不会污染到组件外。
BEM 命名规约是 .block-name__element-name--modifier-name
,即 .模块名__元素名--修饰器名
三个部分,用双下划线 __
来明确区分模块名和元素名,用双横线 --
来明确区分元素名和修饰器名。也可以在保留 BEM 核心思想的前提下,自定义命名风格,如驼峰法、使用单下划线、使用单横线等。
在 BEM 中不建议使用子代选择器,因为每一个类名已经都是全局唯一的了,除非是 block 相互嵌套的场景。
示例:
<div class="card">
<div class="card__head">
<ul class="card__menu">
<li class="card__menu-item">menu item 1</li>
<li class="card__menu-item">menu item 2</li>
<li class="card__menu-item card__menu-item--active">menu item 3</li>
<li class="card__menu-item card__menu-item--disable">menu item 4</li>
</ul>
</div>
<div class="card__body"></div>
<div class="card__foot"></div>
</div>
使用 Sass/Less/Stylus 的父元素选择器 &
可以更高效的编写 BEM:
.card {
&__head {}
&__menu {
&-item {
&--active {}
&--disable {}
}
}
&__body {}
&__foot {}
}
2、OOCSS
OOCSS(Object-Oriented CSS)即面向对象的 CSS,它借鉴了 OOP(面向对象编程)的抽象思维,主张将元素的样式抽象成多个独立的小型样式类,来提高样式的灵活性和可重用性。
OOCSS 有两个基本原则:
- 独立的结构和样式:即不要将定位、尺寸等布局样式与字体、颜色等表现样式写在一个选择器中。
- 独立的容器和内容:即让对象的行为可预测,避免对位置的依赖,子元素即使离开了容器也应该能正确显示。
使用 OOCSS 前:
<div class="box"></div>
<style>
.box {
width: 25%;
margin: 5px 10px 10px;
background: blue;
border: 1px solid #ccc;
}
</style>
使用 OOCSS 后:
<div class="size1of4 bgBlue solidGray mt-5 ml-10 mr-10 mb-10"></div>
<style>
.size1of4 { width: 25%; }
.bgBlue { background: blue; }
.solidGray { border: 1px solid #ccc; }
.mt-5 { margin-top: 5px; }
.mr-10 { margin-right: 10px }
.mb-10 { margin-bottom: 10px; }
.ml-10 { margin-left: 10px; }
</style>
OOCSS 最大的优点是让样式可复用性最大化,也能够显著减少整体的 CSS 代码数量。缺点也很明显,需要为每个元素搜集一大堆类名,这可是一个不小的体力活。
3、SMACSS
SMACSS(Scalable and Modular Architecture for CSS)即可伸缩及模块化的 CSS 结构,由 Jonathan Snook 在 2011 年雅虎时提出。
SAMCSS 按照部件的功能特性,将其划分为五大类:
- 基础(Base)是为HTML元素定义默认样式,可以包含属性、伪类等选择器。
- 布局(Layout)会将页面分为几部分,可作为高级容器包含一个或多个模块,例如左右分栏、栅格系统等。
- 模块(Module)又名对象或块,是可重用的模块化部分,例如导航栏、产品列表等。
- 状态(State)描述的是任一模块或布局在特定状态下的外观,例如隐藏、激活等。
- 主题(Theme)也就是换肤,描述了页面的外观,它可修改前面四个类别的样式,例如链接颜色、布局方式等。
SMACSS 推荐使用前缀来区分不同部件:
- 基础规则是直接作用于元素的,因此不需要前缀。
- 布局的前缀是
l-
或layout-
,例如.l-table
、.layout-grid
等。 - 模块的前缀是
m-
或模块自身的命名,例如.m-nav
、.card
、.field
等。 - 状态的前缀是
is-
,例如.is-active
、.is-current
等。 - 主题的前缀是
theme-
,例如.theme-light
、.theme-dark
等。
<form class="layout-grid">
<div class="field">
<input type="search" id="searchbox" />
<span class="msg is-error">There is an error!</span>
</div>
</form>
4、ITCSS
ITCSS(Inverted Triangle CSS,倒三角 CSS)是一套方便扩展和管理的 CSS 体系架构,它兼容 BEM、OOCSS、SMACSS 等 CSS 命名方法论。
ITCSS 使用 分层 的思想来管理你的样式文件,类似服务端开发中的 MVC 分层设计。
ITCSS 将 CSS 的样式规则划分成以下的几个层次:
- Settings:项目使用的全局变量,比如颜色,字体大小等等。
- Tools:项目使用的 mixins 和 functions。到 Tools 为止,不会生成具体的 CSS 代码。
- Generic:最基本的设定,比如 reset.css、normalize.css 等。
- Base:最基础的元素(elements),比如 img、p、link、list 等。
- Objects:某种设计模式,比如水平居中,
- Components:UI 组件,比如 button、switch、slider 等。
- Trumps:用于辅助和微调的样式,只有这一层才可以使用
!important
。
ITCSS 的分层逻辑越往下就越具体,越局限在某个具体的场景。
根据 ITCSS 的思想,可以这样组织 CSS 样式文件:
stylesheets/
├── settings/
│ ├── colors.scss
│ ├── z-layers.scss
│ └── breakpoints.scss
├── tools/
│ ├── mixins.scss
│ └── functions.scss
├── generic/
│ ├── box-sizing.scss
│ └── normalize.scss
├── base/
│ ├── img.scss
│ └── list.scss
├── objects/
│ ├── grid.scss
│ └── media.scss
├── components/
│ ├── buttons.scss
│ └── slider.scss
├── trumps/
│ ├── widths.scss
│ └── gaps.scss
└── index.scss
二、CSS Modules
CSS 命名方法论虽然已不适用于当今的自动化工作流和大前端环境,但其背后的设计思想同样值得学习。
手写命名前缀后缀的方式非常麻烦,于是 CSS Modules 这种真正的模块化工具就诞生了。
CSS Modules 允许我们像 import 一个 JS Module 一样去 import 一个 CSS Module。每一个 CSS 文件都是一个独立的模块,每一个类名都是该模块所导出对象的一个属性。
通过这种方式,可在使用时明确指定所引用的 CSS 样式。并且 CSS Modules 在打包时会自动将 id 和 class 混淆成全局唯一的 hash 值,从而避免发生命名冲突问题。
CSS Modules 特性:
- 作用域:模块中的名称默认都属于本地作用域,定义在
:local
中的名称也属于本地作用域,定义在:global
中的名称属于全局作用域,全局名称不会被编译成哈希字符串。 - 命名:对于本地类名称,CSS Modules 建议使用 camelCase 方式来命名,这样会使 JS 文件更干净,即
styles.className
。虽然不提倡但仍可以使用styles['class-name']
。 - 组合:使用
composes
属性来继承另一个选择器的样式,这与 Sass 的@extend
规则类似。 - 变量:使用
@value
来定义变量,不过需要安装 PostCSS 和 postcss-modules-values 插件。
示例:
:global(.card) {
padding: 20px;
}
.article {
background-color: #fff;
}
.title {
font-size: 18px;
}
import React from 'react'
import styles from './style.css'
export default function App() {
return (
<article className={styles.article}>
<h2 className={styles.title}>Hello World</h2>
<div className="card">Lorem ipsum dolor sit amet.</div>
</article>
)
}
编译结果:
<style>
.card {
padding: 20px;
}
.style__article--ht21N {
background-color: #fff;
}
.style__title--3JCJR {
font-size: 18px;
}
</style>
<article class="style__article--ht21N">
<h2 class="style__title--3JCJR">Hello World</h2>
<div class="card">Lorem ipsum dolor sit amet.</div>
</article>
1、CSS Modules 集成
在 webpack 中可以使用 CSS Modules(开启 css-loader 的 modules 特性):
{
test: /\.(c|sa|sc)ss$/i,
exclude: /node_modules/,
use: [
'style-loader',
{
loader: 'css-loader',
options: {
importLoaders: 2,
// 开启 CSS Modules
modules: true,
// 借助 CSS Modules,可以很方便地自动生成 BEM 风格的命名
localIdentName: '[path][name]__[local]--[hash:base64:5]',
},
},
'postcss-loader',
'sass-loader',
],
},
在 PostCSS 中使用 CSS Modules(使用 postcss-modules 插件):
module.exports = {
plugins: {
'postcss-modules': {
generateScopedName: '[path][name]__[local]--[hash:base64:5]',
},
},
}
2、配合 CSS 预处理器使用
使用 CSS Modules 时,可以配合 CSS 预处理器(Sass/Less/Stylus)一起使用。
CSS 预处理器提供了许多有用的功能,如嵌套、变量、mixins、functions 等,同时也让定义本地名称或全局名称变得容易。
:global(.title) {
color: yellow;
}
:global {
.global-class-name {
color: green;
}
}
3、VSCode 扩展支持
在 VSCode 中写 CSS Modules 代码,默认是没有自动提示和跳转至定义处的功能,不够智能。
可以安装 CSS Modules 扩展来实现。
三、CSS-in-JS 库
React 是在 JS 中实现了对 HTML 和 CSS 的封装,赋予了 HTML 和 CSS 全新的“编程能力”。对于 HTML,衍生了 JSX 这种 JS 的语法扩展,可以将其理解为 HTML-in-JS;对于 CSS,衍生出一系列的第三方库,用来加强在 JS 中操作 CSS 的能力,它们被称为 CSS-in-JS。
随着 React 的流行以及组件化开发模式的深入人心,这种"关注点混合"的新写法逐渐成为主流。
CSS-in-JS 库目前已有几十种实现,可以在 CSS in JS Playground 上快速尝试不同的实现。常见的 CSS-in-JS 库如下:
- styled-components 35k
- emotion 14k
- Styled System 7k
- styled-jsx 6k
- JSS 6k
其中,styled-components 是目前最流行的 CSS-in-JS 库,在 React 中被广泛使用,不仅可以实现 CSS 的模块化,而且会自动添加浏览器兼容性前缀。
1、基本用法
使用 ES6 提供的模版字符串功能来构造“样式组件”。
import React from 'react'
import styled from 'styled-components'
// 创建一个名为 UIWrapper 的样式组件
const UIWrapper = styled.section`
padding: 10px;
`
// 创建一个名为 UITitle 的样式组件
const UITitle = styled.h1`
font-size: 20px;
`
// 然后,像使用其他 React 组件一样使用这些样式组件
export default function App() {
return (
<UIWrapper>
<UITitle>Hello World</UITitle>
</UIWrapper>
)
}
2、多个组件的引用
当样式组件较多时,可以将样式组件单独存放在一个文件中,通过 *
一次性引入使用。
import styled from 'styled-components'
export const Wrapper = styled.section`
padding: 10px;
`
export const Title = styled.h1`
font-size: 20px;
`
import React from 'react'
import * as UI from './styles'
// 然后,像使用其他 React 组件一样使用这些样式组件
export default function App() {
return (
<UI.Wrapper>
<UI.Title>Hello World</UI.Title>
</UI.Wrapper>
)
}
3、props 传参动态生成样式
可以在样式组件中通过 props
引入属性值,实现样式的动态生成。
// 创建样式组件
export const Title = styled.h1`
color: ${props => props.color || "#000"};
font-size: ${props => props.size === big ? "18px" : "16px"};
`
// 使用样式组件
<UI.Title color="#333" size="big">Hello World</UI.Title>
4、styled() 实现样式继承
可以通过构造函数 styled()
来继承另一个组件的样式。
export const Title = styled.h1`
color: #333;
`
export const Txt = styled(Title)`
font-size: 16px;
`
5、createGlobalStyle 全局样式
使用 createGlobalStyle
来创建全局 CSS 规则,然后作为样式组件引入使用。
// 创建样式组件
import { createGlobalStyle } from "styled-components"
const GlobalStyle = createGlobalStyle`
body {
color: red;
}
`
// 使用样式组件
<>
<Title />
<GlobalStyle />
</>
6、配置类名、SSR 支持等
可以使用 styled-components 的 Babel 插件 babel-plugin-styled-components,它提供了更好的调试体验的支持,比如更清晰的类名、SSR 支持、压缩代码等等。
7、VSCode 扩展语法高亮
默认情况下,模版字符串中的 CSS 代码在 VSCode 中是没有智能提示和语法高亮效果的,可以安装 vscode-styled-components 扩展。
四、Vue 中 CSS 模块化的实现
styled-components 使得 React 的 CSS 模块化得到很好的实现,如果要在 Vue 中来实现 CSS 模块化,可以使用 Scoped 或 CSS Modules。
1、使用 Scoped CSS(推荐)
为 <style>
区块添加 scoped
属性即可开启“组件样式作用域(Scoped CSS)
开启后,Vue 会为该组件内所有的元素都加上一个全局唯一的属性选择器,形如 [data-v-5298c6bf]
,这样在组件内的 CSS 就只会作用于当前组件中的元素。
<template>
<header class="header">header</header>
</template>
<style scoped>
.header {
background-color: #333;
}
</style>
编译结果:
<header class="header" data-v-5298c6bf>header</header>
<style>
.header[data-v-5298c6bf] {
background-color: green;
}
</style>
2、使用 CSS Modules
为 <style>
区块添加 module
属性即可开启 CSS Modules。
开启后,Vue 会为组件注入一个名为 $style
的计算属性,并混淆类名,然后就可以在模板中通过一个动态类绑定来使用它了。
<template>
<header :class="$style.header">header</header>
</template>
<style module>
.header {
background-color: green;
}
</style>
编译结果:
<header class="App__header--382G7">header</header>
<style>
.App__header--382G7 {
background-color: green;
}
</style>