element-plus是一个开箱即用的ui组件库.看看简单的button组件,他们是如何设计的,官方给的contents如下:
通过阅读源码,总结以下几点:
- 封装了原生的button标签,它具备原来的功能,button
- 做公共ui组件样式需要进行隔离,这一点非常重要,提取公共组件的时候,也应该默认加入,很多ui组件库的做法都是通过转换,在某一个区域/块,增加唯一前缀标识,简称命名空间。
<button
ref="_ref"
:class="[
ns.b(),
ns.m(_type),
ns.m(_size),
ns.is('disabled', _disabled),
ns.is('loading', loading),
ns.is('plain', plain),
ns.is('round', round),
ns.is('circle', circle),
ns.is('text', text),
ns.is('link', link),
ns.is('has-bg', bg),
]"
:aria-disabled="_disabled || loading"
:disabled="_disabled || loading"
:autofocus="autofocus"
:type="nativeType"
:style="buttonStyle"
@click="handleClick"
>
</button>
可以看到是通过ns函数进行初始化、转换添加样式前缀,下面来看看这个命名空间函数怎么写的:
packages/hooks/use-namespace
import { useGlobalConfig } from '../use-global-config'
export const defaultNamespace = 'el' // 初始化前缀常量
const statePrefix = 'is-' // 是否拥有某些功能、样式前缀
// 对样式名称重新命名
const _bem = (
namespace: string, // 空间名称,如el
block: string, // 要命名的块,如 使用button
blockSuffix: string, // 区块后缀
element: string, // 普通标签容器
modifier: string // 可修改的样式名称
) => {
let cls = `${namespace}-${block}` // 比如 el-button
if (blockSuffix) { // el-checkbox-group
cls += `-${blockSuffix}`
}
if (element) { // 如 el-scrollbar__wrap 标签容器
cls += `__${element}`
}
if (modifier) { // el-button--warning 其中warning可能为success
cls += `--${modifier}`
}
return cls
}
// 声明命名空间
export const useNamespace = (block: string) => {
const namespace = useGlobalConfig('namespace', defaultNamespace)
const b = (blockSuffix = '') =>
_bem(namespace.value, block, blockSuffix, '', '')
const e = (element?: string) =>
element ? _bem(namespace.value, block, '', element, '') : ''
const m = (modifier?: string) =>
modifier ? _bem(namespace.value, block, '', '', modifier) : ''
const be = (blockSuffix?: string, element?: string) =>
blockSuffix && element
? _bem(namespace.value, block, blockSuffix, element, '')
: ''
const em = (element?: string, modifier?: string) =>
element && modifier
? _bem(namespace.value, block, '', element, modifier)
: ''
const bm = (blockSuffix?: string, modifier?: string) =>
blockSuffix && modifier
? _bem(namespace.value, block, blockSuffix, '', modifier)
: ''
const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
blockSuffix && element && modifier
? _bem(namespace.value, block, blockSuffix, element, modifier)
: ''
/** 切换按钮(是否)作为 链接link、文字text(新版被废弃)、
** 禁用disabled、带loading、round圆角、原形按钮circle、朴素按钮plain的样式
**/
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;
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
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
}
const cssVarName = (name`: string) => `--${namespace.value}-${name}`
const cssVarBlockName = `(name: string) =>
`--${namespace.value}-${block}-${name}`
return {
namespace, // 空间
b, // 区块后缀
e,// 标签容器
m,// 可修改类型
be, // b&&e
em, // e&&m
bm, // b&&m
bem, // b&&e&&
is, // 是否为某个状态
// css
cssVar, // style对象
cssVarName,
cssVarBlock, // style对象
cssVarBlockName,
}
}
export type UseNamespaceReturn = ReturnType<typeof useNamespace>
- 首先定义默认前缀defaultNamespace,element-plus的就定义为el,定义是否拥有某些功能、样式前缀is-,使用该ui库后,查看dom结构都会看到对应的前缀,如下:
- 创建命名空间箭头函数,内联style也要使用cssVar、cssVarBlock增加空间标识和前缀,如--el-border-style:solid; 就是增加了前缀--与开发者项目中的进行border-style进行区分。
- 代码整洁,如大篇幅的props,emit进行抽离,而不是堆积在一个文件内,导致维护成本高。
import { buttonEmits, buttonProps } from './button'
const props = defineProps(buttonProps)
const emit = defineEmits(buttonEmits)
- 原生ts event
- MouseEvent 接口指用户与指针设备(如鼠标)交互时发生的事件
- HTMLButtonElement 接口提供操作 button 元素 的属性和方法。