element-plus组件——ElTag含泪解读

899 阅读3分钟

大家好,我是小c,一个前端世界的无名小卒。当然为了成为大佬,我也是拼劲全力,但偶尔也有懈怠的时候(我发誓,真的只是偶尔)。

这不,最近升级公司的内部系统,使用了Vue3.x, 虽然使用上并没啥问题,但是总觉得棋差一招。

不想读源码的程序员不是好的程序员(前端之所以卷成这个样子,我也是贡献了一部分力量得,哎),于是打算阅读elemen Plus的源码。看看github上的大神是怎么写的。

1665913556340.png

在这里,我先立下一个flag,我打算一周看一个组件。但是我没想到,竟然有110个组件,这不得看个两年的时间。

image.png

我的flag刚立下就要倒了嘛,不行,自我激励的话在此省略一千八百行,总之,我不能放弃。

当然啦,先从最简单的El-Tag看起。

1665913893984.png

基本逻辑

基本逻辑当然在src文件夹【只有两个文件,而且代码量都不超过80行】

1665915115917.png

tag.vue的代码如下:

 <template>
    <!--移除渐变动画-->
    <span
      v-if="disableTransitions"
      :class="classes"
      :style="{ backgroundColor: color }"
      @click="handleClick"
    >
      <span :class="ns.e('content')">
        <!--自定义默认内容-->
        <slot />
      </span>
       <!--关闭标签-->
      <el-icon v-if="closable" :class="ns.e('close')" @click.stop="handleClose">
        <Close />
      </el-icon>
    </span>
    <!--使用渐变动画-->
    <transition v-else :name="`${ns.namespace.value}-zoom-in-center`" appear>
      <span
        :class="classes"
        :style="{ backgroundColor: color }"
        @click="handleClick"
      >
        <span :class="ns.e('content')">
          <!--自定义默认内容-->
          <slot />
        </span>
         <!--关闭标签-->
        <el-icon v-if="closable" :class="ns.e('close')" @click.stop="handleClose">
          <Close />
        </el-icon>
      </span>
    </transition>
  </template>

  <script lang="ts" setup>
  import { computed } from 'vue'
  import ElIcon from '@element-plus/components/icon'
  import { Close } from '@element-plus/icons-vue'

  import { useNamespace, useSize } from '@element-plus/hooks'
  import { tagEmits, tagProps } from './tag'

  defineOptions({
    name: 'ElTag',
  })
  const props = defineProps(tagProps)
  const emit = defineEmits(tagEmits)

  const tagSize = useSize()
  const ns = useNamespace('tag')  
  
  const classes = computed(() => {
    const { type, hit, effect, closable, round } = props
    return [
      ns.b(), // el-tag
      ns.is('closable', closable),
      ns.m(type), //  el-tag--danger
      ns.m(tagSize.value),
      ns.m(effect), //  el-tag--plain
      ns.is('hit', hit),
      ns.is('round', round), // is-round
    ]
  })

  // methods
  // 关闭 Tag 时触发的事件
  const handleClose = (event: MouseEvent) => {
    emit('close', event)
  }
  // 点击 Tag 时触发的事件
  const handleClick = (event: MouseEvent) => {
    emit('click', event)
  }
</script>

其中,tag.ts 只引入了两个常量【tagEmits,tagProps】,我觉得大家应该一看就知道是什么意思了,代码如下

  import { buildProps } from '@element-plus/utils'
  import { componentSizes } from '@element-plus/constants'
  import type Tag from './tag.vue'

  import type { ExtractPropTypes } from 'vue'

  export const tagProps = buildProps({
    // 是否可关闭
    closable: Boolean,
    // 类型
    type: {
      type: String,
      values: ['success', 'info', 'warning', 'danger', ''],
      default: '',
    },
    // 是否有边框描边
    hit: Boolean,
    // 是否禁用渐变动画
    disableTransitions: Boolean,
    // 背景色
    color: {
      type: String,
      default: '',
    },
    // 尺寸
    size: {
      type: String,
      values: componentSizes,
      default: '',
    },
    // 主题
    effect: {
      type: String,
      values: ['dark', 'light', 'plain'],
      default: 'light',
    },
    // Tag 是否为圆形
    round: Boolean,
  } as const)
  export type TagProps = ExtractPropTypes<typeof tagProps>

  export const tagEmits = {
    // 关闭 Tag 时触发的事件
    close: (evt: MouseEvent) => evt instanceof MouseEvent,
    // 点击 Tag 时触发的事件
    click: (evt: MouseEvent) => evt instanceof MouseEvent,
  }
  export type TagEmits = typeof tagEmits

  export type TagInstance = InstanceType<typeof Tag>

我总结了一下,重点代码有两部分:
(1) useSize

 // 参数1: fallback : 字符串,large/default/small/undefined 其中之一
 // 参数2:  ignore: 对象, { prop:boolean, form:boolean, formItem:boolean, global:boolean }
// useProp: 用于获取使用者传的属性值
// useGlobalConfig: 用于获取全局配置中的属性值

 export const useSize = (
 fallback?: MaybeRef<ComponentSize | undefined>,
 ignore: Partial<Record<'prop' | 'form' | 'formItem' | 'global', boolean>> = {}
) => {
 const emptyRef = ref(undefined)
 
 // 如果ignore的prop为true,返回emptyRef, 否则使用useProp<ComponentSize>('size')
 const size = ignore.prop ? emptyRef : useProp<ComponentSize>('size')
 // 如果ignore的global为true,返回emptyRef, 否则使用useGlobalConfig('size')
 const globalConfig = ignore.global ? emptyRef : useGlobalConfig('size')
 // 如果ignore的form为true,返回{ size: undefined }, 否则使用inject(formContextKey, undefined)
 const form = ignore.form
   ? { size: undefined }
   : inject(formContextKey, undefined)
 // 如果ignore的form为true,返回{ size: undefined }, 否则使用iinject(formItemContextKey, undefined)
 const formItem = ignore.formItem
   ? { size: undefined }
   : inject(formItemContextKey, undefined)

// 在ElTag代码里,直接返回的是用户传递的属性值
 return computed(
   (): ComponentSize =>
     size.value ||
     unref(fallback) ||
     formItem?.size ||
     form?.size ||
     globalConfig.value ||
     ''
 )
}

