秋名山上行人稀,常有车神较高低
我叫阿毛,说起来你可能不信,曾经我在秋名山上飙 CSS 也是弯道超 86 的狠角色,
直到有一天……我换了辆叫 BEM 的车:
----------------------------------------------------------------------
如果我被问到写 BEM 是什么感觉,就一个字:长长长长长长长长长长长长长长长长,要问我具体哪里长?来,先看看这样一段 CSS 来写一个人:
.human {
// 为了少打点字,下面就简称 block: b | element: e | modifier: m
&__finger {
&--little {}
}
// case.1 [ b--m 中嵌套 b__e ]
&--male {
.human__leg {}
}
// case.2 [ 伪类或者伪元素中嵌套 b__e ]
&:hover {
.human__hand {}
}
// case.3 [ state 中嵌套 b__e ]
&.is-hurt {
.human__head {}
}
// case.4 [ 任意情况下嵌套 +, ~ 等特殊的选择符 ]
&__arm {
&:focus {
& ~ .human__hand--right {}
}
}
// case.5 [ 共享规则 ]
&__teeth, &__tongue {}
}
上面列举了几种原始画风下 sass 开发时经常会碰到几种不得不把 .block__element--modifier 写全的几种情况,也就是 本文 想解决的 BEM 开发比较痛的地方。嗯?那最痛的是什么? —— 最痛的是还要加 namespace… (这里还想说下我自己感觉写 BEM 最大的作用是为了让 CSS 会说话,而不只是为了解决规则冲突,所以并不是有 CSS-Module 就可以不要 BEM 了)
今天我们的目标就是写这段代码少敲几次键盘,用 sass 去达到尽量不去写完整的 .b__e--m 结构的代码,这就开始吧( FBI WARNING :观看本文你需要一个能智能自动补全的 IDE )
第零步:最简单的 bem-mixins
// 这步比较简单就直接贴代码了
$element-separator: '__';
$modifier-separator: '--';
@mixin b($block) {
.#{$block} {
@content;
}
}
@mixin e($element) {
$selector: &; // & 里面保存着上下文,在这个 mixin 中其实期待的的就是 block
@at-root { // @at-root 指规则跳出嵌套,写在最外层
.#{$selector+ $element-separator + $element} {
@content;
}
}
}
@mixin m($modifier) {
$selector: &;
@at-root {
.#{$selector + $modifier-separator + $modifier} {
@content;
}
}
}
这样子我们写一个 小指头 就是:
@include b(human) {
@include e(finger) {
@include m(little) {}
}
}
第一步:解决 case.1,b--m 内嵌 b__e
// 如果用上面的 mixin 直接写 「雄性的腿」
@include b(human) {
@include m(male) {
@include e(leg) {
// 这里会直接输出 .human--male__leg 而不是 .human--male .human__leg
}
}
}
所以我要做的就是判断当 e 在 m 内部的时候改变成嵌套输出而不是直接拼接,可以上下文其中是否存在「--」来判断
@function containsModifier($selector) {
$selector: selectorToString($selector);
@if str-index($selector, $modifier-separator) {
@return true;
} @else {
@return false;
}
}
// 然后 e 就可以这样写
@mixin e($element) {
$selector: &;
@if containsModifer($selector) {
@at-root {
#{$selector} {
// 这里问题就来了,这里需要个 block 名我们该怎么获取呢?下面讨论
.#{[block名] + $element-separator + $element} {
@content;
}
}
}
} @else {
... // 原来的代码
}
}
关于 block 名的获取可以想到两种办法,一种是根据上下文中的 .b--m 或者 .b__e--m 去进行字符串切割(通过 sass 的 str-index 和 str-slice 实现,这里不展开说,可以看这篇文章,因为感觉不够优雅),要说的是第二种简单的办法,利用 全局变量实现!
// 简单来说就是在一个 block 中将一个全局变量锁定,多个或者多文件编译不会冲突
$B: ''; // 存储当前 block 名
$E: ''; // 也可以存储下 element 名
@mixin b($block) {
$B: $block !global; // ***!global 将这个值覆盖到全局变量***
... // 原来的代码
}
// 所以 e 里那句话就可以写成
@mixin e($element) {
$E: $element !global;
...
@if containsModifer($selector) {
@at-root {
#{$selector} {
.#{$B + $element-separator + $element} {
@content;
}
}
}
}
...
}
// 接着写 「雄性的腿」
@include b(human) {
@include m(male) {
@include e(leg) {
// 就会正确输出 .human--male .human__leg 了
}
}
}
case.1 解决了,这时候我们回过头去看一下 case.2 和 case.3
.human {
// case.2 [ 伪类或者伪元素中嵌套 b__e ]
&:hover {
.human__hand {}
}
// case.3 [ state 中嵌套 b__e ]
&.is-hurt {
.human__head {}
}
}
然后可以发现这些是一个道理,只不过判断的标志不同,判断是否存在「:」和 「is-」而已:
// 先说下 state 的约定,这里用了「is-」,这是需要实现自己定好的,
$state-prefix: 'is-';
@mixin when($state) {
@at-root {
#{&}.#{$state-prefix + $state} {
@content;
}
}
}
// 这里***必须要注意***的是不能直接写 &:hover 这样之类的了,
// 会有个恶心的表现就是 &:hover 包装下的 & 会进行重复拼接:
// @incude b(block) {
// &:hover {
// @error &;
// // 这里打印的 & 不会是想象的 .block:hover 而是 .block .block:hover
// // 猜测是因为这算第二次去读取上下文,第一次会在 &:hover 里,所以有出入
// }
// }
// 所以对伪pseudo 也进行了下包装,用 @at-root 来重置 & 读取次数的计数
@mixin pseudo($pseudo) {
@at-root #{&}#{':#{$pseudo}'} {
@content
}
}
@function containWhenFlag($selector) {
$selector: selectorToString($selector);
@if str-index($selector, '.' + $state-prefix) {
... // 根据结果返回 true/false
}
}
@function containPseudoClass($selector) {
$selector: selectorToString($selector);
@if str-index($selector, ':') {
... // 根据结果返回 true/false
}
}
// 这时候写 case.2 和 case.3 就是这么的 easy 了
@include b(human) {
@include when(hurt) {
@include e(hand) {}
}
@include pseudo(hover) {
@include e(head) {}
}
}
第二步:case.4,任意情况下嵌套 +, ~ 等特殊的选择符
专栏好像不能返回顶部,这里就再贴一下吧
@include b(human) {
// case.4 [ 任意情况下嵌套 +, ~ 等特殊的选择符的 bem 结 ]
&__arm {
&:focus
& + .human__arm {
&--left {}
}
& ~ .human__hand--right {}
}
}
}
直接贴 mixin ,这个比较突然的自我
// 这个mixin 可以直接生成 .b__e 也可以生成 .b__e--m
// 参数的顺序设置成了 (选择符, element, modifier, block)
// 因为一般情况下 block 改动最小,有个默认值 $E 就够了
@mixin spec-selector($specSelector: '', $element: $E,$modifier: false, $block: $B) {
// 判断输出的是 b__e 还是 b__e--m
@if $modifier {
$modifierCombo: $modifier-separator + $modifier;
}
@at-root {
#{&}#{$specSelector}.#{$block+$element-separator+$element+$modifierCombo} {
@content
}
}
}
// 然后写写看,bingoooooo
@include b(human) {
@include e(arm) {
@include pseudo(focus) {
@include spec-selector('+') {
// .human__arm:focus + .human__arm
@include m(left) { // .human__arm:focus + .human__arm--left }
}
@include spec-selector('~', hand, right) {
// .human__arm:focus ~ .human__hand--right
}
}
}
}
第三步:case.5,共享规则
.human {
// case.5 [ 共享规则 ]
&__teeth, &__tongue {}
// 这里其实比较有意思,在实际开发中一般遇到共享规则,大部分情况还会有对单个的定义
&__teeth {}
&__tongue {}
// 这样子其实写得繁琐了,在 SASS 中可以创建个「共享规则」,
// 然后用 @extend 进行继承最好,例如
// 编译结果是一样的哦,@extend 会将有公共规则的选择器拎出来组成 a, b {} 的形式
%shared-rule {}
&__teeth {
@extend %shared-rule;
}
&__tongue {
@extend %shared-rule;
}
}
然后呢,这跟 BEM 又有什么关系呢,我们先不妨按照之前的写一遍
@include b(human) {
%shared-rule {}
@include b(teeth) { @extend %shared-rule; }
@include b(tongue) { @extend %shared-rule;}
}
// 这样子输出其实不会是想像的那样
// 期望的
.human__teeth, .human__tongue {}
// 实际的...
.human .human__teeth, .human .human__tongue {}
// 这里是因为 %shared-rule 被定义在了 .human 内部 所以其实把上下文也是带进来的
// 要解决的话也很简单,用 @at-root 来做,定义下这样两个 mixin
@mixin share-rule($name) {
$rule-name: '%shared-'+$name;
@at-root #{$rule-name} {
@content
}
}
@mixin extend-rule($name) {
@extend #{'%shared-'+$name};
}
// 然后!
@include b(human) {
@include share-rule(skin) {}
@include b(teeth) { @include extend-rule(skin); }
@include b(tongue) { @include extend-rule(skin); }
}
// ***注意:这里共享规则的名字不能重复,不然不会覆盖****
// ***注意:所以需要一个 list-map 来存储已有规则名,遇到重复时 @error 提示****
// ***注意:具体代码就不上了****
终于,终于,终于大功告成了
@include b(human) {
@include e(finger) {
@include m(little) {}
}
@include m(male) {
@include e(leg) {}
}
@include pseudo(hover) {
@include e(hand) {}
}
@include when(hurt) {
@include e(hand) {}
}
@include e(arm) {
@include pseudo(focus) {
@include spec-selector('+') {
@include m(left) {}
}
@include spec-selector('~', hand, right) {}
}
}
}
什么??看过去要写得字数还是很多?????