基于element-plus button 源码分析造轮子
前言
实现组件
button
新增功能和自定义UI换肤,使用
SCSS
变量和
CSS 自定义属性
,参考
element-plus
源码造轮子
button 组件
element-plus 的
button
文件
/packages/components/button/src/button.vue
和 element-ui 实现逻辑是相似的,不同地方在于生成
bem
规范实现方式不一样,前者通过函数创建命名空间对象,然后调用
b()
、
e()
、
m()
、
is()
等函数返回符合
bem
规范的类,后者通过字符串拼接生成
脚本函数创建命名空间对象
- 优点:可读性强,减少模版编写,方便维护管理,可以动态的更改命名空间前缀
- 缺点:每个组件创建命名空间对象,占用额外内存
// 参考element-plus button 实现
<template>
<button
:class="[
ns.b(), // el-button
ns.m(type), // 传入 type 生成 el-button--primary/success/info 等
ns.m(buttonSize), // 传入 size 得到 el-button--large/small
ns.is('disabled', buttonDisabled), // is-disabled
ns.is('loading', loading), // is-loading
ns.is('plain', plain), // is-plain
ns.is('ghost', ghost), // is-ghost
ns.is('round', round), // is-round
ns.is('circle', circle), // is-circle
ns.is('text', text), // is-text
ns.is('link', link), // is-link
@click="handleClick"
:disabled="buttonDisabled || loading" // 禁用
:autofocus="autofocus"
:type="nativeType"
<template v-if="loading"> // 加载图标
<slot v-if="$slots.loading" name="loading"></slot>
<i v-else class="el-icon-loading"></i>
</template>
// 自定义图标
<template v-else-if="icon || $slots.icon">
<span v-if="$slots.icon"><slot name="icon"></slot></span>
<i v-else :class="icon"></i>
</template>
<span v-if="$slots.default"><slot></slot></span>
</button>
</template>
<script>
const { useNamespace } from '@element-plus/hooks'
// 创建 button 命名空间
const ns = useNamespace('button')
</script>
useNamespace
从全局获取命名空间,我这里没有用,直接使用默认的命名空间,例如
el
,然后调用不同的函数,根据传入参数判断拼接字符串返回
export const defaultNamespace = 'el'
const statePrefix = 'is-'
* 生成 bem
* @param {} namespace 命名空间
* @param {*} block 块
* @param {*} blockSuffix 块多个单词
* @param {*} element 元素
* @param {*} modifier 修饰符
* @returns
const _bem = (namespace, block, blockSuffix, element, modifier) => {
let cls = `${namespace}-${block}` // el-button
if (blockSuffix) {
cls += `-${blockSuffix}`
if (element) {
cls += `__${element}`
if (modifier) {
cls += `--${modifier}`
return cls
export const useNamespace = (block) => {
// 默认命名空间
const namespace = defaultNamespace
// b() => el-button
const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', '')
// e(primary) => el-button__primary
const e = (element) => element ? _bem(namespace, block, '', element, '') : ''
// m(primary) => el-button--primary
const m = (modifier) => modifier ? _bem(namespace, block, '', '', modifier) : ''
const be = (blockSuffix, element) => blockSuffix && element
? _bem(namespace, block, blockSuffix, element, '')
const em = (element, modifier) => element && modifier
? _bem(namespace, block, '', element, modifier)
const bm = (blockSuffix, modifier) => blockSuffix && modifier
? _bem(namespace, block, blockSuffix, '', modifier)
const bem = (blockSuffix, element, modifier) => blockSuffix && element && modifier
? _bem(namespace, block, blockSuffix, element, modifier)
// is(disabled) => is-disabled
const is = (name, ...args) => {
const state = args.length >= 1 ? args[0] : true
return name && state ? `${statePrefix}${name}` : ''
return {
bem
规范脚本生成的方式灵活,单独维护不嵌入代码,如果要替换组件库的前缀命名空间,只需要在全局配置传入替换就行
公共样式 scss 变量
element-plus
scss 文件结构和 element-ui 差不多,区别在于使用
Dart Sass
的
sass:map...
和
@use
重构所有的
SCSS
变量,解决
@import
造成的重复输出问题,SASS 使用可以看下之前整理的
这篇文章
scss
样式变量定义在
packages/theme-chalk/src/common/var.scss
,例如主题颜色、字体颜色、边框颜色、背景颜色、字体大小、组件样式变量等
下面是部分代码,
$types
定义 6 种主要类型,是列表数组类型;
$colors: () !default;
初始化
$colors
变量,
map.deep-merge()
是调用
sass:map
函数深度合并,然后通过
map.get
取值,获取
map
多层嵌套值,传入多个参数,逗号隔开
map.get($colors, 'primary', 'base')
注意
:
$color-primary
不以下划线或横杆开头声明
$-color-primary
,是因为横杠开头声明为私有变量,
@use
是没办法在外部引入使用
@use 'sass:map';
// types
$types: primary, success, warning, danger, error, info;
// Color
$colors: () !default;
$colors: map.deep-merge(
'white': #ffffff,
'black': #000000,
'primary': (
'base': #409eff,
'success': (
'base': #67c23a,
'warning': (
'base': #e6a23c,
'danger': (
'base': #f56c6c,
'error': (
'base': #f56c6c,
'info': (
'base': #909399,
$colors
$color-white: map.get($colors, 'white') !default;
$color-black: map.get($colors, 'black') !default;
$color-primary: map.get($colors, 'primary', 'base') !default;
$color-success: map.get($colors, 'success', 'base') !default;
$color-warning: map.get($colors, 'warning', 'base') !default;
$color-danger: map.get($colors, 'danger', 'base') !default;
$color-error: map.get($colors, 'error', 'base') !default;
$color-info: map.get($colors, 'info', 'base') !default;
@each
遍历
$typs
,调用
set-color-mix-level
函数,使用
mix(color1, color2, percent)
进行颜色混合,它接收三个参数,前面两个参数是两种混合的颜色,
$mix-color
默认是白色,
map.get($colors, $type, 'base')
获取
type
类型 base 颜色,第三个参数是两个混合颜色的百分占比,例如
0.1
表示第一个参数颜色占比 10%,第二个颜色 90%;
dark-2
值是混合黑色的颜色
// https://sass-lang.com/documentation/values/maps#immutability
// mix colors with white/black to generate light/dark level
@mixin set-color-mix-level(
$type,
$number,
$mode: 'light',
$mix-color: $color-white
$colors: map.deep-merge(
$type: (
'#{$mode}-#{$number}':
$mix-color,
map.get($colors, $type, 'base'),
math.percentage(math.div($number, 10))
$colors
) !global;
// $colors.primary.light-i
// --el-color-primary-light-i
// 10% 53a8ff
// 20% 66b1ff
// 30% 79bbff
// 40% 8cc5ff
// 50% a0cfff
// 60% b3d8ff
// 70% c6e2ff
// 80% d9ecff
// 90% ecf5ff
@each $type in $types {
@for $i from 1 through 9 {
@include set-color-mix-level($type, $i, 'light', $color-white);
// --el-color-primary-dark-2
@each $type in $types {
@include set-color-mix-level($type, 2, 'dark', $color-black);
遍历混合后,打印
$colors
颜色值
(
info: ("dark-2": #73767a, "light-9": #f4f4f5, "light-8": #e9e9eb, "light-7": #dedfe0, "light-6": #d3d4d6, "light-5": #c8c9cc, "light-4": #bcbec2, "light-3": #b1b3b8, "light-2": #a6a9ad, "light-1": #9b9ea3, "base": #909399),
error: ("dark-2": #cc3c2d, "light-9": #ffedeb, "light-8": #ffdbd7, "light-7": #ffc9c3, "light-6": #ffb7af, "light-5": #ffa59c, "light-4": #ff9388, "light-3": #ff8174, "light-2": #ff6f60, "light-1": #ff5d4c, "base": #FF4B38),
danger: ("dark-2": #cc3c2d, "light-9": #ffedeb, "light-8": #ffdbd7, "light-7": #ffc9c3, "light-6": #ffb7af, "light-5": #ffa59c, "light-4": #ff9388, "light-3": #ff8174, "light-2": #ff6f60, "light-1": #ff5d4c, "base": #FF4B38),
warning: ("dark-2": #cc7a00, "light-9": #fff5e6, "light-8": #ffebcc, "light-7": #ffe0b3, "light-6": #ffd699, "light-5": #ffcc80, "light-4": #ffc266, "light-3": #ffb84d, "light-2": #ffad33, "light-1": #ffa31a, "base": #FF9900),
success: ("dark-2": #309e70, "light-9": #ecf9f4, "light-8": #d8f3e8, "light-7": #c5eedd, "light-6": #b1e8d1, "light-5": #9ee2c6, "light-4": #8adcba, "light-3": #77d6af, "light-2": #63d1a3, "light-1": #50cb98, "base": #3CC58C),
primary: ("dark-2": #337ecc, "light-9": #ecf5ff, "light-8": #d9ecff, "light-7": #c6e2ff, "light-6": #b3d8ff, "light-5": #a0cfff, "light-4": #8cc5ff, "light-3": #79bbff, "light-2": #66b1ff, "light-1": #53a8ff, "base": #409eff),
"white": #ffffff,
"black": #000000)
除了定义常用的字体颜色、边框颜色等变量外,所有的组件变量也定义在这个文件,例如
checkbox 复选框
// Components
// ---
// Checkbox
// css3 var in packages/theme-chalk/src/checkbox.scss
$checkbox: () !default;
$checkbox: map.merge(
'font-size': 14px,
'font-weight': getCssVar('font-weight-primary'),
'text-color': getCssVar('text-color-regular'),
'input-height': 14px,
'input-width': 14px,
'border-radius': getCssVar('border-radius-small'),
'bg-color': getCssVar('fill-color', 'blank'),
'input-border': getCssVar('border'),
'disabled-border-color': getCssVar('border-color'),
'disabled-input-fill': getCssVar('fill-color', 'light'),
'disabled-icon-color': getCssVar('text-color-placeholder'),
'disabled-checked-input-fill': getCssVar('border-color-extra-light'),
'disabled-checked-input-border-color': getCssVar('border-color'),
'disabled-checked-icon-color': getCssVar('text-color-placeholder'),
'checked-text-color': getCssVar('color-primary'),
'checked-input-border-color': getCssVar('color-primary'),
'checked-bg-color': getCssVar('color-primary'),
'checked-icon-color': getCssVar('color', 'white'),
'input-border-color-hover': getCssVar('color-primary'),
$checkbox
上面定义变量值有使用
getCssVar()
函数,它是应用 css 自定义属性,接下来介绍它
两种 css 自定义变量
CSS 自定义属性(变量)
设定标记值(比如:
--main-color: black;
),由
var()
函数来获取值(比如:
color: var(--main-color);
)
:root {
--main-bg-color: brown;
局部变量时用
var()
函数包裹以表示一个合法的属性值,
var()
如果第一个参数不生效,可以接受第二个参数默认值
注意:自定义属性名是大小写敏感的,
--my-color
和
--My-color
会被认为是两个不同的自定义属性。
element {
background-color: var(--main-bg-color);
通过
JavaScript
操作
var
变量值
// 获取一个 Dom 节点上的 CSS 变量
element.style.getPropertyValue("--my-var");
// 获取任意 Dom 节点上的 CSS 变量
getComputedStyle(element).getPropertyValue("--my-var");
// 修改一个 Dom 节点上的 CSS 变量
element.style.setProperty("--my-var", 'red');
在
element-plus
有两种
css
自定义属性:全局
root
和局部组件
全局 css 变量
全局的 css 变量定义在
packages/theme-chalk/src/var.scss
,它被引入
theme-chalk/src/base.scs
文件,
base.scss
分别引入到了
/theme-chalk/src/index.scss
和
packages/components/base/style/css.ts
如果全量注册组件,引入
index.scss
打包编译后的样式;如果是按需注册组件,从组件的
style
目录下引入
css
文件,其中加入了
base/style/css.ts
,例如
button
import '@element-plus/components/base/style/css';
import '@element-plus/theme-chalk/el-button.css';
element-plus 全局css变量
定义两个
root
, 通用和
light
主题
// common
:root {
@include set-css-var-value('color-white', $color-white);
@include set-css-var-value('color-black', $color-black);
// get rgb
@each $type in (primary, success, warning, danger, error, info) {
@include set-css-color-rgb($type);
// Typography
@include set-component-css-var('font-size', $font-size);
// for light
:root {
color-scheme: light;
@include set-css-var-value('color-white', $color-white);
@include set-css-var-value('color-black', $color-black);
// --el-color-#{$type}
// --el-color-#{$type}-light-{$i}
@each $type in (primary, success, warning, danger, error, info) {
@include set-css-color-type($colors, $type);
css 变量生成的函数定义在 packages/theme-chalk/src/mixins/_var.scss
例如
set-css-var-value('color-white', $color-white)
, 调用
joinVarName
得到
--el-color-white
,最后结果是
--el-color-white: #fff;
@mixin set-css-var-value($name, $value) {
#{joinVarName($name)}: #{$value};
theme-chalk/src/mixins/function.scss#L47-L55
@function joinVarName($list) {
$name: '--' + config.$namespace;
@each $item in $list {
@if $item != '' {
$name: $name + '-' + $item;
@return $name;
全局 css 变量执行结果如下
局部组件css变量
button.scss
会在前面执行下面这段代码生成
组件局部 css 自定义变量
@include b(button) {
@include set-component-css-var('button', $button);
$button
组件变量是定义在
common/var.scss
// Button
// css3 var in packages/theme-chalk/src/button.scss
$button: () !default;
$button: map.merge(
'font-weight': getCssVar('font-weight-primary'),
'border-color': getCssVar('border-color'),
'bg-color': getCssVar('fill-color', 'blank'),
'text-color': getCssVar('text-color', 'regular'),
'disabled-text-color': getCssVar('disabled-text-color'),
'disabled-bg-color': getCssVar('fill-color', 'blank'),
'disabled-border-color': getCssVar('border-color-light'),
'divide-border-color': rgba($color-white, 0.5),
'hover-text-color': getCssVar('color-primary'),
'hover-bg-color': getCssVar('color-primary', 'light-9'),
'hover-border-color': getCssVar('color-primary-light-7'),
'active-text-color': getCssVar('button-hover-text-color'),
'active-border-color': getCssVar('color-primary'),
'active-bg-color': getCssVar('button', 'hover-bg-color'),
'outline-color': getCssVar('color-primary', 'light-5'),
'hover-link-text-color': getCssVar('color-info'),
'active-color': getCssVar('text-color', 'primary'),
$button
set-component-css-var
遍历
$button
,然后拼接
css
变量名和值
@mixin set-component-css-var($name, $variables) {
@each $attribute, $value in $variables {
@if $attribute == 'default' {
#{getCssVarName($name)}: #{$value};
} @else {
#{getCssVarName($name, $attribute)}: #{$value};
生成 button 组件的css局部变量
设置相同的 name
--name
可以覆盖
root
变量值
button.scss 源码分析
button.scss
样式文件结构和 element-ui 差别不大,可以阅读
element-ui 组件库 button 源码分析
分析一下差异点
-
使用
getCssVar()
设置 css 变量值,例如getCssVar('button', 'bg-color');
生成var(--el-button-bg-color
,它使用的组件局部 css 变量,局部又是继承全局的--el-bg-color
这样做的好处是如果要更改 button 的背景,只需要修改
--el-button-bg-color
值,这样就不会影响到全局的背景颜色
--el-bg-color
-
之前生成
primary, success, warning, danger, info
6种类型的按钮分别调用button-variant
,现在使用 Sass 重构后直接@each
遍历就行
@each $type in (primary, success, warning, danger, info) {
@include m($type) {
@include button-variant($type);
-
在
_button.scss
文件的
button-variant
悬浮、激活、禁用等状态不再直接编写代码,而是定义好各个状态的数据结构,然后遍历修改background
、color
、border-color
css变量值
@mixin button-variant($type) {
$button-color-types: (
'': (
'text-color': (
'color',
'white',
'bg-color': (
'color',
$type,
'border-color': (
'color',
$type,
'outline-color': (
'color',
$type,
'light-5',
'active-color': (
'color',
$type,
'dark-2',
'hover': (
'text-color': (
'color',
'white',
'link-text-color': (
'color',
$type,
'light-5',
'bg-color': (
'color',
$type,
'light-3',
'border-color': (
'color',
$type,
'light-3',
'active': (
'bg-color': (
'color',
$type,
'dark-2',
'border-color': (
'color',
$type,
'dark-2',
'disabled': (
'text-color': (
'color',
'white',
'bg-color': (
'color',
$type,
'light-5',
'border-color': (
'color',
$type,
'light-5',
@each $type, $typeMap in $button-color-types {
@each $typeColor, $list in $typeMap {
@include css-var-from-global(('button', $type, $typeColor), $list);
&.is-plain,