CSS如何做样式隔离
CSS 的样式隔离回归本质地讲,就是对 CSS 的命名下手,通过各种约定或是形式生成独立的命名从而使其在全局作用域下保持其独立性。
# 1. BEM 命名规范
BEM 命名规范通过一种命名约定来保证其样式的独立, 即模块名 + 元素名 + 修饰器名。
- B: 即 Block ,以元素本身所具备的功能为主。
- E: 即 Element, 以元素本身的位置与形状、描述为主。
- M: 即 Modifier,以当前元素的颜色,状态为主。
BEM 的使用我推荐可以看(Element 2 (饿了么组件))[https://github.com/ElemeFE/element/blob/dev/packages/theme-chalk/src/alert.scss]的 scss 代码,其也是使用了 BEM 的命名规范,下面我以其 alert 组件为例:
// B 即组件本身
.el-alert {
}
// BM 即当前组件的状态,这里指成功的样式
.el-alert--success {
}
// BE 即当前元素的某个具体功能,这里指内容
.el-alert__content {
}
2
3
4
5
6
7
8
9
10
11
其优点是结构清晰明了,看到类名就能知道其使用的目的。缺点是命名通常较长
# 2.CSS modules
css-modules 将 css 代码模块化,可以避免本模块样式被污染,并且可以很方便的复用 css 代码, 在现代的项目中,我们通常采用 scss/less 并搭配 webpack 插件来进行模块化的处理。实际上,它会帮我们把样式名增加一段哈希(hash)后缀,从而保证了其命名的独立。这也是目前许多项目在使用的一种方式。
webpack 的配置可以看看我的 script (opens new window),里面配置好了 css/less/scss 的模块化打包。 当然,现在 webpack 已经默认开启 CSS modules 功能,下面提一个最简单的使用方式
//webpack
{
test: /.css$/,
loader: "style-loader!css-loader?modules"
}
// ts
import styles from './index.css'
export default () => {
return (
<div className={styles.title}>
css style test.
</div>
);
};
// 举个例子 打包后:
<div class='title__lR4PU'>
css style test.
</div>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
该方法的优点是可以轻松的进行模块化处理样式问题,缺点是每次都要进行 styles.xx 的书写,且可读性差,如果你配置打包生成的 css 命名不像我这样为真实命名+hash 值而是全 hash 值的话,debug 极不方便。
# 3.CSS IN JS
CSS in JS 是 2014 年推出的一种设计模式,它的核心思想是把 CSS 直接写到各自组件中,也就是说用 JS 去写 CSS,而不是单独的样式文件里, 该方式通常出现在 React 框架里,因为 Vue 和 Angular 都有自己的样式隔离方案。当下热门的框架有
额外的:
- 像当前的 Antd V5 也采用了 CSS In Js 来处理其组件的整体样式问题, 相关仓库 (opens new window)
它允许你像写 JS/TS 一样去书写 Css, 举个例子
import styled from "styled-components";
export const EliminateWrap = styled.div<{ bg: any }>`
width: 100%;
height: 100%;
background: url(${(props) => props.bg}) no-repeat;
background-size: 100% 100%;
.rt-btn {
float: right;
width: 50px;
height: 148px;
img {
width: 50px;
height: 54px;
margin-top: 20px;
}
}
`;
export const MainContent = styled.div`
.ant-card {
margin-bottom: 13px;
.ant-card-body {
padding: 16px 20px;
}
}
`;
function App() {
return (
<MainContent>
<EliminateWrap bg="asdsad">CSS in JS TEST</EliminateWrap>
</MainContent>
);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
优点
你的 css 代码会和你的 js 代码共处,在这种做法成为「共置」,如果在项目中裸写 CSS 的话,不管你的 .css 文件被放置在哪里,你的样式和可能会作用到全局。如果你使用 CSS-in-JS,你可以直接在 React 组件内部书写样式,如果代码管理组织得明确,那么你的项目的可维护性将大大提升。并且其赋予了 JS 变量的能力。
缺点
CSS-in-JS 的运行时问题。当你的组件进行渲染的时候,CSS-in-JS 库会在运行时将你的样式代码 ”序列化” 为可以插入文档的 CSS 。这无疑会消耗浏览器更多的 CPU 性能
CSS-in-JS 让你的包体积更大了。 这是一个明显的问题。每个访问你的站点的用户都不得不加载关于 CSS-in-JS 的 JavaScript。Emotion 的包体积压缩之后是 7.9k ,而 styled-components 则是 12.7 kB 。虽然这些包都不算是特别大,但是如果再加上 react & react-dom 的话,那也是不小的开销。
CSS-in-JS 让 React DevTools 变得难看。 每一个使用 css prop 的 react 元素, Emotion 都会渲染成
和 组件。如果你使用很多的 css prop,那么你会在 React DevTools 看到一大堆自定义组件的名字,即使他们并没有实际的功能 它会被迫使得浏览器需要做更多的工作。React 进行渲染的每一帧,所有 DOM 元素上的 CSS 规则都会重新计算,它会不断的占用资源。
使用 CSS-in-JS ,会有更大的概率导致项目报错,特别是在 SSR 或者组件库这样的项目中, 通常是因为版本的不同。
总的来说,我经过实践及比较,我并不推荐使用这种方式进行样式的隔离,它不仅会消耗性能,还可能会使你的项目存在 cssinjs/less/scss/scoped scss 等等多种 css 组合方案,在代码里看上去十分混乱(除非你所有的都用 cssinjs 书写,但那很耗时间)。
# 4.Shadow Dom
Shadow Dom 是 Web Components 封装的一个重要属性,它能将一个隐藏的、独立的 DOM 附加到一个元素上。它相当于可以作为一个根节点,我们可以为其增加子节点、设置属性,并可以为其节点添加自己的内联样式,在 Shadow Dom 根元素以下相当于形成了一个局部作用域, 其内部的元素及属性的变化不会影响到它外部的元素。
基本用法 可以使用 Element.attachShadow() 方法来将一个 shadow root 附加到任何一个元素上。它接受一个配置对象作为参数,该对象有一个 mode 属性,值可以是 open 或者 closed:
let shadow = elementRef.attachShadow({ mode: "open" });
let shadow = elementRef.attachShadow({ mode: "closed" });
2
open 表示可以通过页面内的 JavaScript 方法来获取 Shadow DOM,例如使用 Element.shadowRoot 属性:
let myShadowDom = myCustomElem.shadowRoot; // 倘若mode为closed 则返回nulls
// 将所创建的元素添加到 Shadow DOM 上
myShadowDom.appendChild(document.createElement("span"));
myShadowDom.appendChild(wrapper);
myShadowDom.appendChild(icon);
myShadowDom.appendChild(info);
2
3
4
5
6
优点
- 局部作用域,样式隔离
- 原生属性支持
缺点
- 浏览器兼容问题
- 局部作用域,既是其优点,也有其缺点,例如当你添加的 dom 元素不在对应的作用域下时,会失去其样式。
对于上面第二点缺点的问题,在微前端中使用会出现较为常见的问题,微前端通常会将嵌入基座的子应用的全局变量 window 和 document 进行一层重写或是代理(具体可以看微前端方案的文章),并通过以上 css 方案做样式隔离。但它并未为对我们微前端列如: document.body.append 这类原生插入方案进行重写。当我们样式隔离在子应用下时(例如基座在#app 下挂载,子应用在#app 下的#micro-app 下挂载),原生的挂载方案会将插入的元素挂载在子应用的作用域之外,则造成了样式的冲突。而子应用又在基座之下,子应用可以得到主营用基座下的 CSS 样式,也存在了被污染的可能。
# 5.Vue Scoped
vue 的 scoped 则是通过 postcss 插件将原有 css 添加一个全局唯一的属性选择器来限制 css 只能在这个范围生效,也就是 scoped 的意思: 通过编译的方式在元素上添加了 data-xxx 的属性,然后给 css 选择器加上[data-xxx] 的属性选择器的方式实现 css 的样式隔离。
<style scoped>
.example {
color: red;
}
</style>
2
3
4
5
优点是简单好用,vue 集成好方案,无需多想,缺点是只适用于 vue。