(2) useNamespace

       element Plus的样式采用的是bem的命名规范,Bem是块(block)、元素(element)、修饰符(modifier)的简写,由Yandex团队提出的一种前端CSS命名方法论。
    -中划线:仅作为连字符使用,表示某个块或者某个子元素的多单词之间的连接记号。
    __双下划线:双下划线用来连接块和块的子元素
   _单下划线:单下划线用来描述一个块或者块的子元素的一种状态
 
   import { useGlobalConfig } from '../use-global-config'
   
   // 默认命名空间
   export const defaultNamespace = 'el'
   // 状态前缀:is
   const statePrefix = 'is-'

   // 命名空间 + 块 + 块后缀 + 元素 + 修饰符
   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 = useGlobalConfig('namespace', defaultNamespace)

     // 块  el-tag
     const b = (blockSuffix = '') =>
       _bem(namespace.value, block, blockSuffix, '', '')

     // 元素 el-tag__element
     const e = (element?: string) =>
       element ? _bem(namespace.value, block, '', element, '') : ''

     // 修饰符 el-tag--modifier
     const m = (modifier?: string) =>
       modifier ? _bem(namespace.value, block, '', '', modifier) : ''
     
     // el-tag-blockSuffix__element
     const be = (blockSuffix?: string, element?: string) =>
       blockSuffix && element
         ? _bem(namespace.value, block, blockSuffix, element, '')
         : ''
     // el-tag__element--modifier
     const em = (element?: string, modifier?: string) =>
       element && modifier
         ? _bem(namespace.value, block, '', element, modifier)
         : ''
     // el-tag-blockSuffix--modifier
     const bm = (blockSuffix?: string, modifier?: string) =>
       blockSuffix && modifier
         ? _bem(namespace.value, block, blockSuffix, '', modifier)
         : ''
     // el-tag-blockSuffix__element--modifier
     const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
       blockSuffix && element && modifier
         ? _bem(namespace.value, block, blockSuffix, element, modifier)
         : ''
     // is('hit', hit)  hit 为true, 返回is-hit, 否则返回''
     const is: {
       (name: string, state: boolean | undefined): string
       (name: string): string
     } = (name: string, ...args: [boolean | undefined] | []) => {
       const state = args.length >= 1 ? args[0]! : true
       return name && state ? `${statePrefix}${name}` : ''
     }

     // for css var
     // --el-xxx: value;
     //  ns.cssVar({
     //     'text-color': props.textColor || '',    --el-text-color  
     //     'hover-text-color': props.textColor || '', --el-hover-text-color  
     //   })
     const cssVar = (object: Record<string, string>) => {
       const styles: Record<string, string> = {}
       for (const key in object) {
         if (object[key]) {
           styles[`--${namespace.value}-${key}`] = object[key]
         }
       }
       return styles
     }
     // with block
     //  ns.cssVarBlock({
     //     'text-color': props.textColor || '', --el-tag-text-color  
     //     'hover-text-color': props.textColor || '', --el-tag-hover-text-color
     //   })
     const cssVarBlock = (object: Record<string, string>) => {
       const styles: Record<string, string> = {}
       for (const key in object) {
         if (object[key]) {
           styles[`--${namespace.value}-${block}-${key}`] = object[key]
         }
       }
       return styles
     }
     
     // --el-tag
     const cssVarName = (name: string) => `--${namespace.value}-${name}`
     // --el-tag-name
     const cssVarBlockName = (name: string) =>
       `--${namespace.value}-${block}-${name}`

     return {
       namespace,
       b,
       e,
       m,
       be,
       em,
       bm,
       bem,
       is,
       // css
       cssVar,
       cssVarName,
       cssVarBlock,
       cssVarBlockName,
     }
   }

额外扩展有三部分:
(1) defineOptions

  这里是用了 unplugin-vue-define-options 一个npm插件,实现 在Vue3 setup语法糖中,自定义组件的 name 属性,最后实现组件的全局自动注册。详细的配置的过程链接如下: https://www.npmjs.com/package/unplugin-vue-define-options
  
  vue官方给出:在 3.2.34 或以上的版本中,使用 `<script setup>` 的单文件组件会自动根据文件名生成对应的 `name` 选项,即使是在配合 `<KeepAlive>` 使用时也无需再手动声明。【不得不说,官方太贴心啦】
 

(2) buildProps

  // todo

(3) componentSizes

   element Plus组件基本上都有一个size属性,可以是large / default /small其中之一。默认是default。对应的组件height分别是403224.

样式方面

使用使用bem和Sass Modules【sass-lang.com/documentati… 使样式更容易维护。具体代码就不细讲了,后面专门针对theme-chalk进行细讲

总计了几个知识点:

  1. Scss 定义变量 【!default、!global、!optional】 blog.csdn.net/hide_in_dar…

  2. Scss 指令学习【关于Scss指令@mixin、@include、@content、@function】 blog.csdn.net/hide_in_dar…

总之,ElTag代码量方面看着比较少,但涉及的东西还是比较多的,自己也学习了不少的东西。最后给自己打点鸡血,加油继续干。