前言
平时我们在写项目的时候似乎都不太重视“命名”这个似乎不在技术范畴之内的东西🤔,随着项目更新迭代变得庞大,维护起来欲哭无泪。尤其是在CSS中,一个高效的命名规范到底有多重要?
BEM命名的核心就是可以清晰的描述页面的结构,从其名字就可以知道某个标记的含义,👀通过查看class属性就可以知道元素之间的关联。
BEM是什么
BEM 是由 Yandex 团队提出的一种 CSS 命名方法论,即 Block(块)、Element(元素)、和 Modifier(修改器)的简称
在选择器中,由以下三种符号来表示扩展的关系:
-(中划线):仅作为连接字符使用,表示某个块或者子元素的多个单词之间的链接符号。
__(双下划线):用来链接块与块的子元素
--(双中线):用来链接块元素与修饰符
举个栗子🌰
人 #Block
人__手 #Element
人__手--小手 #Modifier
人__手--大手 #Modifier
人__脚 #Element
人--男人 #Modifier
人--男人__手 #Element
人--男人__脚 #Element
人--女人 #Modifier
人--女人__手 #Element
人--女人__脚 #Element
放在代码中
.aside {}
.aside__toggle--show {}
.aside__toggle--hide {}
.aside__menu {}
.aside__menu__item {}
当然这些规则并不一定需要严格遵守的,也可以根据你的团队风格进行修改。
通过 JS 生成 BEM 规范名称
从上文看来,BEM好像确实让代码结构更加清晰有意义,使维护变得简单
但是需要经常写 - 、 __ 、 --,那么就会变得非常繁琐😤
那我们可不可以通过 JavaScript 按照 BEM 命名规范进行动态生成呢🧐
答案当然是可以的,我们以element Plus为例
BEM 命名字符拼接函数 :
// BEM 命名字符拼接函数
const _bem = (
namespace: string,
block: string,
blockSuffix: string,
element: string,
modifier: string
) => {
// 默认是 Block
let cls = `${namespace}-${block}`
// 如果存在 Block 后缀,也就是 Block 里面还有 Block,例如:el-form 下面还有一个 el-form-item
if (blockSuffix) {
cls += `-${blockSuffix}`
}
// 如果存在元素
if (element) {
cls += `__${element}`
}
// 如果存在修改器
if (modifier) {
cls += `--${modifier}`
}
return cls
}
通过 BEM 命名字符拼接函数,我们就可以自由组合生成各种符合 BEM 规则的 classname 了。
export const useNamespace = (block: string) => {
// 命名前缀也就是命名空间
const namespace = computed(() => defaultNamespace)
// 创建块 例如:el-form
const b = (blockSuffix = '') =>
_bem(unref(namespace), block, blockSuffix, '', '')
// 创建元素 例如:el-input__inner
const e = (element?: string) =>
element ? _bem(unref(namespace), block, '', element, '') : ''
// 创建块修改器 例如:el-form--default
const m = (modifier?: string) =>
modifier ? _bem(unref(namespace), block, '', '', modifier) : ''
// 创建后缀块元素 例如:el-form-item
const be = (blockSuffix?: string, element?: string) =>
blockSuffix && element
? _bem(unref(namespace), block, blockSuffix, element, '')
: ''
// 创建元素修改器 例如:el-scrollbar__wrap--hidden-default
const em = (element?: string, modifier?: string) =>
element && modifier
? _bem(unref(namespace), block, '', element, modifier)
: ''
// 创建块后缀修改器 例如:el-form-item--default
const bm = (blockSuffix?: string, modifier?: string) =>
blockSuffix && modifier
? _bem(unref(namespace), block, blockSuffix, '', modifier)
: ''
// 创建块元素修改器 例如:el-form-item__content--xxx
const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
blockSuffix && element && modifier
? _bem(unref(namespace), block, blockSuffix, element, modifier)
: ''
// 创建动作状态 例如:is-success is-required
const is: {
(name: string, state: boolean | undefined): string
(name: string): string
} = (name: string, ...args: [boolean | undefined] | []) => {
const state = args.length >= 1 ? args[0]! : true
return name && state ? `${statePrefix}${name}` : ''
}
return {
namespace,
b,
e,
m,
be,
em,
bm,
bem,
is,
}
}
最后我们就可以在组件中引入 BEM 命名空间函数进行创建各种符合 BEM 命名规范的 classname 了,例如:
- 创建块 el-form、
- 创建元素 el-input__inner、
- 创建块修改器 el-form--default、
- 创建块后缀元素 el-form-item、
- 创建元素修改器 el-scrollbar__wrap--hidden-default、
- 创建动作状态 例如:is-success is-required
具体创建代码使用代码如下:
import {
useNamespace,
} from '@element-plus/hooks'
// 创建 classname 命名空间实例
const ns = useNamespace('button')
然后就可以在 template 中进行使用了:
<template>
<button ref="_ref" :class="[ns.b()]">按钮</button>
<template>
通过 SCSS 生成 BEM 规范样式
既然可以通过 JavaScript 按照 BEM 命名规范进行动态生成类名
那可以通过 SCSS 按照 BEM 命名规范进行动态生成样式吗🧐
Element Plus 的样式采用 SCSS 编写的,那么就可以通过 SCSS 的 @mixin 指令定义 BEM 规范样式。在 mixins 目录下新建三个文件:config.scss、function.scss、mixins.scss。 其中 config.scss 文件编写 BEM 的基础配置比如样式名前缀、元素、修饰符、状态前缀:
$namespace: 'el' !default; // 所有的组件以el开头,如 el-input
$common-separator: '-' !default; // 公共的连接符
$element-separator: '__' !default; // 元素以__分割,如 el-input__inner
$modifier-separator: '--' !default; // 修饰符以--分割,如 el-input--mini
$state-prefix: 'is-' !default; // 状态以is-开头,如 is-disabled
在 SCSS 中,我们使用 $+ 变量名:变量 来定义一个变量。在变量后加入 !default 表示默认值。给一个未通过 !default 声明赋值的变量赋值,此时,如果变量已经被赋值,不会再被重新赋值;但是如果变量还没有被赋值,则会被赋予新的值。
mixins.scss 文件编写 SCSS 的 @mixin 指令定义的 BEM 代码规范。
定义 Block:
@mixin b($block) {
$B: $namespace + '-' + $block !global;
.#{$B} {
@content;
}
}
$B 表示定义一个一个变量,$namespace 是来自 config.scss 文件中定义的变量, !global 表示其是一个全局变量,这样就可以在整个文件的任意地方使用。#{} 字符串插值,类似模板语法。通过 @content 可以将 include{} 中传递过来的内容导入到指定位置。
定义 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;
}
}
}
}
首先定义一个全局变量 $E,接着定义父选择器 $selector,再定义当前的选择器 $currentSelector,再通过循环得到当前的选择器。接着通过函数 hitAllSpecialNestRule(hitAllSpecialNestRule 函数在 mixins 目录的 function.scss 文件中) 判断父选择器是否含有 Modifier、表示状态的 .is- 和 伪类,如果有则表示需要嵌套。@at-root 的作用就是将处于其内部的代码提升至文档的根部,即不对其内部代码使用嵌套。
定义修改器:
@mixin m($modifier) {
$selector: &;
$currentSelector: '';
@each $unit in $modifier {
$currentSelector: #{$currentSelector +
$selector +
$modifier-separator +
$unit +
','};
}
@at-root {
#{$currentSelector} {
@content;
}
}
}
这个非常好理解,就是定义了父选择器变量 $selector 和 当前选择器变量 $currentSelector,并且当前选择器变量初始值为空,再通过循环传递进来的参数 $modifier,获得当前选择器变量 $currentSelector 的值,再定义样式内容,而样式内容是通过 @content 将 include{} 中传递过来的内容。
定义动作状态:
@mixin when($state) {
@at-root {
&.#{$state-prefix + $state} {
@content;
}
}
}
选择器就是 config.scss 文件中的变量 $state-prefix 加传进来的状态变量,而样式内容是通过 @content 将 include{} 中传递过来的内容。
接着我们再看下上面定义 Element 的时候说到的 hitAllSpecialNestRule 函数,这个函数是定义在 mixins 目录下的 function.scss 文件中。function.scss 文件内容如下:
@use 'config';
// 该函数将选择器转化为字符串,并截取指定位置的字符
@function selectorToString($selector) {
$selector: inspect(
$selector
); // inspect(...) 表达式中的内容如果是正常会返回对应的内容,如果发生错误则会弹出一个错误提示。
$selector: str-slice($selector, 2, -2); // str-slice 截取指定字符
@return $selector;
}
// 判断父级选择器是否包含'--'
@function containsModifier($selector) {
$selector: selectorToString($selector);
@if str-index($selector, config.$modifier-separator) {
// str-index 返回字符串的第一个索引
@return true;
} @else {
@return false;
}
}
// 判断父级选择器是否包含'.is-'
@function containWhenFlag($selector) {
$selector: selectorToString($selector);
@if str-index($selector, '.' + config.$state-prefix) {
@return true;
} @else {
@return false;
}
}
// 判断父级是否包含 ':' (用于判断伪类和伪元素)
@function containPseudoClass($selector) {
$selector: selectorToString($selector);
@if str-index($selector, ':') {
@return true;
} @else {
@return false;
}
}
// 判断父级选择器,是否包含`--` `.is-` `:`这三种字符
@function hitAllSpecialNestRule($selector) {
@return containsModifier($selector) or containWhenFlag($selector) or
containPseudoClass($selector);
}
通过上述代码我们就可以知道 hitAllSpecialNestRule 函数是如何判断父选择器是否含有 Modifier、表示状态的 .is- 和 伪类的了。