本文已参与「新人创作礼」活动, 一起开启掘金创作之路。
背景
css 没有作用域的概念,这就导致 css 在书写的时候都是全局作用域。产生的影响如下:
- 开发者在书写过程中会造成因
classname相同,产生样式污染、css规则层叠(CSS Cascade)。
- 因某一个元素具有不同的状态增加众多的冗余css代码。
随着 SPA 应用的兴起,全部的 css 会在同一个环境中同时进行加载,相互影响的问题更加严峻。
方案
目前业界几种常见的 css 隔离方案分为两种,一种是运行时,一种是编译时。
运行时,即我们常见的 命名规范。 通过规则进行约束开发人员,但是并不能完全 css 隔离,也不适用于现在工程化的方案。随着构建工具的改进、工程化集成的出现,出现了几种比较常见的方案,如css in js、css module、vue 的 scoped。
- 命名规范( BEM 、OOCSS、SMACSS等)
- css in js
- css module
- style scoped(Vue)
- shadow DOM
介绍
命名规范
BEM
由 Yandex 提出,核心思想是通过维护一个唯一的 className 实现样式局部性的概念。
BEM 是 Block Element Modifier 的缩写,指代 块 - 元素 - 修饰符(.block_name__element_name--modifier)。详细使用说明
- Block 以元素本身所具备的功能为主。
- Element 以元素本身的位置与形状、描述为主。
- Modifier 以当前元素的的颜色,状态为主。
例如:
.Selector{}
.Selector__SItem {}
.Selector__SItem--focus{}
<section class="container">
<p class="container__paragraph">This is a paragraph inside a container.</p>
<p class="container__paragraph container__paragraph--bold">
This is a paragraph inside a container, with a modifier that adds bold styling.
</p>
</section>
在使用过程中我们往往会配合预处理器( less、sass )进行协同开发。
.Selector {
&__SItem {
&--focus{
}
}
}
优点
看到 classname 就能知道对应的 html 结构,功能明确。
缺点
一般 classname 会比较长,相对麻烦
OOCSS
Object-Oriented CSS,面向对象的 css ,借鉴了面向对象的编程思想,将元素的样式抽离为更小的模块,使其能够不依赖任何容器也能够独立的显示样式。
两个主要的原则
-
Separate structure and skin (分离结构与皮肤)
- 这意味着将重复的视觉特征(如背景和边框样式)定义为单独的“皮肤”,您可以将其与各种对象混合搭配,无需太多代码即可实现大量视觉变化。
-
Separate container and content (分离容器与内容)
- 本质上,这意味着“很少使用与位置相关的样式”。一个物体无论放在哪里都应该是一样的。因此,不要使用设置特定的样式。
为什么使用?
It’s scalable. OOCSS allows you to freely mix and re-apply classes on different elements without much regard for their context. Instead of piling on CSS, newcomers to a project can reuse what their predecessors have already abstracted out.
Boost in site speed. Cutting down on repetition helps applications run faster. CSS files have a habit of expanding exponentially as websites grow in complexity, thus increasing web page size.
It’s maintainable. Adding or rearranging HTML markups no longer requires you to rethink your entire CSS flow. This is especially helpful for larger ongoing projects.
It’s readable. When other programmers see your CSS, they’ll be able to quickly understand its structure.
It’s a proven approach. Understanding the object-oriented methodology makes it easier to learn programming languages like PHP. Conversely, anyone who already understands PHP can quickly pick up OOCSS.
是可扩展的。 可以在不同元素上自由搭配类名
有助于提升网站加载速度。 由于是可复用的情况,可以减少css的文件大小,进而减小网页文件的大小。
是可维护的。
是可读的。
很容易轻松上手。
劣势
小型项目不适用:小型项目不一定需要可伸缩性、可读性和可维护性。
它增加了元素类的数量。您可能需要向一个元素添加多个类以考虑所有样式元素。这可能会给那些不熟悉 OOCSS 的人带来一些困惑,并且会使您的标记变得混乱。
有一个学习曲线。如果您正在使用 OOCSS 而您的同事不熟悉它,这将需要他们在继续之前学习如何使用它,这需要时间。
分离结构与皮肤:
组件的结构是指对用户 “不可见”的东西, 例如元素大小和定位的说明。这些属性包括:
- 高度
- 宽度
- 边距
- 填充
- 溢出
应用程序的皮肤是指元素的视觉属性,例如:
- 颜色
- 字体
- 阴影
- 渐变
例如:
// bad
<button class='btn-class btn-class-error'></button>
<button class='btn-class-error'></button>
<style>
.btn-class{
color:blue;
padding:4px;
font-size:12x;
line-height: 20px;
}
.btn-class-error{
color:red;
padding:4px;
font-size:12x;
line-height: 20px;
}
</style>
// good
<button class='btn-class btn-class-error'></button>
<button class='btn-class btn-class-primary'></button>
<style>
.btn-class{
padding:4px;
font-size:12x;
line-height: 20px;
}
.button-class-primary{
color:blue;
}
.btn-class-error{
color:red;
}
</style>
在上面的案例中我们可以理解为 button-class-primary 是 btn-class 的子类。通过继承的思想获得预期结果。
分离 容器 与内容
OOCSS 的第二个原则是容器和内容的分离。我们应该很少使用依赖于位置的样式,因为这些样式会被锁定到特定的选择器中。一个物体的“皮肤”无论在哪里都应该看起来一样。将容器与内容分开可以提供更加一致和可预测的用户体验。在这种情况下,内容是指嵌套在其他元素中的元素,例如图像、段落和 div 标签,这些元素充当容器。大多数容器可以由结构类表示。
例如:
// bad
<div class='example'>
<p></p>
</div>
.example p{
color: red;
background-color: pink;
font-size:12px;
width: 100px;
height:100px;
}
// good
<div>
<p class='shape color-red bg-pink font-size-12'></p>
</div>
.shape{
width: 100px;
height:100px;
}
.color-red{
color:red;
}
.bg-pink{
background-color:pink;
}
.font-size-12{
font-size:12px
}
我们需要将一个大的元素块,拆分为不同的子块,然后在元素上进行组装。这样的好处也是显而易见 css 重复的样式会很少。
SMACSS
Scalable and Modular Architecture for CSS 是 Jonathan Snook 提出。
它的核心是分类,通过对 css 规则的分类。
在 smacss 中将 css 分为5类别:
- Base (基础规则)
- Layout (布局规则)
- Module(模块规则)
- State (状态规则)
- Theme (主题规则)
每一种类别通过 perfix 来进行类型的区分,如下:
Base 规则:他们是单元素的选择器,也是最基础的原子,不需要添加特定的标识。例如:
html, body, form { margin: 0; padding: 0; }
input[type=text] { border: 1px solid #999; }
a { color: #039; }
a:hover { color: #03C; }
Layout 规则:将页面区分为多个部分,而布局就是一个或者多个模块的组合。通过 l- 或者 layout- 或者 grid- 的 perfix 作为区分。例如:
.l-inline { }
.l-header {}
Module 规则: 页面中可复用的模块化的部分,侧边栏、产品列表、角色信息等。由于其可以成为任何项目的主体,因此以 m- 或者 module- 作为开头是没有必要。建议是通过每个模块的功能作为perfix,例如:
/* Example Module */
.example { }
/* Callout Module */
.callout { }
/* Callout Module with State */
.callout.is-collapsed { }
/* Form field module */
.field { }
state规则: 用来描述我们模块或者布局处于某一个特定的状态下的外观方法,比如元素展示/隐藏,模块在不同视图、状态下的外观。通过 is- 的 prefix 作为区分。例如:
/* Callout Module with State */
.callout.is-collapsed { }
主题规则:类似于 state规则,定义了整个应用程序或者网站的外观与感觉。其内容可以影响任何类型的规则。 t- / theme-
<div class='l-fixed'>
<div id='article'>
<p>基本模块</p>
<div class='article-title'>title</div>
<button class='is-btn-acitve'>active</button>
</div>
</div>
ITCSS
Inverted Triangle Cascading Style Sheets (倒三角css)Harry Roberts 提出。 他个人的 npm 仓库
倒三角从上向下代表特性增加,产生的影响会越来越具体,后面的样式可以影响到前面的样式。我们来看一下分别代表什么含义。
| Setting | 自定义变量,比如字体,颜色定义等。不写具体的css |
|---|---|
| Tools | 全局使用的mixin和功能。不写具体的css |
| Generic | 重置和/或标准化样式、框大小定义,例如 normalize.css、reset.css。开始写具体css |
| Base(Elements) | 定义网站 HTML 元素的样式 |
| Objects | 类名样式中,不允许出现外观属性,例如 Color。类似于OOCSS中的结构 |
| Components | UI组件。 |
| Trumps | 用于辅助和微调的样式,ps:只有这一层才可以使用important |
例如:
即使将 css 区分为7层,依旧会比较抽象。比如在 component 层,我们需要考虑应该去如何处理组件的具体实现。在这里 ITCSS 的作者提倡使用 BEM 的方式相结合,成为 BEMIT 。
项目参考地址:
ITCSS的开源示例 (ITCSS创建者的个人提供)
ACSS
一个样式一个类,所有的类都是一个原子。典型的 css 框架: TailwindCSS
例如:
.color-red{
color: red;
}
.hidden{
display:none;
}
.flex{
display:flex;
}
以上就是命名规范的部分的相关知识。
下图是2020年统计的 css 设计模式的使用情况。网站地址
css module
css module 可以使我们像 js 一样的去引入 css,并且每一个css都会是一个独立的模块,我们可以在使用过程中通过变量的形式进行引入。并且会在构建过程中,生成唯一确定的 hash 通过映射关系找到对应的 className 。让我们来看看他的神奇:
首先我们先开启 css-module :我们使用的插件是 css-loader ,它对于css module 的支持比较好。
//webpack.config.js
rules: [{
loader: 'css-loader',
options: {
modules: true
}
}]
局部作用域
默认情况下,启动 css-module 模式以后,我们在css/less中编写的样式,就是局部的样式。当然我们也可以通过 :local(.className) 例如
import React from 'react';
import styles from './index.less'
const CssModules = () => {
return <>
<div className={styles.title}>
css modules
</div>
</>
}
export default CssModules
// index.less
.title{
color: green;
}
:local(.title) {
color: pink
}
// 编译后
.OsUATsUmVtlCrqBvKGE5 {
color: green;
}
.kRFrWBrC47GRspHy73aE {
color: pink;
}
\
最终我们 className 会被编译为一串 hash。
全局作用域
我们通过 :global(.className) 的语法,声明一个全局变量,此时就不会被编译为 hash,而是原样进行输出,使用时直接使用对应的 className 即可。
import React from 'react';
import styles from './index.less'
const CssModules = () => {
return <>
<div className='title'>
css modules
</div>
</>
}
export default CssModules
:global(.title){
color: gray;
}
// 编译后
.title{
color: gray;
}
定制 hash
modules: {
localIdentName: '[path][name]__[local]--[hash:base64:5]',
}
upported template strings:
[name]the basename of the resource
[folder]the folder the resource relative to thecompiler.contextoption ormodules.localIdentContextoption.
[path]the path of the resource relative to thecompiler.contextoption ormodules.localIdentContextoption.
[file]- filename and path.
[ext]- extension with leading..
[hash]- the hash of the string, generated based onlocalIdentHashSalt,localIdentHashFunction,localIdentHashDigest,localIdentHashDigestLength,localIdentContext,resourcePathandexportName
[<hashFunction>:hash:<hashDigest>:<hashDigestLength>]- hash with hash settings.
[local]- original class.
.title{
color: green;
}
//编译后
.docs-demos-cssModules-index__title--OsUAT{
}
Class 组合
.className {
color: green;
background: red;
}
.otherClassName {
composes: className;
color: yellow;
}
需要注意 composes 需要在其他的rules之前进行使用。
输入其他模块
.className {
color: green;
background: red;
}
.otherClassName {
composes: className from './xxx.less';;
color: yellow;
}
使用变量
通过 @value 关键字定义特殊变量
建议使用 v- 对应值。 s- 代表选择器, m- 代表媒体查询
@value v-primary: #BF4040;
@value s-black: black-selector;
@value m-large: (min-width: 960px);
.header {
color: v-primary;
padding: 0 10px;
}
.s-black {
color: black;
}
@media m-large {
.header {
padding: 0 20px;
}
}
详细的使用以及其他的 css-loader 配置,css-loader地址
css in js
其核心的思想就是将 css 直接写在组件中,并不是说通过引入的方式,而是指通过 js 的方式去写css。这与我们视图层与逻辑层分离的概念相悖(关注点分离)。
随着 React 、 Vue、 Angular 框架的出现,开发者更期望将样式写入到组件内,而 Vue、Angular都拥有自己的样式隔离方案。 虽然 React 中没有提供对应的隔离方案,但是关注点混合观点在社区中有不错的支持者。
Any application that can be written in JavaScript, will eventually be written in JavaScript. —— Jeff Atwood
例如:
const CssModules = () => {
const styles = {
color: 'pink',
border: '1px solid #ccc'
}
return <>
<div style={styles} >
css modules
</div>
</>
}
目前对于 css-in-js 已经有很多成熟的库(统计数据:超过60个),流传最广的就是 style-components 使用说明
我们简单演示一下 style-componens
import React from 'react';
import styled, { css } from 'styled-components'
// Create a Title component that'll render an <h1> tag with some styles
const Title = styled.h1`
font-size: 1.5em;
text-align: center;
color: palevioletred;
`;
// Create a Wrapper component that'll render a <section> tag with some styles
const Wrapper = styled.section`
padding: 4em;
background: papayawhip;
`;
const CssModules = () => {
return <>
<Wrapper>
<Title>
Hello World!
</Title>
</Wrapper>
</>
}
export default CssModules
Vue style scoped
在 Vue 中,style 标签上添加 scoped 属性,使用 className 的元素上,通过添加 data-xxx 属性,对 css 建立作用域。
例如:
<style scoped>
.example {
color: red;
}
</style>
<template>
<div class="example">hi</div>
</template>
转换以后
<style>
.example[data-v-f3f3eg9] {
color: red;
}
</style>
<template>
<div class="example" data-v-f3f3eg9>hi</div>
</template>
Vue 通过 vue-loader 插件进行转换。其工具集成 ****css module 的能力。 在 style 上面添加 module 字段,在组件中就可以通过 $style.xxx 的方式进行转换。如下:
<template>
<p :class="$style.red">
This should be red
</p>
</template>
<style module>
.red {
color: red;
}
</style>
更多使用说明: vue-scoped 使用文档
Shadow DOM
我们都知道 web Components 具有封装性,而 Shadow DOM 接口是关键所在,它可以将一个隐藏的、独立的 DOM 附加到一个元素上。我们看一下,官方对于 Shadow DOM 的解释:
- Shadow DOM(影子DOM):一组JavaScript API,用于将封装的“影子”DOM树附加到元素(与主文档DOM分开呈现)并控制其关联的功能。通过这种方式,您可以保持元素的功能私有,这样它们就可以被脚本化和样式化,而不用担心与文档的其他部分发生冲突。
例如:
我们会创建两个容器。
<div>
<div class="wrapper">shaodow 外面的wrapper</div>
<popup-info>shaodow 里面的wrapper</popup-info>
</div>
一个容器内容是 shadow 内部 通过 attachShadow 的方式创建一个shadow DOM。它会引用style.css 。元素上面有一个 className 叫做 wrapper。
// Create a class for the element
class PopUpInfo extends HTMLElement {
constructor() {
// Always call super first in constructor
super();
// Create a shadow root
const shadow = this.attachShadow({mode: 'open'});
// Create spans
const wrapper = document.createElement('span');
wrapper.innerHTML = this.innerHTML;
wrapper.setAttribute('class', 'wrapper');
// Apply external styles to the shadow dom
const linkElem = document.createElement('link');
linkElem.setAttribute('rel', 'stylesheet');
linkElem.setAttribute('href', 'style.css');
// Attach the created elements to the shadow dom
shadow.appendChild(linkElem);
shadow.appendChild(wrapper);
}
}
// Define the new element
customElements.define('popup-info', PopUpInfo);
//style.css
.wrapper{
color:pink
}
一个容器的内容是 shadow 外部 ,通过原生方法创建。他会引用 index .css。元素上面也有一个叫 wrapper 的 className。
//index.css
.wrapper{
color:pink
}
我们可以看到外面的 className 并没有影响到 shadow DOM 内 的 className。即便我们添加了最高优先级,两个 className之间依旧不能相互影响,(ps:input、video 这些标签,本身也是web Components)
优势:
浏览器原生支持。
具有足够的封装性,对于事件、样式都能够很好的与外层隔离。类似于iframe。
缺点:
浏览器的兼容性问题。
没有从根本上解决组件内部资源的相对路径问题。
总结
总的来说,css 的隔离方案分为两个方向:
一个方向是运行过程中,以 BEM 为首,根据 css 的设计模式,输出不同功能模块、不同作用域的样式表,此方向还是主人为控制作用域,无法做到完全的样式表隔离。
另一个方向是编译过程中,通过构建工具帮助开发者进行样式表隔离,做到全自动、工程化控制。当然在这其中也会掺杂运行时的一些设计理念。