在一些小型项目中,搭建样式体系并不复杂,但当涉及到更大、更复杂,需要团队协作的项目时,组织代码至少从以下几个方面考虑
- 编写代码所花时间
- 所需代码量
- 高性能,浏览器要做多少加载
- 可读性、灵活性、是否方便协同开发
因此,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方法相对来说比较简单;主要运用了以下知识点
- 使用变量的拼接运算;namespace在config.scss文件中有定义,为“el”,假设namespace 在config.scss文件中有定义,为“el”,假设namespace在config.scss文件中有定义,为“el”,假设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)使用inspect(value) 使用inspect(value)使用inspect( value)函数来生成一个对调试Map有用的输出字符串,因为Map无法转换为纯CSS。使用一个作为CSS函数的变量或参数的值将导致错误。
-
str-slice(string,string, string,start-at, end−at:−1)从end-at:-1) 从 end−at:−1)从string 中截取子字符串,通过 start−at和start-at 和 start−at和end-at 设置始末位置,未指定结束索引值则默认截取到字符串末尾。
-
str-index(string,string, string,substring) 返回一个下标,标示 substring在substring 在 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(element)很相似,这里不做过多描述;与element)很相似,这里不做过多描述;与element)很相似,这里不做过多描述;与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(modifier,modifier, modifier,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(specSelector:””,specSelector: “”,specSelector:““,element: E,E,E,modifier: false,block:block: block: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(modifier:false,modifier: false, modifier:false,element: E,E, E,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在项目中的实际运用,模块化的架构做一次总结分析,敬请期待!