前言
之前分析过了 element-ui
的项目(自建vue组件 air-ui (2) -- 先分析一下 element ui 项目),包括目录结构,构建以及项目开发的整体思路,今天打算还是以 element-ui
这个项目的源码为主,我们来聊一聊开发一个ui组件的时候,我们应该怎么设计 css 的开发规范。 这一点我觉得 element-ui
它们封装的非常好,该抽象的有抽象,该封装的有封装。所以后续我开发 air-ui
直接把这一套挪过去用了。
什么是 BEM
关于 BEM
的更详细的介绍,可以看它的官网: BEM官网
BEM
是 Block
(块) Element
(元素) Modifier
(修饰器)的简称, 它可以帮助你创建出可以复用的前端组件和前端代码, 他有以下三个特点:
重用性
不同方式组织独立的块,并智能地重用它们,可以减少必须维护的CSS代码量。通过一系列风格指南,您可以构建一个块库,使您的CSS超级有效。单元性
块的样式从来不依赖同页面其它的元素,所以你永远不会遇到级联问题。您还可以将完成的项目中的块转移到新项目中。结构化
BEM方法可以使得你的CSS代码结构性很好,从而更加容易理解。
使用BEM规范来命名CSS,组织HTML中选择器的结构,利于CSS代码的维护,使得代码结构更清晰。 当然也有弊端,比如名字会稍长, 而且因为大部分都是只有一层结构,还要注意样式覆盖问题
如何使用 BEM
- 一个独立的(语义上或视觉上),可以复用而不依赖其它组件的部分,可作为一个块(Block)
- 属于块的某部分,可作为一个元素(Element)
- 用于修饰块或元素,体现出外形行为状态等特征的,可作为一个修饰器(Modifier)
element ui 的使用
接下来我要讲的就是 element-ui
如何利用sass,编写具有可读性和可维护性的BEM规则的css代码。
命名空间定义
在 package/theme-chalk/src/mixins/config.scss
这个文件中,有一个针对 BEM 的规范定义:
1 | $namespace: 'el'; |
- 双下划线
__
来作为块和元素的间隔, 比如el-form-item__content
- 双中划线
--
来作为块和修饰器 或 元素和修饰器 的间隔, 比如el-form--inline
- 中划线
-
来作为 块|元素|修饰器 名称中多个单词的间隔 - 状态的前缀用
is
, 比如是否选中,就是is-checked
命名空间 $namespace
其实就是 BEM 的前缀,也就是说,后面我在做 air-ui
的时候,直接设置$namespace: 'air';
就可以了。
BEM 在 sass 中的定义
在 packages/theme-chalk/src/mixins/mixins.scss
有对 BEM 的宏定义:
block 的定义
1 | @mixin b($block) { |
逻辑看起来不难理解,结合上面的 config.scss
的定义,这时就能很清楚的看到block
的生成就是基于BEM规范中,块是设计或布局的一部分,具有一定的意义,利用命名空间el
加上中划线,以及传入的block
的名字,构建出block
的样式,例如alert
组件,在渲染完成后是el-alert
,体现出它的唯一性。而在块的内部,再来编写跟这个块关联的其他样式代码。
这边注意两个细节:
!global
表示变量提升,将局部变量B
提升为全局变量,这样子在其他函数体内也能访问到此变量 (这样子 b 里面的 e 才能访问到 b 的选择器B
)#{}
表示插值,可以通过#{}
插值语法在选择器和属性名中使用SassScript
变量, 比如原先是这样子:
1 | $name: foo; |
经过编译之后,就会变成:
1 | p.foo { |
element 的定义
1 | @mixin e($element) { |
上面的 each
很好理解,因为有可能传进去的多个参数,比如在 table
中,就有这种写法:
1 | @include b(table) { |
编译成 css 就是:
1 | .el-table__body-wrapper, .el-table__footer-wrapper, .el-table__header-wrapper { |
当然这边也要注意一个细节,就是 @at-root
将父级选择器直接暴力的改成根选择器。
接下来我们将看一下这个 if
和 else
分支,为什么会出现 hitAllSpecialNestRule
函数判断的分支,原因是在修饰符或者其他mixin中(比如 伪类或者状态) 嵌套一个元素element,会出现修饰符在前,而元素在后的编译结果,所以我们用 hitAllSpecialNestRule
函数来判断是否存在特殊的嵌套,如果存在的话,就将 element 元素嵌套在里面,如果不存在,则原样输出(改成根选择器输出)
接下来看一下 hitAllSpecialNestRule
的定义, 在 packages/theme-chalk/src/mixins/function.scss
中
1 | @import "config"; |
第一个函数 selectorToString
,就是将我们的选择器转换成一个字符串,而接下来的三个函数,分别判断了:
- 是否存在 修饰符, 就是
m
, 通过上级选择器是否含有标记为m
的--
子串 - 是否存在 flag,比如
.is-checked
,通过上级选择器是否含有is-
子串 - 是否存在伪类,比如
:hover
,通过上级选择器是否含有:
子串
最后综合在一起返回结果,避免嵌套。
m 嵌套 e 的情况
比如在 message-box.scss
当为居中布局的时候,就会出现这种情况:
1 | @include b(message-box) { |
编译成 css 就是:
1 | .el-message-box--center .el-message-box__header { |
可以看到有嵌套进去了
包含状态 b 嵌套 e
比如在 step.scss
当为横向展示的时候,就会出现这种情况:
1 | @include b(step) { |
编译成 css 就是:
1 | .el-step.is-horizontal .el-step__line { |
其中 when
也是一个自定义的宏,主要是用来添加状态:
1 | @mixin when($state) { |
包含伪类 b 嵌套 e
比如还是在 step.scss
也会出现这种情况:
1 | @include b(step) { |
编译成 css 就是:
1 | .el-step:last-of-type .el-step__line { |
其中 pseudo
也是一个自定义的宏,主要是用来添加伪类状态:
1 | @mixin pseudo($pseudo) { |
modifier 的定义
相对于 e 的定义, m 的定义就会比较好懂了:
1 | @mixin m($modifier) { |
这边要注意一个细节,在拼接 currentSelector
字符串时,使用了$
父级选择器,而没有使用全局变量B
+ 全局变量E
来拼接,因为结构不一定是B-E-M
,有可能是B-M
。
同时也有存在一次定义多个 m 的情况,比如在 table.scss
中就有:
1 | @include b(table) { |
编译成 css 就是:
1 | .el-table--border, .el-table--group { |
基本上关于 BEM
的分析也就这些了,接下来就是在具体写组件的时候,熟练应用了。 接下来用一个例子来回顾一下。
BEM 例子
要写例子,直接在 scss在线编译 直接模拟,非常方便:
首先先把一些 参数和用到的宏定义,预填进去:
1 | $namespace: 'el'; |
接下来就可以在下面直接写 sass 代码了:
1 | .container { |
右边就会生成对应的 css 代码:
实现响应式布局的 宏 res
其实 element-ui
在 sass 定义的宏和函数不多,大部分都是为了 BEM
服务的。 只有一个 宏 res
是为了实现响应式布局用的:
1 | @mixin res($key, $map: $--breakpoints) { |
用过 element-ui
应该知道在使用 col
组件的时候,是允许设置根据不同的屏幕尺寸来设置不同的响应尺寸的,比如以下这个:
1 | <el-row :gutter="10"> |
其实这个就是通过 res 这个宏实现的:具体配置是这样子的,这个是针对大屏幕的:
1 | @include res(lg) { |
然后再结合这些规定的参数,就可以实现不同的媒体查询的样式:
1 | $--sm: 768px !default; |
我们可以在线实现一下: 具体实现:
参数定制 var.scss
element-ui
中所有可定制的参数,全部在 var.scss
文件里面。这个文件也是后面做主题定制的关键,因为只要重写这个文件的某些参数,重新打包成 css,就相当于做了一个新的主题了。 这个在后面的文章中会再讲到
系列文章:
自建vue组件 air-ui (1) -- 为啥我要自建一个类 element ui 的组件
自建vue组件 air-ui (2) -- 先分析一下 element ui 项目
自建vue组件 air-ui (3) -- css 开发规范
自建vue组件 air-ui (4) -- air-ui 环境搭建和目录结构
自建vue组件 air-ui (5) -- 创建第一个组件 Button
自建vue组件 air-ui (6) -- 创建内置服务组件
自建vue组件 air-ui (7) -- 创建指令组件
自建vue组件 air-ui (8) -- 实现部分引入组件
自建vue组件 air-ui (9) -- 用 vuepress 写文档
自建vue组件 air-ui (10) -- vuepress 写文档 (进阶版)
自建vue组件 air-ui (11) -- vuepress 写文档 (爬坑版)
自建vue组件 air-ui (12) -- 国际化机制
自建vue组件 air-ui (13) -- 国际化机制(进阶版)
自建vue组件 air-ui (14) -- 打包构建(dev 和 dist)
自建vue组件 air-ui (15) -- 主题定制
自建vue组件 air-ui (16) -- 打包构建 pub 任务
自建vue组件 air-ui (17) -- 开发爬坑篇以及总结