大家好,我是小c,一个前端世界的无名小卒。当然为了成为大佬,我也是拼劲全力,但偶尔也有懈怠的时候(我发誓,真的只是偶尔)。
这不,最近升级公司的内部系统,使用了Vue3.x, 虽然使用上并没啥问题,但是总觉得棋差一招。
不想读源码的程序员不是好的程序员(前端之所以卷成这个样子,我也是贡献了一部分力量得,哎),于是打算阅读elemen Plus的源码。看看github上的大神是怎么写的。
在这里,我先立下一个flag,我打算一周看一个组件。但是我没想到,竟然有110个组件,这不得看个两年的时间。
我的flag刚立下就要倒了嘛,不行,自我激励的话在此省略一千八百行,总之,我不能放弃。
当然啦,先从最简单的El-Tag看起。
基本逻辑
基本逻辑当然在src文件夹【只有两个文件,而且代码量都不超过80行】
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分别是40, 32 和24.
样式方面
使用使用bem和Sass Modules【sass-lang.com/documentati… 使样式更容易维护。具体代码就不细讲了,后面专门针对theme-chalk进行细讲
总计了几个知识点:
-
Scss 定义变量 【!default、!global、!optional】 blog.csdn.net/hide_in_dar…
-
Scss 指令学习【关于Scss指令@mixin、@include、@content、@function】 blog.csdn.net/hide_in_dar…
总之,ElTag代码量方面看着比较少,但涉及的东西还是比较多的,自己也学习了不少的东西。最后给自己打点鸡血,加油继续干。