Sass在Element Plus组件库中的BEM架构详解
平时我们写JS代码时会有一些架构的设计,css也不例外,它也有自己的自己架构设计,尤其是在组件库中。今天带来的是BEM架构在Element Plus组件库中的设计详解,包括代码的实现,以及最重要的——这样设计的目的是什么。
前置知识
BEM
BEM 是由 Yandex 团队提出的一种 CSS 命名方法论,是OOCSS模式的一种实现,BEM实际上是block、element、modifier的缩写,分别为块层、元素层、修饰符层,命名格式为:
block-name__<element-name>--<modifier-name>-<modifier_value>
block与element使用__相接- modifier与前面修饰的结构用
--相接(可以与block或element相接) - 修饰符的值用
-与修饰符层相连
例如button组件
<button class="button">
<div class="button__inner button__inner--primary">
button
</div>
</button>
在组件库中,每一个组件被分为一个个的block,组件中的每一块被分成了一个个的element,对block或element的修饰被分成一个个的modifier,这样不但有效解决了css类名的命名冲突,也方便了我们维护。
调试
我们写的是scss文件,调试scss文件可使用sassnpm包来完成,先全局安装
npm install -g sass
先在项目中使用@debug打印在控制台或使用@error抛出错误。
然后对scss文件所在目录运行如下命令,可以使用-w开启监听模式,若scss文件或所依赖的文件发生变化,将自动重新编译
sass .\icon.scss icon.css -w
在你的scss文件没有错误的情况下,以上命令将icon.scss编译为icon.css文件。
组件库中的BEM模式有哪些写法
BEM的scss一共有以下几种形态
- 常规写法
b--m嵌套b__e- 伪类或表示状态的
.is-选择器中嵌套b__e - 特殊选择符
+、~等的衔接 - 共享和继承
- 多个
element或多个modifier共享
组件库中常见的写法有这些,一个组件这样写还好,如果是所有的组件的样式都要加上__、--等符号,写起来将会特别麻烦,接下来我们就看看怎么来解决这个问题。
常规写法
BEM架构最平常的写法就是这样,我们拿b、e、m分别为icon、item、color举例:
.icon {
&__item {
&--color {}
}
}
block后面接element,element后面接modifier,现在我们要简化的就是拼接__e和--m的过程,所以我们使用定义混合指令``@mixin来表示b、e、m,在这之前我们先定义一些常量
$common-separator: '-' !default; // 公共的连接符
$element-separator: '__' !default; // 元素以__分割
$modifier-separator: '--' !default; // 修饰符以--分割
$state-prefix: 'is-' !default; // 状态以is-开头
然后定义三个@mixin
@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(icon) {
--color: inherit;
height: 1em;
svg {
height: 1em;
width: 1em;
}
@include e(item) {
width: 0.5rem;
@include m(color) {
color: aqua;
}
}
}
编译后的css如下
.icon {
--color: inherit;
height: 1em;
}
.icon svg {
height: 1em;
width: 1em;
}
.icon__item {
width: 0.5rem;
}
.icon__item--color {
color: aqua;
}
可以看到编译后的css并没有生成后代选择器,也就是类似.icon .icon__item,可以知道生成的scss文件中类名选择器并没有嵌套,而是拼接后被@at-root提到最外面,可以推知拼接类名后的scss的形式为:
.icon {
--color: inherit;
svg {
height: 1em;
width: 1em;
}
@at-root {
.icon--item {
color: aqua;
@at-root {
.icon__item--color {
width: 0.5rem;
}
}
}
}
}
有同学疑惑到底是为啥需要@at-root,我们让只有block和element的例子试一下如果没有@at-root会是怎样:
// 转换前
@include b(icon) {
--color: inherit;
svg {
height: 1em;
width: 1em;
}
@include e(item) {
color: aqua;
}
}
// 转换后的scss
.icon {
--color: inherit;
svg {
height: 1em;
width: 1em;
}
.icon--item {
color: aqua;
}
}
// 编译后的css
.icon {
--color: inherit;
}
.icon svg {
height: 1em;
width: 1em;
}
.icon .icon--item {
color: aqua;
}
加上modifier试一下
// 转换后的css
.icon {
--color: inherit;
}
.icon svg {
height: 1em;
width: 1em;
}
.icon .icon__item {
color: aqua;
}
.icon .icon__item .icon .icon__item--color {
width: 0.5rem;
}
// 可以推知转换前的scss的形态
.icon {
--color: inherit;
svg {
height: 1em;
width: 1em;
}
.icon--item {
color: aqua;
.icon__item--color {
width: 0.5rem;
}
}
}
可以看到如果没有@at-root,将会无限嵌套。
所以得出@at-root的作用:在编译时不进行嵌套形成(类似.icon .icon__item这种选择器)
b--m嵌套b__e
按b、e、m的顺序生成已经可以,但是我们可能要生成.icon--color内嵌.icon__item,也就是b--m内嵌b__e,我们先试一下如果直接写:
@include b(icon) {
--color: inherit;
@include m(color) {
color: aqua;
@include e(item) {
width: 0.5rem;
}
}
}
// 编译后的css
.icon {
--color: inherit;
}
.icon--color {
color: aqua;
}
.icon--color__item {
width: 0.5rem;
}
// 实际上期望的
.icon--color {
color: aqua;
}
.icon--color .icon__item {
width: 0.5rem;
}
所以当@mixin e()的父选择器的类名是modifier的时候不进行类名拼接,而是嵌套。
创建一个function.scss文件来放置函数,因为选择器是一个数组(scss的数组形式是(,,,)),而判断类名用到的字符串方法的操作对象是字符串,所以需要先将数组转为字符串,我们抽成一个函数:
@function selectorToString($selector) {
$selector: inspect($selector); // Debug: (.icon--color,)
$selector: str-slice($selector, 2, -2); // 把前后的括号删掉
@return $selector;
}
关于Sass字符串的方法可以参考Sass String Functions和Sass inspect() Function
再判断modifier
// 判断是否有Modifier
@function containsModifier($selector) {
$selector: selectorToString($selector);
// str-index()返回 $modifier-separator在$selector的位置
@if str-index($selector, $modifier-separator) {
@return true;
} @else {
@return false;
}
}
我们还需要或许被嵌套的b__e中block的名称
block的名称可以通过传入的字符串$selector切割获得,也可以利用全局变量,这里贴上字符串切割代码
@import "function";
$modifier-string: selectorToString($selector);
$ms-index: str-index($modifier-string, $modifier-separator);
$end-index: $ms-index + 1;
$block: str-slice($modifier-string, 2, $ms-index - 1);
不过我们还是通过全局变量来实现更为简洁,在@mixin b()和@mixin e()都加上全局变量,element的名称后面也可能用上
根据function.scss的函数改写@mixin e()
在@mixin e()里面判断上层是否是b--m,如果是,手动进行嵌套(因为@at-root的存在)且改变类名
@mixin b($block) {
$B: $block !global;
.#{$B} {
@content;
}
}
@mixin e($element) {
$selector: &;
$E: $element !global;
@if containsModifier($selector) {
// 如果这是一个b--m,也就是先走了@mixin m(),里面就变成b--m嵌套b__e
@at-root {
#{$selector} { // b--m
.#{$B + $element-separator + $E} { // b__e
@content;
}
}
}
} @else {
@at-root {
// @at-root 指规则跳出嵌套,写在最外层
#{$B + $element-separator + $E} {
@content;
}
}
}
}
伪类或状态state中嵌套b__e
我们约定使用.is-xxx类名选择器来表示dom节点的状态,而且将伪元素和state也用@mixin表示
@mixin when($state) {
$selector: &;
@at-root {
// 这里不是拼接,而是使用兄弟
#{$selector + "." + $state-prefix + $state} {
@content;
}
}
}
// 伪类
@mixin pseudo($pseudo) {
$selector: &;
@at-root {
#{$selector + ":" + $pseudo} {
@content;
}
}
}
当在@mixin e中没有处理外层的伪类和state时,依旧是直接将__e直接拼上去
@include b(icon) {
@include pseudo(hover) {
@include e(side) {
color: blueviolet;
}
}
@include when(click) {
@include e(side) {
color: darkcyan;
}
}
}
// 编译后的css
.icon:hover__side {
color: blueviolet;
}
.icon.is-click__side {
color: darkcyan;
}
所以这里的处理方法和前面b--e嵌套b__e一样,即判断外层是否有表示状态的.is-或伪类,我们将这几个判断合并在一起写在function.scss中
// 判断是否有表示状态的 .is-
@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;
}
}
// 判断是否含有 Modifier、表示状态的 .is- 和 伪类
@function hitAllSpecialNestRule($selector) {
@return containsModifier($selector) or containWhenFlag($selector) or containPseudoClass($selector);
}
然后将@mixin e的containsModifier换成hitAllSpecialNestRule即可
再编译一下上面的scss
// 编译后的css
.icon:hover .icon__side {
color: blueviolet;
}
.icon.is-click .icon__side {
color: darkcyan;
}
特殊选择符拼接
在使用>、+等符号时,右边的选择器就得自己写了,比如:
@include b(icon) {
&:focus + .icon__item {
position: inherit;
}
}
为了方便,写一个@mixin spec-selector来进行特殊选择符的拼接
@mixin spec-selector($specSelector: "", $element: $E, $modifier: false, $block: $B) {
$modifierCombo: "";
// 判断输出的是 b__e 还是 b__e--m
@if $modifier {
// 如果是b__e--m,就将m接上
$modifierCombo: $modifier-separator + $modifier;
}
@at-root {
// 默认是父级用特殊符号接上目前的b__e(--m),bem都是可选的,
#{&}#{$specSelector}.#{$block + $element-separator + $element + $modifierCombo} {
@content;
}
}
}
@mixin spec-selector支持自定义右边选择器,默认是当前的b__e,这是在大部分应用场景下的情况,modifier默认为false,也就是不拼接上modifier
共享
首先,当有多个元素@extends一个占位符或者选择器时,会被编译为下面这样
Sass 额外提供了一种特殊类型的选择器:占位符选择器 (placeholder selector)。与常用的 id 与 class 选择器写法相似,只是 # 或 . 替换成了 %。必须通过 @extend 指令调用,更多介绍请查阅 @extend-Only Selectors。
%share {
position: absolute;
left: 50%;
top: 50%;
}
.test1 {
@extend %share;
color: antiquewhite;
}
.test2 {
@extend %share;
}
.test3 {
@extend %share;
}
// 编译后的css
.test3, .test2, .test1 {
position: absolute;
left: 50%;
top: 50%;
}
.test1 {
color: antiquewhite;
}
当%share在其他作用域时,会被编译成下面这样
.box {
%share-test {
position: absolute;
left: 50%;
top: 50%;
}
}
.test1 {
@extend %share-test;
color: antiquewhite;
}
.test2 {
@extend %share-test;
}
.test3 {
@extend %share-test;
}
// 编译后的css
.box .test3, .box .test2, .box .test1 {
position: absolute;
left: 50%;
top: 50%;
}
.test1 {
color: antiquewhite;
}
所以,如果当%share在@include b()里面时,他的作用域时在.icon里面,编译出来的css选择器前面就会多出一个.icon,这个是不符合预期的,因为我们的element写在b里面只是为了拼接,而不是嵌套
@include b(item) {
%share {
position: absolute;
left: 50%;
top: 50%;
}
@include e(item) {
@extend %share;
}
}
// 会被编译成下面的css
.icon .icon__item {
position: absolute;
left: 50%;
top: 50%;
}
// 也就是说scss是这样的
.icon {
.icon__item {
position: absolute;
left: 50%;
top: 50%;
}
}
解决方法,就是利用@at-root把%share提到@include b(item)外面去,这样%share就不在.icon里面了
@mixin share-rule($name) {
$rule-name: "%shared-" + $name;
@at-root #{$rule-name} {
@content;
}
}
@mixin extend-rule($name) {
@extend #{"%shared-" + $name};
}
重新编译
@include b(item) {
@include share-rule(position) {
position: absolute;
left: 50%;
top: 50%;
}
@include e(item) {
@include extend-rule(position);
}
}
// 编译后
.icon__item {
position: absolute;
left: 50%;
top: 50%;
}
多个element或多个modifier共享
如果有多个element(modifier同理)使用同一套样式,在没有创建这些element之前(或者说这些element需要其他样式),我们需要这样写:
@include b(item) {
@include share-rule(color-shared) {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
@include e(white) {
@include extend-rule(color-shared);
}
@include e(black) {
@include extend-rule(color-shared);
}
@include e(origin) {
@include extend-rule(color-shared);
}
@include e(blue) {
@include extend-rule(color-shared);
}
// ...
}
要创建很多次element,每次都要去extend,特别麻烦;我们试试在创建element的时候就生成一套公用的样式:
@include b(icon) {
// 传入不确定个参数
@include e(white, black, origin, blue) {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
}
让我们改写@mixin e(),里用参数数组传入多个参数
这里的
$element...类似JS的拓展运算符,$element为数组
@mixin e($element...) {
$selector: &;
$E: $element !global;
$currentSelector: "";
// 用@each in遍历数组,在每一个选择器后面加逗号
@each $unit in $element {
$currentSelector: #{$currentSelector + "." + $B + $element-separator + $unit + ","};
}
@if hitAllSpecialNestRule($selector) {
@at-root {
#{$currentSelector} {
@content;
}
}
}
} @else {
@at-root {
#{$currentSelector} {
@content;
}
}
}
}
编译后的css如下
.icon__white, .icon__black, .icon__origin, .icon__blue {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
}
@mixin modifier()也是同样道理
@mixin m($modifier...) {
$selector: &;
$currentSelector: "";
@each $unit in $modifier {
// 这里$selector带了. ,不用加 .
$currentSelector: #{$currentSelector + $selector + $modifier-separator + $unit + ","};
}
@debug $currentSelector;
@at-root {
#{$currentSelector} {
@content;
}
}
}
测试一下
@include b(icon) {
// 传入不确定个参数
@include e(item) {
@include m(primary, err) {
color: red;
}
}
}
// 编译后
.icon__item--primary, .icon__item--err {
color: red;
}
总结
可以大概总结一下这个过程:遇到bem、伪类、状态is-、特殊选择符就拼接,在element中遇到父选择器是modifier、状态is-、伪类就嵌套,通过混合指令,在组件库中我们能更好地使用BEM架构去构建样式。
相关代码链接:github.com/plutoLam/pl…
希望这篇文章对屏幕前的你有帮助,原创不易,欢迎点赞、收藏、转发、关注~~!
转载自:https://juejin.cn/post/7177185484177670205