element-plus 架构 - BEM和命名空间的实现

1,什么是BEM

对于一个前端开发团队来说,随着项目的规模越来越大,良好的代码规范越显得重要。

在 CSS 命名方面,经常会遇到的问题:

  • 绞尽脑汁的去想一个 class,还得和其他类似的 class 做区分。
  • 修改旧代码时,得去仔细确认每个 class 的作用,是修改、删除,还是添加一个新的 class 覆盖。
  • 协作开发时 class 命名的冲突。

BEM 可以解决这些问题,让代码更容易阅读和控制,规范团队的代码风格

1.1,介绍

BEM 是一种CSS命名规范。由 B(block)模块,E(element)元素,M(modifier)修饰符组成。

书写规范:

  • 连接 element 用 __双下划线
  • 连接 modifier 用 --双中划线
  • 1个 class 中,B、E、M 尽量都只有1个(比如:尽量避免block__el1__el2

具体表现如下:

.block {}
.block__element {}
.block__element--modifier {}
.block--modifier {}
  1. 在前端项目中,一般是由多个组件构成的,组件就是一个模块 block。
  2. element 表示 block 内更细粒度的元素,一般以布局或功能区分这些元素。
  3. modifier 用于标记 block 或 element 的不同版本或状态

1.2,使用

举例:弹出框组件 dialog

<div class="dialog center">
  <div class="header">
    <div class="title"></div>
    <div class="closebtn"></div>
  </div>
  <div class="body"></div>
  <div class="footer"></div>
</div>

这样的命名方式,在阅读时并不能确定:

  1. center 是一个通用的 class,还是只对 dialog 生效。
  2. body 等是否也只在 dialog 中用到。如果body 中又嵌套了一个组件,也有类似body 的结构,命名就有点麻烦了。

BEM改造:

<div class="dialog dialog--center">
  <div class="dialog__header">
    <div class="dialog__title"></div>
    <div class="dialog__closebtn"></div>
  </div>
  <div class="dialog__body"></div>
  <div class="dialog__footer"></div>
</div>

1.3,问题

问题1:注意到定义的是dialog__title 而不是dialog__header__title

  1. 满足 BEM 书写规范。
  2. 在BEM规范中,不关心 DOM 元素的层级结构,关注的是在BEM三者之间的关系:block 下有哪些 element,block 和 element 下又有哪些 modifier。

所以对于同一个 blockdialog),headertitle 都是它的 element

问题2:如果元素有很多的话,BEM规范还适用吗?

适用。因为 block 可以嵌套,就像组件之间的嵌套。

<div class="dialog dialog--center">
  <div class="dialog__body">
    <form class="form form--default">
      <div class="form-item">
        <div class="form-item__label"></div>
        <div class="form-item__content"></div>
	  </div>
	</form>
  </div>
</div>

问题3: 注意到上面定义的是form-item,而不是form__item

和问题1的区别是:因为后面还有form-item__label,如果命名为form__item,那 label 就变成form__item__label,不符合BEM命名规范。

这种情况,item 被称为blockSuffix,本质上form-item整体是一个block

1.4,总结

可以看到通过BEM规范化后,代码更易阅读和控制:

  1. 增加了代码的自解释性,能够直观的看到层级和依赖关系。
  2. class 单一职责。
  3. 避免了命名污染(污染外层或公共的同名 class)。

2,命名空间

命名空间(namespace)也是一种CSS命名规范,一般会配合 BEM 使用。

作用:进一步增加代码的自解释性,能够更快定位。

分类:命名空间并没有明确的分类,常用的有3种

2.1,组件类

c- 表示(Component)

.c-table {}
.c-form {}

2.2,状态类

is-has- 表示

.has-footer {}
.is-disabled {}

在介绍BEM时,modifier 用于标记 block 或 element 的不同版本或状态
当 BEM 结合状态类之后,M 就可以专注的表示版本,状态由 is- 来表示

2.3,作用域类

s- 或自定义的字母来表示。

