美文网首页
基于element-plus button 源码分析造轮子

基于element-plus button 源码分析造轮子

作者: WebJeffery | 来源:发表于2022-09-07 22:06 被阅读0次

    前言

    实现组件 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 {
        b,
        e,
        m,
        be,
        em,
        bm,
        bem,
        is,
      }
    }
    

    bem 规范脚本生成的方式灵活,单独维护不嵌入代码,如果要替换组件库的前缀命名空间,只需要在全局配置传入替换就行

    公共样式 scss 变量

    element-plus scss 文件结构和 element-ui 差不多,区别在于使用 Dart Sasssass: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(
                $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.scsspackages/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 源码分析

    分析一下差异点

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

    这样做的好处是如果要更改 button 的背景,只需要修改 --el-button-bg-color 值,这样就不会影响到全局的背景颜色 --el-bg-color

    1. 之前生成 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);
        }
      }
    
    1. _button.scss 文件的 button-variant 悬浮、激活、禁用等状态不再直接编写代码,而是定义好各个状态的数据结构,然后遍历修改 backgroundcolorborder-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 源码分析,造轮子后的演示地址

    相关文章

      网友评论

          本文标题:基于element-plus button 源码分析造轮子

          本文链接:https://www.haomeiwen.com/subject/wyknnrtx.html