css 如何实现样式隔离?
由于传统 CSS 没有作用域的概念,不同组件中的 CSS 样式会全局生效,因此开发过程中有极大可能会遇到样式冲突的问题。尤其是多人协作的项目,如果没有统一的 CSS 样式命名规范,很容易出现同一类名导致的样式覆盖情况。因此开发过程中需要对不同组件的样式进行隔离,以避免协作开发时产生样式冲突。为保证样式互不影响,有以下一些方案可供选择:
BEM
BEM 是 Yandex 公司提出的一套CSS 样式命名规范,BEM 分别指代 Block(块)、Element(元素)、Modifier(修饰符),BEM 规定 CSS 样式名称由这三部分内容组成。
- Block,有意义的独立模块。
- Element,Element 是 Block 的子元素,语义上与 Block 相关联,但本身没有独立意义,这里无独立意义指的是 Element 不会脱离 Block 单独使用,即非通用样式。
- Modifier,修饰器,通常用于表示 Block 或 Element 的外观和行为变化。
BEM 三部分内容有以下几种组合方式:
.Block {}
.Block__Element {}
.Block--Modifier {}
.Block__Element--Modifier {}
Block 和 Element 之间用双下划线__相连接,Block 或 Element 与 Modifier 之间用双中划线--连接。下面的例子展示了 nav 导航栏如何使用 BEM 规范进行命名:
<nav class="nav">
<ul class="nav__list">
<li class="nav__item">
<a href="#" class="nav__link">Home</a>
</li>
<li class="nav__item">
<a href="#" class="nav__link nav__link--active">Careers</a>
</li>
<li class="nav__item">
<a href="#" class="nav__link">About us</a>
</li>
</ul>
</nav>
BEM 使用的一个误区是多个层级嵌套时,在类名中展示出全部的层级结构,如下方代码所示,这样做会使得类名过长。
<nav class="nav">
<ul class="nav__list">
<li class="nav__list__item">
<a href="#" class="nav__list__item__link">Home</a>
</li>
<li class="nav__list__item">
<a href="#" class="nav__list__item__link nav__list__item__link--active">Careers</a>
</li>
<li class="nav__list__item">
<a href="#" class="nav__list__item__link">About us</a>
</li>
</ul>
</nav>
BEM 并不需要考虑结构的深度,Block 子元素的 BEM 类名应当仅包含 Block 名称和本身 Element 的名称。
优势和缺陷
BEM 对类名进行人为约束以避免样式污染问题,而且使用 BEM 命名规范,在一定程度上能够使得代码看起来更结构化,可读性更高。当然也有开发者认为 BEM 类名写起来繁杂冗长,不够优雅。
Vue style scoped
BEM 命名规范是一种 CSS 隔离的解决方案,但寄希望于人为命名约束并不可靠,vue 框架本身内置 scoped 属性实现组件间样式隔离。以下是一个 vue 项目的实例,当组件 style 标签加上 scoped 属性后表示样式仅在当前组件生效。
<template>
<div>
<ul class="list">
<li class="list-item">home</li>
</ul>
</div>
</template>
<style scoped>
.list-item {
color: red;
}
</style>
编译后结果:
<template>
<div data-v-31a768e4>
<ul class="list" data-v-31a768e4>
<li class="list-item" data-v-31a768e4>home</li>
</ul>
</div>
</template>
<style scoped>
.list-item[data-v-31a768e4] {
color: red;
}
</style>
scoped 其原理是编译后会给当前组件内所有标签元素添加一个唯一标识,同时样式类名也通过[]
添加上该标识符,实现了组件间样式的私有化,当前组件的样式也就不会对其他组件产生影响。
但是在使用第三方组件库时常会遇到需要覆盖默认样式的情况,由于组件 scoped 属性的私有化作用,修改样式后并不会生效,此时可以用 vue 官方提供的深度选择器解决,vue3.x 写法如下:
<template>
<div>
<el-input v-model="name" class="input" placeholder="请输入姓名"/>
</div>
</template>
<style scoped>
:deep(.input) .el-input__inner {
padding: 0 12px;
}
</style>
编译结果:
<template>
<div class="el-input el-input--mini input" data-v-31a768e4>
<input class="el-input__inner" type="text" autocomplete="off" placeholder="请输入姓名">
</div>
</template>
<style scoped>
[data-v-31a768e4] .input .el-input__inner {
padding: 0 12px;
}
</style>
从编译结果能够看出其原理是把父组件的标识放到了样式选择器头部进行样式穿透覆盖。由于 React 框架没有给出官方的 CSS 样式隔离方案,于是涌现出一批例如 CSS Module,CSS-IN-JS 的解决方案。
CSS Module
CSS Module 将 CSS 作为独立文件导入,在组件内部使用时将 CSS 文件类名当做导出对象的属性进行引用,通过构建工具动态生成唯一的类名替换源文件引用的类名,相当于实现了 CSS 局部作用域。
基础用法
下面的代码是通过 create-react-app 脚手架创建的一个 react 组件,create-react-app 脚手架已经内置了 CSS Module 用法,在使用时只需将样式文件名扩展为[className].module.css
用以标识使用 CSS Module。
import logo from './logo.svg';
import styles from './App.module.css';
function App() {
return (
<div className={styles.App}>
<header className={styles['App-header']}>
<img src={logo} className={styles['App-logo']} alt="logo" />
<p>
Edit <code>src/App.js</code> and save to reload.
</p>
<a
className={styles['App-link']}
href="https://reactjs.org"
target="_blank"
rel="noopener noreferrer"
>
Learn React
</a>
</header>
</div>
);
}
export default App;
App.module.css
.App {
text-align: center;
}
.App-logo {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App-logo {
animation: App-logo-spin infinite 20s linear;
}
}
.App-header {
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-link {
color: #61dafb;
}
@keyframes App-logo-spin {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
样式表文件中的类名被编译成一串独一无二的 hash 值,同时从控制台进行元素审查能够发现 HTML 元素的类名也变成了相应的 hash 值,因此不同组件中使用相同的类名也就不会产生冲突。
.App_App__xiv9k {
text-align: center;
}
.App_App-logo__BvZpA {
height: 40vmin;
pointer-events: none;
}
@media (prefers-reduced-motion: no-preference) {
.App_App-logo__BvZpA {
animation: App_App-logo-spin__v012M infinite 20s linear;
}
}
.App_App-header__NHbRY {
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_App-link__z56I2 {
color: #61dafb;
}
@keyframes App_App-logo-spin__v012M {
from {
transform: rotate(0deg);
}
to {
transform: rotate(360deg);
}
}
保持原始类名
在开发过程中,可能会遇到这样的场景,引入第三方库后需要对引入库的样式进行调整,比如我们需要调整库中类名为 nav 的样式,此时在样式文件 App.module.css 中直接重新声明类 nav 的样式不会生效,因为 nav 类名已经被编译成了一个 hash 字符串。CSS Module 中允许使用 :global(.nav)
语法将 nav 类作用范围调整为全局,使用该种语法定义的类不会被编译成 hash 值,从而实现保持原始类名。
与全局作用域相对应,CSS Module 中允许使用 :local(.className)
语法显示定义局部作用域样式,此时等同于直接声明 .className 样式。
样式复用
CSS Module 中可以使用composes
关键字继承同一样式文件内其他选择器的样式规则。例如 App.module.css 文件中,让 .App-link 继承 .bgc 的规则,
.bgc {
background-color: #fff;
}
.App-link {
composes: bgc;
color: #61dafb;
}
编译后,
.App_bgc__JWFRU {
background-color: #fff;
}
.App_App-link__z56I2 {
color: #61dafb;
}
对应的 HTML 元素编译结果:
<a class="App_App-link__z56I2 App_bgc__JWFRU" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a>
另外,CSS Module 支持使用composes
关键字继承来自于其他样式文件内的选择器规则,例如让 .App-link 继承 style.module.css 内部的 .bgc 规则:
style.module.css
.bgc {
background-color: #fff;
}
App.module.css
.App-link {
composes: bgc from './style.css';
color: #61dafb;
}
对应的 HTML 元素被编译为:
<a class="App_App-link__z56I2 style_bgc__3I-M0" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">Learn React</a>
优势和缺陷
从以上关于 CSS Module 的介绍中不难看出,CSS Module 语法规则简单易用,其提供的模块化能力能够有效避免全局污染和样式冲突,而且便于样式的复用。但 CSS Module 不是完美的,样式逻辑判断以及动态修改较为麻烦。
CSS-in-JS
CSS-in-JS 是一种样式化技术方案,与以往提倡的『关注点分离』的思路不同,CSS-in-JS 直接将样式写到 JS 文件而不是单独的样式文件里。如此一来,就可以在 CSS 中使用一些属于 JS 的语言特性(例如模块声明,变量定义,函数调用和条件判断等)以提供灵活且可扩展的样式定义。
实现 CSS-in-JS 的第三方库有许多,根据样式处理方式大致可分为唯一 CSS 选择器和内联样式两大类。
唯一 CSS 选择器是指 CSS 选择器名称会被编译为唯一的随机字符串,采用该种做法的库有 styled-components,jss,emotion,glamorous 等。
内联样式是指将 CSS 动态转换为标签内联样式,代表库有 radium。
下图是一些常用的 CSS-in-JS 库近一年内的 npm 包下载趋势图,当前最为流行的 CSS-in-JS 库当属 styled-components。
styled-components
styled-components 处理样式实际上是通过 ES6 的标签模板语法创建了一个样式组件,后续能够在 JSX 中直接使用该组件设置样式。一个简单的示例如下所示:
import styled from 'styled-components';
const Title = styled.h1`
color: #666;
text-align: center;
text-decoration: underline;
`;
function App() {
return (
<Title>
这是一个标题
</Title>
);
}
export default App;
编译后结果如下图所示:
当创建样式组件的 JS 代码被执行时会动态生成一个名称唯一的 CSS 选择器,并将选择器规则添加到 head 标签的 style 子标签内。CSS 选择器名称是动态生成的 hash 字符串,用以避免样式冲突。
关于 styled-components 库其他语法诸如组件样式扩展,嵌套,伪元素/伪选择器支持等本文不多做介绍,感兴趣的小伙伴可以参考官方文档,中文文档进行学习和实践。
优势和缺陷
CSS-in-JS 有以下一些优势:
- CSS-in-JS 提供了在样式中访问 JavaScript 变量的能力,使用 inline styles 也能实现高度自定义的样式还原,但会增加大量冗余代码,使用 CSS 变量则不如 CSS-in-JS 直观。
- 直接在组件内部书写样式,方便模块化管理。
同时,CSS-in-JS 也存在一些争议和不足:
- CSS-in-JS 会增加运行时的开销。组件进行渲染的时候,CSS-in-JS 库会在运行时将样式代码『序列化』为可插入文档的 CSS,此举会消耗浏览器更多的 CPU 性能。
- CSS-in-JS 会增加 Bundle 的大小。每个访问网站的用户都必须下载对应 CSS-in-JS 库。
- CSS-in-JS 会使 React DevTools 结构变得复杂。如 Emotion 会包裹额外的 React 组件层实现样式插入,进而影响 React DevTools 组件结构的展示。
总结
本文介绍的 CSS 隔离方案大体都是通过限制 CSS 选择器名称的唯一性进而实现样式隔离,这些避免样式冲突的方案各有优劣,在开发过程中需要结合实际,充分考虑框架选型、具体业务场景、项目规模以及团队习惯等因素后选择合适的 CSS 隔离方案。
转载自:https://juejin.cn/post/7207239167083135032