在一些小型项目中,搭建样式体系并不复杂,但当涉及到更大、更复杂,需要团队协作的项目时,组织代码至少从以下几个方面考虑
- 编写代码所花时间
- 所需代码量
- 高性能,浏览器要做多少加载
- 可读性、灵活性、是否方便协同开发
因此,BEM的css命名方法论就诞生了!
概念
Bem 是块(block)、元素(element)、修饰符(modifier)的简写,由 Yandex 团队提出的一种前端 CSS, 命名方法论。class名可以获得更多的描述和更加清晰的结构;使代码易于阅读理解,方便协同开发
- 块(block) 代表更高级别的抽象或组件,可理解为组件最外层元素
- 元素(element)组件的后代元素
- 修饰符(modifier)表示块或者后代元素的 的不同状态或版本
命名规范
- -中弧线连接单词,可以是块元素也可以是子元素
- __ 双下划线 连接块与块的子元素 (其实单下划线也是可以的,但由于命名习惯中单下划线有时候也作为单词连接符,造成混淆,所以使用双下划线更为稳妥)
- -- 双中划线 描述一个块或者块的子元素的一种状态
- is is-关键字结合使用时,指示模块特定的状态类;一般用于js控制样式时,css命名用is-开头 例如 is-open、is-disabled
BEM的优势
我们以实际例子为证;在该例子当中,css的命名很松散,没有对应的关联关系,我们看不到card 与header和body的关联,也看不出该组件由什么组成
<div class="card shadow primary">
<div class="header">
<span>卡片名称</span>
<button type="button" class="button">
<span>操作按钮</span>
</button>
</div>
<div class="body">
<div class="text item">列表内容 1</div>
</div>
</div>
我们再来看看使用BEM 风格的命名,我们看到mk-card--primary是当前card组件的主题(修饰状态),mk-card__header和mk-card__body分别是mk-card的子元素;is-shadow则是代表该组件的一些控制样式
<div class="mk-card mk-card--primary is-shadow">
<div class="mk-card__header">
<span>卡片名称</span>
<button type="button" class="button">
<span>操作按钮</span>
</button>
</div>
<div class="mk-card__body">
<div class="text item">列表内容 1</div>
</div>
</div>
你看BEM的优势是不是出来了
- 可读性强 (类名语义化、结构化)
- 扩展性强 (CSS选择器的粒度足够地细,可灵活改动,不用考虑选择器之间的权重问题)
- 适应性强 (模块化复用的理念,让BEM很容易配合其他框架一起使用)
BEM 缺点
BEM经常受人诟病的一个点在于命名长而难看,但是我们不能因此忽略BEM给我们带来的好处,通过less/scss编写会更加便捷;我们要尽量避免过长的嵌套,这就要明确组件划分的细腻度,从哪里划分,何时划分。
实例分析
<div class="collapse">
<div class="wrap">
<div
class="header"
>
<div class="header-title">
<span>标题1</span>
</div>
<i class="right-icon"></i>
</div>
<div class="divider"></div>
<div class="content">
<div class="">Hello World</div>
</div>
</div>
</div>
看上面的命名方式,很难通过css的命名看出组件之间的关联,但似乎这种结构使用BEN命名风格,会出现多层级的嵌套;那么我们如何使用BEM构建层级关系但又避免多级嵌套呢?这就需要我们将该组件粒度细化,划分成多个小组件,以下面的结构为例
<div class="mk-collapse">
<div class="mk-collapse-item mk-collapse-item--expanded">
<!--
对于折叠面板的header 层,我们可以划分一个单元格组件 mk-cell 组件 减少嵌套,当前层级可加mk-collapse-item__title class建立关联,后续的子元素归于mk-cell的层级,后续mk-cell也可独立在其他地方复用
-->
<div
class="mk-cell mk-collapse-item__title "
>
<div class="mk-cell__title">
<span>标题1</span>
</div>
<i class="mk-icon mk-cell__right-icon"></i>
</div>
<!--
对于分割线这类常规公告样式,我们可以直接用class ,独立出来,不必可以建立关联
-->
<div class="divider"></div>
<div class="mk-collapse-item__wrapper">
<div class="mk-collapse-item__content">Hello World</div>
</div>
</div>
</div>
你看,这样划分,是不是就避免了多级嵌套,命名又臭又长的情况产生呢;
结合Element分析基于scss实现BEM的方法
回到我们本篇文章的重点板块了,结合element,我们来分析下sass如何实现BEM 结构命名;在源码解析的开始,如果对sass的高阶用法还没有一个系统的认识,建议先看看 SASS的知识体系构建这篇文章
element scss文件源码
在element 源码中,主题文件放在element/tree/dev/packages/theme-chalk目录
在 theme-chalk/src/mixins/config.scss 文件中,有对elment ui风格的基础配置
$namespace: 'el'; //前缀名
$element-separator: '__'; //子元素连接符
$modifier-separator: '--'; //块样式状态连接符
$state-prefix: 'is-'; //特定状态列前缀
在theme-chalk/src/mixins/mixins.scss 中,则定义了BEM的混合方法;
/* BEM
-------------------------- */
@mixin b($block) {
$B: $namespace+'-'+$block !global;
.#{$B} {
@content;
}
}
@mixin e($element) {
$E: $element !global;
$selector: &;
$currentSelector: "";
@each $unit in $element {
$currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
}
@if hitAllSpecialNestRule($selector) {
@at-root {
#{$selector} {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
}
@mixin m($modifier) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
$currentSelector: #{$currentSelector + & + $modifier-separator + $unit + ","};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
element scss源码解析
@mixin b($block) 的解析
@mixin b($block) {
$B: $namespace+'-'+$block !global; //变量拼接形成对应格式的class;再使用!global将其提升为全局变量
.#{$B} {
@content; // 使用混合;大括号后定义的样式将都会解析到这里
}
}
b方法相对来说比较简单;主要运用了以下知识点
- 使用变量的拼接运算;block为card,则拼接结果为el-card
- !global 变量提升为全局变量
- 使用插值语法将$B这个选择器作为选择器使用
- @content 将引用混合后大括号外额外的样式
我们可以通过案例更直观的感受;scss在线编译工具;也可以在我写的文章 SASS的知识体系构建 中的安装编译模块查看相关的编译方法
//编译前
$namespace: "mk";
@mixin b($block) {
$B: $namespace + "-" + $block !global;
.#{$B} {
@content;
}
}
@include b((card)) {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
@include e((hover-zone, title)) {
padding: 20px;
}
}
//编译后
.mk-card {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
-webkit-transition: 0.3s;
transition: 0.3s;
}
@mixin e($element) 解析
这一块相对来说比较复杂,
@mixin e($element) {
$E: $element !global;
$selector: &;
$currentSelector: "";
@each $unit in $element {
$currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
}
@if hitAllSpecialNestRule($selector) {
@at-root {
#{$selector} {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
}
先来分析这部分的代码
//定义混合
@mixin e($element) {
$E: $element !global;
$selector: &;
$currentSelector: "";
@each $unit in $element {
$currentSelector: #{$currentSelector +
"." +
$B +
$element-separator +
$unit +
","};
}
@debug $currentSelector; //我们使用sass debug语句来查看改变量的值
#{$currentSelector} {
content: "11";
@content;
}
}
//执行混合;b方法的定义在这里就不重复写了
@include b((card)) {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
@include e((title, body)) {
padding: 20px;
}
}
我们看看打印出的变量
实际上@each内 其实就是变量拼接,通过__符连接父级选择器和传入的子元素,而传入的值可以是一个,也可以是数组;通过实例来理解
//执行混合;b方法的定义在这里就不重复写了
@include b((card)) {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
@include e(footer) { //传入单个
padding: 20px;
}
@include e((title, body)) { //传入数组
padding: 20px;
}
}
//编译后
.mk-card .mk-card__footer { //传入单个编译结果
content: "11";
padding: 20px;
}
.mk-card .mk-card__title, //传入数组编译结果
.mk-card .mk-card__body {
content: "11";
padding: 20px;
}
再来分析@if 这块的逻辑
@if hitAllSpecialNestRule($selector) {
@at-root {
#{$selector} {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
hitAllSpecialNestRule 是在 ==theme-chalk/src/mixins/function.scss== 中 定义的方法,其主要作用是判断父级选择器是否包含'--','is-',':'
如果包含则e方法中定义的样式要嵌套于改父级样式之下;若不包含,则通过@at-root跳出选择器嵌套,
/* BEM support Func
-------------------------- */
@function selectorToString($selector) {
$selector: inspect($selector);
$selector: str-slice($selector, 2, -2);
@return $selector;
}
@function containsModifier($selector) {
$selector: selectorToString($selector);
@if str-index($selector, $modifier-separator) {
@return true;
} @else {
@return false;
}
}
@function containWhenFlag($selector) {
$selector: selectorToString($selector);
@if str-index($selector, '.' + $state-prefix) {
@return true
} @else {
@return false
}
}
@function containPseudoClass($selector) {
$selector: selectorToString($selector);
@if str-index($selector, ':') {
@return true
} @else {
@return false
}
}
@function hitAllSpecialNestRule($selector) {
@return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}
- containsModifier 方法是判断父级选择器是否包含'--'
- containWhenFlag 方法是判断 父级选择器是否包含'.is-'
- containPseudoClass 方法是判断 父级是否包含 ':'
理解这几个函数只需要弄清楚自定义函数selectorToString以及inspect和str-slice、str-index这三个sass内建函数 (就是sass自带函数)即可
-
inspect( value)函数来生成一个对调试Map有用的输出字符串,因为Map无法转换为纯CSS。使用一个作为CSS函数的变量或参数的值将导致错误。
-
str-slice(start-at, string 中截取子字符串,通过 end-at 设置始末位置,未指定结束索引值则默认截取到字符串末尾。
-
str-index(substring) 返回一个下标,标示 string 中的起始位置。没有找到的话,则返回 null 值。这里$string必须为字符串 这里的下标都是从 1 开始
我们结合debug语句来看看selectorToString 的执行
//这里有人可能觉得这里是多此一举,但其实这里是通过inspect和str-slice把传入的选择器变量转换为字符串;因为在str-index 函数中传入的str-index($string, $substring)中,$string 必须要是一个字符串
@function selectorToString($selector) {
@debug "初始化的选择器: #{$selector}"; //打印结果为.mk-card
$selector: inspect($selector);
@debug "inspect格式化后的选择器: #{$selector}"; //打印结果为 (.mk-card)
$selector: str-slice($selector, 2, -2);
@debug "str-slice格式化后的选择器: #{$selector}"; //打印结果为.mk-card;所以我觉得这里不是多余的么
$selector: inspect($selector);
@return $selector;
}
@include b(card) {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
@include e(footer) {
//传入单个
padding: 20px;
}
}
再来分别看看containsModifier、containWhenFlag、containPseudoClass这几个函数
@function containsModifier、($selector) {
$selector: selectorToString($selector);
//看看当前选择器是否存在$modifier-separator这个变量也就是"--";
@if str-index($selector, $modifier-separator) {
@return true;
} @else {
@return false;
}
}
@function containWhenFlag($selector) {
$selector: selectorToString($selector);
//看看当前选择器是否存在$state-prefix这个变量也就是"is-";
@if str-index($selector, "." + $state-prefix) {
@return true;
} @else {
@return false;
}
}
@function containPseudoClass($selector) {
$selector: selectorToString($selector);
//看看当前选择器是否存在":";用于判断伪类和伪元素
@if str-index($selector, ":") {
@return true;
} @else {
@return false;
}
}
我们可以看看含有这几个元素与不含这些元素的编译区别
//****************不含"--"、"is-"、 ":"
//编译前 不包含,则通过@at-root跳出选择器嵌套
.mk-card{
$selector: &;
@include e(footer) {
//传入单个
padding: 20px;
}
}
//编译后
.mk-card__footer {
padding: 20px;
}
//****************含"--"、"is-"、 ":"
//编译前 ;包含则嵌套于父级选择器下
.mk-card:hover {
$selector: &;
@include e(footer) {
//传入单个
padding: 20px;
}
}
//编译后
.mk-card:hover .mk-card__footer {
padding: 20px;
}
到这里,@mixin e($element) 的解析就完了
@mixin m($modifier)
@mixin m($modifier) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
$currentSelector: #{$currentSelector +
& +
$modifier-separator +
$unit +
","};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
其实与@mixin e(modifier-separator(--)拼接;我们看看编译结果就理解了
//编译前
@include b(card) {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
@include e(footer) {
//传入单个
padding: 20px;
}
@include m(primary) {
background: #409eff;
}
}
//编译后
.mk-card {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
-webkit-transition: 0.3s;
transition: 0.3s;
}
.mk-card__footer {
padding: 20px;
}
.mk-card--primary {
background: #409eff;
}
至此,清楚的了解B、E、M这三个混合的实现原理,剩下的其他混合其实就都可以理解了
@mixin configurable-m(E-flag: false)
不做过多分析,直接看编译结果就懂了
@mixin configurable-m($modifier, $E-flag: false) {
$selector: &;
$interpolation: "";
@if $E-flag {
$interpolation: $element-separator + $E-flag;
}
@at-root {
#{$selector} {
.#{$B + $interpolation + $modifier-separator + $modifier} {
@content;
}
}
}
}
@include b(card) {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
@include configurable-m(primary, task) {
display: flex;
}
@include configurable-m(primary, false) {
display: flex;
}
}
//编译后
.mk-card .mk-card__task--primary {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
.mk-card .mk-card--primary {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
@mixin spec-selector(element: modifier: false,B)
不做过多分析,直接看编译结果就懂了
@mixin spec-selector(
$specSelector: "",
$element: $E,
$modifier: false,
$block: $B
) {
$modifierCombo: "";
@if $modifier {
$modifierCombo: $modifier-separator + $modifier;
}
@at-root {
#{&}#{$specSelector}.#{$block
+ $element-separator
+ $element
+ $modifierCombo} {
@content;
}
}
}
@include b(card) {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
@include e(footer) {
//传入单个
padding: 20px;
@include spec-selector(
$specSelector: "",
$element: $E,
$modifier: primary,
$block: $B
) {
display: flex;
}
}
}
//编译后
.mk-card {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
-webkit-transition: 0.3s;
transition: 0.3s;
}
.mk-card__footer {
padding: 20px;
}
.mk-card__footer.mk-card__footer--primary {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
@mixin meb(element: block: $B)
不做过多分析,直接看编译结果就懂了
@mixin meb($modifier: false, $element: $E, $block: $B) {
$selector: &;
$modifierCombo: "";
@if $modifier {
$modifierCombo: $modifier-separator + $modifier;
}
@at-root {
#{$selector} {
.#{$block + $element-separator + $element + $modifierCombo} {
@content;
}
}
}
}
@include b(card) {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
transition: 0.3s;
@include e(footer) {
//传入单个
padding: 20px;
@include meb($modifier: primary) {
display: flex;
}
}
}
//编译后
.mk-card {
border-radius: 4px;
border: 1px solid #ebeef5;
background-color: #fff;
overflow: hidden;
color: #303133;
-webkit-transition: 0.3s;
transition: 0.3s;
}
.mk-card__footer {
padding: 20px;
}
.mk-card__footer .mk-card__footer--primary {
display: -webkit-box;
display: -ms-flexbox;
display: flex;
}
@mixin when($state)
定义一些状态样式;不做过多分析,直接看编译结果就懂了
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
@include when(active) {
color: red;
}
//编译后
.is-active {
color: red;
}
到这里 ,结合Element分析基于scss实现BEM的方法分析就结束了,此篇文章也是自己多sass的学习的一个总结,文章可能会有一些不足之处,欢迎大家批评指正,当然如果觉得这篇文章有干货,也可以点个赞赞鼓励一下哦;后期也会以sass在项目中的实际运用,模块化的架构做一次总结分析,敬请期待!