哈喽大家好,好久没写组件库的文章了。之前搞完初版的组件库后搁置了一段时间,最近有资源投入,趁机会继续完善组件库建设,加个 BEM 规范!接下来有时间的话会完善组件库的单元测试,到时候还会再写一篇文章!
组件库系列文章:
回顾之前,笔者写了三篇关于组件库建设的文章,包括:组件库项目结构、多包管理、开发环境搭建、文档站点搭建、解决多框架融合问题、文档自动引入组件演示案例源码等等。虽然内容也挺多,但是距离一个完善的组件库还是差了点,比如发包流程、版本控制、BEM规范缺失、单元测试缺失…
但是,笔者一直认为:人不能一口吃成一个胖子(其实就是懒),功能也是这样,要一点点完善、新增,所以这次我们先来搞个 BEM 规范吧。let’s go!
源码了解
一、BEM 是什么
网上关于 BEM 的定义、介绍的文章有很多(一搜一大把),笔者不打算在本文进行过多展开,仅围绕核心的点和本文实战相关的点进行讲解说明。有些时候仅仅从概念上去学习、理解新知识可能会比较糙,所以我们结合一些实际项目来进行了解。
也许有覆盖过原生组件库样式的童鞋可能会遇到这样的元素类名:x-xxx
、x-xxx__x
、x-xxx--xxx
。不知道你们第一次遇到的时候会不会跟笔者有一样的想法,这玩意怎么这么长这么多线段…光说无益,我们一起看看一些组件库中 Button
组件的类名:
- element-plus:
el-button
、el-button--primary
- t-design:
t-button
t-button--theme-primary
由上可以发现他们都有些共同点:
- 有一个独立的命名空间,如
el
、t
- 有一个组件名称,如
button
- 有一个组件特性标识,如
primary
于是在这个时候,我们引出 BEM 的命名规范:
- B:块(block)。逻辑、功能独立的组件,就如上述的
button
组件。 - E:元素(element)。块(block)的组成部分。比如有文案
text
元素,其就可作为button
的元素。 - M:修改器(modifier)。顾名思义修改器,就是对
block
、element
中的样式进行修改的。比如button
组件的不同状态:danger、primary、warning 就可以为其分配不同的修改器…
一些连接符规范:
- 命名连接符用
-
连接。如:el-table
、el-table-column
- 块与元素用
__
连接。如:el-table__header
- 块与修改器用
--
连接。如:el-button--primary
、el-button--danger
那么有了以上规范以后,我们就可以通过这种规范来设置组件的样式了。以 el-button
为例来加深一下对 BEM 规范的理解:
/* 普通的 button 样式 */
.el-button {
color: var(--el-button-text-color);
background-color: var(--el-button-bg-color);
}
/* warning 的样式 */
.el-button--warning {
--el-button-text-color: 'warning text color';
--el-button-bg-color: 'warning bg color';
}
很显然,上述例子中,当存在修改器 --warning
的类名时,button
组件的背景色、字体会是 warning
状态的颜色。
其实 BEM 规范没啥难点,就是一个约定俗成的规则,笔者认为在了解 BEM 的时候,更需要了解其使用场景,也就是为什么、在什么时候需要使用 BEM 规范来开发我们的项目,我们接着往下看。
二、为什么需要 BEM
相信应该不少开发童鞋跟笔者一样没怎么写过 BEM 规范的类名。毕竟在现如今的业务开发中,配合着 CssScope
、 CssModules
等 Css模块化方案,已经没有了旧时代写 Css 时容易导致命名冲突等问题的心智负担。简单展开说说:
- CssScope。如写 Vue 项目时,通常会在模版的
style
标签中加入scope
属性:<!-- template中 --> <style scope> .el-button { ... } </style> <!-- 打包完后效果 --> <div class="el-button" data-v-123456></div> <style> /* 多了个属性选择器 */ .el-button[data-v-123456] { ... } </style>
- CssModules。如写 React 项目时会引入一个
style
,然后在类名中使用插值style.xxx
:/* jsx中 */ import style from './style' () => <div className={style.button}></div> /* 打包完后效果 */ <div class="button_123456"></div> <style> .button_123456 { ... } </style>
正因为如此,现代的前端开发写 Css
样式时已经不需要人为的处理 nameSpace
等为了避免命名冲突、样式覆盖等问题,那我们还为什么需要使用 BEM 规范呢?这不是加大自己的开发工作量和参与组件库共建童鞋的心智负担吗?
就从笔者自己的实战经验来看,做组件库的开发采用 BEM 规范极为重要。为什么?因为规范不仅仅是为了规范,更是为了方便组件库用户能轻松地覆盖原组件样式。
举一个实际场景:组件库中有一个 menu
组件,组件库的开发同学由于当前没有一个命名空间规范,害怕引起全局样式的冲突,于是用了 CssScope 写了点样式如下:
由上图可知,其有一个属性选择器 [data-v-73c599ea]
,然后 meun
的背景色是白色的。现有一个安装了该组件库的童鞋,由于系统有其独自的风格,他就想在全局对 menu
进行统一的样式修改以便后续使用,将背景颜色调整为红色。于是他就在 main.js
中引入一个 index.css
,并在其中根据类名改写了 menu
的背景色如下:
.vc-ui-menu-wrapper {
background: red;
}
但是此时页面上的 menu
背景色无动于衷…我们看看实时的「样式表」发现:
红色背景的样式覆盖失败了。这个现象很好理解,根据浏览器对 Css 的权重计算,带属性选择器的权重是(前者):(0, 0, 2, 0)
,没有属性选择器的权重是(后者):(0, 0, 1, 0)
,所以红色背景的样式覆盖失败。那么,为了解决这个问题我们可以增加后者的权重:
-
加
id
选择器 -
加属性选择器:
.vc-ui-menu-wrapper[data-v-73c599ea] { background: red; }
效果如下:
-
使用
!important
.vc-ui-menu-wrapper { background: red !important; }
效果如下:
-
等等…
实现的方案可以有很多,但此时的你有没有发现?我们为了覆盖这个样式要付出很大的代价呢?而且属性选择器的值是通过前端工程化处理的,每次打包出来的结果可能都不一样,所以上述的方案2并不实际。再者,如果当前中台是由一个后端来主导开发的,那对于谈Css变色的他们来说,怕是头都要变大了…
上面长篇大论了一个实际案例,总结出两个点:
- 组件库需要有一个独立的命名空间规范来避免跟业务项目产生类名、样式冲突
- 组件库实现 Css模块化、独立命名空间的同时,又要方便业务项目按照自己的需求对样式进行修改
或许组件库使用 BEM 规范的重要性还有很多方面没提到,但仅按照笔者亲身经历来的这两点来说,足够痛到要让我给组件库加上一个 BEM 规范了。所以,不为了 BEM 而 BEM,加上这个规范是为了解决项目投产中的实际问题的。
三、实战组件库添加 BEM
对于 BEM 的实战应用,方式有很多。笔者参加过两个开源项目的建设,其组件库对 BEM 的实现都不太一样,当然我们也没必要说要把规范做到什么程度,只要能够实现我们功能,解决实际问题即可。
这里笔者简单跟大家分析一下 element-plus
、 tdesign-react
两个组件库对 BEM 规范的实现。
1. elp
对 BEM 的实现:
首先来看 element-plus
的,我们从用法看起:
<template>
<button :class="[ ns.b(), ns.m(_type), ns.m(_size), ns.is('disabled', _disabled), ns.is('loading', loading), ... ]" />
</template>
<script> // 使用了一个 hook,传入组件名称 const ns = useNamespace('button') </script>
其实从用法上来看,已经不难猜出 useNamespace
返回的 ns
是什么了。这里盲猜一下,ns.b()
就代表了 el-button
;ns.m(_type)
这里是 .m
(对应 modify),也就是说,如果 _type
是 primary
那就是 el-button--primary
…
那接下来一起看看 useNamespace
的源码实现(精简过):
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
}
export const useNamespace = (block: string) => {
const namespace = 'el'
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, '')
: ''
...
return {
namespace,
b,
e,
m,
be,
...
}
}
虽然精简过的代码还是很长,但是大家可以不用看,笔者简单说说。_bem函数
就是对各种命名规范的一个封装,比如 block 之间的名称用 -
分割;block 和 elmenet 的连接用 __
;连接 modify 的用 --
。useNamespace
中根据不同的组合情况控制参数调用 _bem
,提供如 be
、 bem
、 bm
等组合给用户。
说白了就是做了一层封装,主要按照封装规范使用即可,比如需要加一个 button
的 red
作为 modify,我们就调用 ns.bm('red')
即可。
2. tdesign-react
对 BEM 的实现
这里看看 tdesign
的实现。其实这个库的实现就很简单了,直接上一张图:
没错,非常的直接,可以说除了 prefix
之外都是直接根据规范写类名的。这样开发者根本不需要去了解任何封装,直接上手开发,可以说心智负担是很低的了。毕竟任何的封装,即使再简单,非开发者使用时都需要去了解一下封装后的用法。
由于笔者都参加过这两个项目,对于这两种 BEM 的实现方式算是有一丢丢的心得。总的来说:
-
element-plus
的:-
优点:无需关注各名称之间的符号连接。不管是
bm
、em
,反正调用封装好的函数传入字符串就行了。 -
优点:连接符强规范。不用担心由于开发者的粗心写错连接符。比如
bm
之间写少了一个-
,由el-button--primary
变成el-button-primary
。 -
缺点:需要先了解
useNamespace
封装规范,调用规范的方法传参使用有一定的学习成本。 -
缺点:类名搜索难度提高。当开发者想要改某块样式时,想通过直接搜索类名比较难命中,需要掌握一定的搜索规律(如通过
bm('xxx')
去搜索),特别是一些文件分布较多的组件,如 table:
-
-
tdesign
的:当然,对于
tdesign
来说就恰恰相反的,上述element-plus
的优点在这里可能就是缺点,而上述的缺点就是这里的优点。- 比如没有强规范的封装,可能会写错连接符,或者没按 BEM 规范来命名;
- 另一方面,由于其的直观性,开发者无需任何学习成本即可直接写类名、并且可以通过对类名直接搜索命中我们想要找到的文件~
笔者了解过这两种方式加入 BEM 规范后,根据当前自己的“小”组件库规模,决定采用直观、简单的方式应用(说实话连 prefix
都想直接写死),也就是后者。具体实现就很简单了,几行代码搞定。
首先在组件库目录中新建 hooks
目录:
在 usePrefix
中写几句简单的代码即可:
const DEFAULT_PREFIX = 'mm'
export function usePrefix () {
return {
classPrefix: DEFAULT_PREFIX
}
}
紧接着我们对 element-plus
的自定列 table
进行 BEM 规范的改造: 首先拿到 classPrefix
:
import { usePrefix } from '@much-more/hooks';
const { classPrefix } = usePrefix()
再在 template 使用 BEM 规范改造类名:
css
中编写样式(使用css预处理器写法很方便):
.mm-table {
&__options {
height: 32px;
display: flex;
align-items: center;
justify-content: flex-end;
&__item {
cursor: pointer;
margin-right: 8px;
}
}
}
最后在浏览器中看看 BEM 应用后的类名效果:
当然,如果是大型的项目,对于 class命名、css写法 都可以适当的抽象封装,使用 less
、 scss
都可以像 class 那样,定义一些全局的变量,如 prefix
结合使用。本文就不再展开了,如果后续有相关的优化实现,也会同步写成一篇文章来分享。
写在最后
其实从个人的期望度来讲,更希望后续组织上有时间投入到组件库就把单元测试加上,毕竟对于组件库这种基建工程来说,单元测试还是非常有必要的。一个大型的基建项目,需要保证其迭代、bug fix
或者重构后功能依旧稳定,单元测试可以说是一把利器。特别是一些复杂bug的修复、大型重构,如果每个用例都能跑过那基本上出现问题的概率会大大降低。好吧,本文就先到这里吧,如果有下一篇,那一定是:实战组件库加入单元测试!(标题我都想好了,就差实战撸码了😝)
今天的文章组件库实战—— BEM 命名规范分享到此就结束了,感谢您的阅读。
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
如需转载请保留出处:https://bianchenghao.cn/23434.html