作用:和整个站点的样式做隔离。最常见的使用就是组件库,每个组件库都有自己的命名空间。

组件库命名空间
element-plusel-
vantvan-
ant designant-

3,element-plus的BEM

源代码路径:packages\hooks\use-namespace\index.ts

3.1,实现

1. 定义一个函数,用于组装符合BEM规范的 class

const _bem = (namespace: string, block: string, blockSuffix: string, element: string, modifier: string) => {
  let cls = `${namespace}-${block}`
  if (blockSuffix) {
    cls += `-${blockSuffix}`
  }
  if (element) {
    cls += `__${element}`
  }
  if (modifier) {
    cls += `--${modifier}`
  }
  return cls
}

2. 定义获取全局配置中命名空间的函数

element-plus 的命名空间是可以自定义配置的,默认是el参考:element-plus的全局配置解析

const namespaceContextKey = Symbol('namespaceContextKey')
// 默认返回 el
const useGetDerivedNamespace = (namespaceOverrides?: Ref<string | undefined>) => {
  const derivedNamespace = namespaceOverrides || inject(namespaceContextKey, ref('el'))
  const namespace = computed(() => {
    return unref(derivedNamespace)
  })
  return namespace
}

3. 定义使用命名空间+BEM的 hooks: useNamespace

代码看起来有点多,其实并不复杂。主要实现了 BEM 三者的组合,并将定义的组合方法返回。

  1. 当调用 useNamespace(block) ,此时已经确定block
  2. 函数体内的 b 方法,指的是添加blockSuffix
  3. 函数体内的 is 方法原本使用了函数重载,这里做了简化。
  4. args[0]! 后面的!是非空断言,断言args[0]不会是 nullundefined。从而取消编译器对该表达式可能为 null 或 undefined 的类型检查。
const useNamespace = (block: string, namespaceOverrides?: Ref<string | undefined>) => {
  // namespace 默认 el
  const namespace = useGetDerivedNamespace(namespaceOverrides)
  const b = (blockSuffix = '') => _bem(namespace.value, block, blockSuffix, '', '')
  const e = (element?: string) => (element ? _bem(namespace.value, block, '', element, '') : '')
  const m = (modifier?: string) => (modifier ? _bem(namespace.value, block, '', '', modifier) : '')
  const be = (blockSuffix?: string, element?: string) =>
    blockSuffix && element ? _bem(namespace.value, block, blockSuffix, element, '') : ''
  const em = (element?: string, modifier?: string) =>
    element && modifier ? _bem(namespace.value, block, '', element, modifier) : ''
  const bm = (blockSuffix?: string, modifier?: string) =>
    blockSuffix && modifier ? _bem(namespace.value, block, blockSuffix, '', modifier) : ''
  const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
    blockSuffix && element && modifier ? _bem(namespace.value, block, blockSuffix, element, modifier) : ''

  const is = (name: string, ...args: [boolean | undefined] | []) => {
    // args[0]! 断言args[0]不会是 null 或 undefined。从而取消编译器对该表达式可能为 null 或 undefined 的类型检查。
    const state = args.length >= 1 ? args[0]! : true
    return name && state ? `is-${name}` : '' // is-disabled
  }

  return {
    namespace,
    b,
    e,
    m,
    be,
    em,
    bm,
    bem,
    is,
  }
}

3.2,使用

const ns = useNamespace('button')
ns.b() // el-button
ns.b('group') // el-button-group
ns.m('primary') // el-button--primary
ns.em('text', 'expand') // el-button__text--expand
ns.is('disabled', true) // is-disabled

const ns = useNamespace('dialog')
ns.e('title') // el-dialog__title

const ns = useNamespace('checkbox')
ns.be('button', 'original') // el-checkbox-button__original
ns.bm('button', 'small') // el-checkbox-button--small

const ns = useNamespace('progress')
ns.bem('bar', 'inner', 'indeterminate') // el-progress-bar__inner--indeterminate

以上。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

下雪天的夏风

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值