关于程序员:基于elementplus-button-源码分析造轮子

8次阅读

共计 13893 个字符,预计需要花费 35 分钟才能阅读完成。

前言

实现组件 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](https://github.com/element-plus/element-plus/blob/dev/packages/hooks/use-namespace/index.ts) 从全局获取命名空间,我这里没有用,间接应用默认的命名空间,例如 `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 {

b,
e,
m,
be,
em,
bm,
bem,
is,

}
}


`bem` 标准脚本生成的形式灵便,独自保护不嵌入代码,如果要替换组件库的前缀命名空间,只须要在全局配置传入替换就行

### 公共款式 scss 变量

`element-plus` scss 文件构造和 element-ui 差不多,区别在于应用 `Dart Sass` 的 `sass:map...` 和 `@use` 重构所有的 `SCSS` 变量,解决 `@import` 造成的反复输入问题,SASS 应用能够看下之前整顿的[这篇文章](https://juejin.cn/post/7130472496242884645)

`scss` 款式变量定义在 [packages/theme-chalk/src/common/var.scss](https://github.com/element-plus/element-plus/blob/dev/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/documen…
// 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(
        $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 自定义属性(变量)](https://developer.mozilla.org/zh-CN/docs/Web/CSS/Using_CSS_custom_properties) 设定标记值(比方:`--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](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/var.scss),它被引入 [theme-chalk/src/base.scs](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/base.scss) 文件,`base.scss` 别离引入到了 [/theme-chalk/src/index.scss](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/index.scss) 和 [packages/components/base/style/css.ts](https://github.com/element-plus/element-plus/blob/dev/packages/components/base/style/css.ts)

如果全量注册组件,引入 `index.scss` 打包编译后的款式;如果是按需注册组件,从组件的 `style` 目录下引入 `css` 文件,其中退出了 `base/style/css.ts`,例如 [button](https://github.com/element-plus/element-plus/blob/dev/packages/components/button/style/css.ts)

import ‘@element-plus/components/base/style/css’;
import ‘@element-plus/theme-chalk/el-button.css’;


[element-plus 全局 css 变量](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/var.scss) 定义两个 `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](https://github.com/element-plus/element-plus/blob/dev/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](https://github.com/element-plus/element-plus/blob/dev/packages/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 变量执行后果如下 
![](https://files.mdnice.com/user/26477/9f6e0d85-70c3-4fa3-af39-ffc4b05b6e11.png)


#### 部分组件 css 变量

[button.scss](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/button.scss#L19-L21) 会在后面执行上面这段代码生成 ` 组件部分 css 自定义变量 `

@include b(button) {
@include set-component-css-var(‘button’, $button);
}


`$button` 组件变量是定义在 [common/var.scss](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss#L628-L650)

// 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](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/mixins/_var.scss#L38-L46) 遍历 `$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 局部变量

![](https://files.mdnice.com/user/26477/1e84d281-9acf-4611-98f2-c9721a8ca0ac.png)

设置雷同的 name `--name` 能够笼罩 `root` 变量值


#### button.scss 源码剖析

`button.scss` 款式文件构造和 element-ui 差异不大,能够浏览 [element-ui 组件库 button 源码剖析](https://juejin.cn/post/7138611140023566350#heading-8)

剖析一下差别点

1. 应用 `getCssVar()` 设置 css 变量值,例如 `getCssVar('button', 'bg-color');` 生成 `var(--el-button-bg-color`,它应用的组件部分 css 变量,部分又是继承全局的 `--el-bg-color`

这样做的益处是如果要更改 button 的背景,只须要批改 `--el-button-bg-color` 值,这样就不会影响到全局的背景色彩 `--el-bg-color`

2. 之前生成 `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);
}

}


3. 在 [_button.scss](https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/mixins/_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,
&.is-text,
&.is-link {

@include button-plain($type);

}
}


以上是 `element-plus` button 源码剖析,造轮子后的[演示地址](https://jefferyxzf.github.io/douluo-ui/packages/element/button/)
正文完
 0