示例
ts
import { DEFAULT_NAMESPACE } from '@vben-core/shared/constants';
/**
* @description
* BEM 命名工具函数,帮助生成统一的 className 与 css 变量名
* 来源参考:Element Plus 的 useNamespace 实现
*/
const statePrefix = 'is-'; // 状态前缀,例如 "is-active"、"is-disabled"
/**
* BEM 字符串生成函数
* @param namespace 命名空间(例如 el、vb)
* @param block 组件块名
* @param blockSuffix 块后缀
* @param element 元素名
* @param modifier 修饰符
* @returns 完整的 BEM class 字符串
*/
const _bem = (
namespace: string,
block: string,
blockSuffix: string,
element: string,
modifier: string,
) => {
let cls = `${namespace}-${block}`; // 基础 class,例如 el-button
if (blockSuffix) {
cls += `-${blockSuffix}`; // 添加块后缀,例如 el-button-group
}
if (element) {
cls += `__${element}`; // 添加元素,例如 el-button__icon
}
if (modifier) {
cls += `--${modifier}`; // 添加修饰,例如 el-button--primary
}
return cls;
};
/**
* 用于生成状态类名
* @example is('active') => 'is-active'
* @example is('disabled', false) => ''
*/
const is: {
(name: string): string;
(name: string, state: boolean | undefined): string;
} = (name: string, ...args: [] | [boolean | undefined]) => {
const state = args.length > 0 ? args[0] : true;
return name && state ? `${statePrefix}${name}` : '';
};
/**
* 核心 Hook,用于统一命名空间、生成 className 与 CSS 变量名
* @param block 组件块名(例如 button)
*/
const useNamespace = (block: string) => {
const namespace = DEFAULT_NAMESPACE;
// 生成块 class,例如 el-button
const b = (blockSuffix = '') => _bem(namespace, block, blockSuffix, '', '');
// 生成元素 class,例如 el-button__icon
const e = (element?: string) =>
element ? _bem(namespace, block, '', element, '') : '';
// 生成修饰符 class,例如 el-button--primary
const m = (modifier?: string) =>
modifier ? _bem(namespace, block, '', '', modifier) : '';
// 生成 块+元素 class,例如 el-button-group__icon
const be = (blockSuffix?: string, element?: string) =>
blockSuffix && element
? _bem(namespace, block, blockSuffix, element, '')
: '';
// 生成 元素+修饰符 class,例如 el-button__icon--active
const em = (element?: string, modifier?: string) =>
element && modifier ? _bem(namespace, block, '', element, modifier) : '';
// 生成 块+修饰符 class,例如 el-button-group--active
const bm = (blockSuffix?: string, modifier?: string) =>
blockSuffix && modifier
? _bem(namespace, block, blockSuffix, '', modifier)
: '';
// 生成 块+元素+修饰符 class,例如 el-button-group__icon--active
const bem = (blockSuffix?: string, element?: string, modifier?: string) =>
blockSuffix && element && modifier
? _bem(namespace, block, blockSuffix, element, modifier)
: '';
/**
* 将一组 key-value 转换为 css 变量(全局级别)
* @example { color: 'red' } => { '--el-color': 'red' }
*/
const cssVar = (object: Record<string, string>) => {
const styles: Record<string, string> = {};
for (const key in object) {
if (object[key]) {
styles[`--${namespace}-${key}`] = object[key];
}
}
return styles;
};
/**
* 将一组 key-value 转换为 css 变量(块级别)
* @example { color: 'red' } => { '--el-button-color': 'red' }
*/
const cssVarBlock = (object: Record<string, string>) => {
const styles: Record<string, string> = {};
for (const key in object) {
if (object[key]) {
styles[`--${namespace}-${block}-${key}`] = object[key];
}
}
return styles;
};
// 获取全局变量名,例如 '--el-color'
const cssVarName = (name: string) => `--${namespace}-${name}`;
// 获取块级变量名,例如 '--el-button-color'
const cssVarBlockName = (name: string) => `--${namespace}-${block}-${name}`;
return {
b, // block
e, // element
m, // modifier
be, // block + element
em, // element + modifier
bm, // block + modifier
bem, // block + element + modifier
is, // state helper
cssVar, // 全局 CSS 变量
cssVarBlock, // 块级 CSS 变量
cssVarName,
cssVarBlockName,
namespace,
};
};
// 类型导出:用于 TS 类型推导
type UseNamespaceReturn = ReturnType<typeof useNamespace>;
export type { UseNamespaceReturn };
export { useNamespace };
第一章 引言
1.1 组件库开发中的命名难题
1.1.1 命名混乱带来的问题
在组件库开发过程中,命名混乱就像是在一个杂乱无章的仓库里找东西,让人头疼不已😣。以下是命名混乱可能带来的一些严重问题:
- 样式冲突:当不同组件使用了相同的类名时,样式就会相互覆盖或干扰。例如,在一个组件库中,有两个不同功能的按钮组件,开发者不小心都命名为 "button" 类。那么在应用样式时,就可能会出现一个按钮的样式影响到另一个按钮的情况,导致界面显示异常。
- 代码可读性差:如果命名没有规律,代码就会变得像一团乱麻。其他开发者在阅读和维护代码时,很难理解每个类名所代表的含义。比如,类名 "a123" 或者 "xyz" 这样毫无意义的命名,让人完全摸不着头脑,增加了开发和维护的成本。
- 可维护性降低:随着组件库的不断发展和更新,命名混乱会让后续的修改和扩展变得异常困难。当需要对某个组件的样式或功能进行调整时,由于命名不清晰,很难准确找到相关的代码,甚至可能会误改其他组件的代码,引发一系列的问题。
1.1.2 样式可维护性的重要性
样式可维护性就像是给组件库建立了一个清晰的地图🗺️,让开发者能够轻松地找到和修改需要的样式。以下是其重要性的体现:
- 提高开发效率:当样式具有良好的可维护性时,开发者可以快速定位和修改样式,减少了查找和调试的时间。例如,在一个大型的组件库中,如果按照一定的规则对样式进行命名和组织,开发者可以根据命名规则快速找到某个组件的样式代码,提高了开发效率。
- 保证代码质量:可维护的样式代码结构清晰,逻辑明确,减少了出现错误的可能性。同时,也便于进行代码审查和测试,有助于保证组件库的质量。
- 适应业务变化:随着业务的发展和变化,组件库需要不断进行更新和扩展。良好的样式可维护性使得开发者能够轻松地对样式进行修改和调整,以适应新的业务需求。
1.2 BEM 命名规范的意义
1.2.2 BEM 规范简介
BEM 是 Block(块)、Element(元素)、Modifier(修饰符)的缩写,它是一种前端命名方法论,就像是给代码制定了一套统一的"语言规则"📖。以下是对 BEM 各部分的详细解释:
- Block(块):表示一个独立的实体,具有语义化的名称。它可以是一个组件、一个页面模块等。例如,一个登录表单可以看作是一个块,命名为 "login-form"。
- Element(元素):是块的组成部分,依赖于块存在。元素的命名通过双下划线 "__" 与块名连接。比如,在 "login-form" 块中,用户名输入框可以命名为 "login-form__username-input"。
- Modifier(修饰符):用于表示块或元素的不同状态或变体。修饰符的命名通过双连字符 "--" 与块名或元素名连接。例如,登录按钮有禁用状态,那么可以命名为 "login-form__submit-button--disabled"。
1.2.2 BEM 规范对组件库的价值
BEM 规范就像是组件库的"守护神",为组件库带来了很多重要的价值:
- 避免样式冲突:BEM 规范通过独特的命名方式,确保每个类名都是唯一的。每个块、元素和修饰符的组合形成了一个特定的类名,大大降低了样式冲突的风险。例如,不同组件的按钮元素可以通过块名进行区分,如 "header__button" 和 "footer__button",避免了样式的相互干扰。
- 提高代码可读性:BEM 命名具有很强的语义化,从类名就可以清晰地看出组件的结构和用途。开发者可以很容易地理解代码的含义,提高了代码的可读性和可维护性。例如,"product-card__title--highlighted" 这个类名,一眼就能看出它是产品卡片的标题,并且处于高亮状态。
- 便于团队协作:在团队开发中,BEM 规范提供了统一的命名标准,使得不同开发者编写的代码风格一致。大家可以按照相同的规则进行命名和开发,减少了沟通成本,提高了团队协作的效率。
1.3 useNamespace 工具函数的引入
1.3.1 useNamespace 的作用概述
useNamespace 工具函数就像是一个智能的命名助手🤖,它可以帮助我们更方便地使用 BEM 命名规范。其主要作用如下:
- 简化命名过程:在使用 BEM 规范时,手动编写复杂的类名会比较繁琐。useNamespace 函数可以自动生成符合 BEM 规范的类名,减少了开发者的工作量。例如,通过传入块名,它可以自动生成包含元素和修饰符的类名。
- 提高代码的可维护性:使用 useNamespace 函数可以将命名逻辑封装起来,使得代码更加简洁和易于维护。当需要修改命名规则时,只需要修改函数内部的实现,而不需要在整个代码中进行查找和替换。
- 增强代码的灵活性:useNamespace 函数可以根据不同的条件动态生成类名,使得组件的样式可以根据不同的状态进行灵活调整。例如,根据组件的禁用状态动态添加 "--disabled" 修饰符。
1.3.2 它在组件库中的应用场景
useNamespace 函数在组件库中有很多实用的应用场景:
- 组件开发:在开发组件时,使用 useNamespace 函数可以方便地为组件的各个部分生成符合 BEM 规范的类名。例如,在开发一个模态框组件时,可以使用 useNamespace 为模态框的标题、内容、关闭按钮等元素生成类名,如 "modal__title"、"modal__content"、"modal__close-button"。
- 样式定制:当需要对组件的样式进行定制时,useNamespace 函数可以帮助我们根据不同的需求生成不同的类名。例如,为组件添加不同的主题样式,通过 useNamespace 生成带有主题修饰符的类名,如 "button--primary"、"button--secondary"。
- 状态管理:在处理组件的不同状态时,useNamespace 函数可以根据状态动态生成类名。例如,当按钮处于加载状态时,生成 "button--loading" 类名,方便为加载状态添加特定的样式。
第二章 useNamespace 函数的代码剖析
2.1 常量与基础函数
2.1.1 DEFAULT_NAMESPACE 常量
DEFAULT_NAMESPACE
常量就像是一个"默认指挥官"😎,它为整个命名空间设定了一个基础的默认值。当我们在使用 useNamespace
函数时,如果没有特别指定命名空间,就会使用这个 DEFAULT_NAMESPACE
常量的值。例如:
javascript
const DEFAULT_NAMESPACE = 'el';
这里的 'el'
就是默认的命名空间,后续生成的类名等可能就会基于这个 'el'
来展开。
2.1.2 statePrefix 状态前缀
statePrefix
状态前缀就像是给状态类名加上的一个"小标签"🏷️,用于标识这是一个状态相关的类名。它通常是一个固定的字符串,比如:
javascript
const statePrefix = 'is-';
有了这个前缀,我们就可以很容易地区分普通类名和状态类名。比如 is-disabled
就表明这是一个表示"禁用"状态的类名。
2.1.3 _bem 字符串生成函数
2.1.3.1 _bem 函数的参数解析
_bem
函数就像是一个"魔法工厂"🧙♂️,它接收几个重要的参数来生成特定格式的字符串。一般来说,它可能接收以下几个参数:
- block :这是"块"的名称,是整个命名的基础部分,就像是一座大楼的地基🏗️。例如在
el-button
中,el
是命名空间,button
就是block
。 - element :这是"元素"的名称,用于表示块中的某个具体元素,就像是大楼里的一个个房间🏠。比如
el-button__icon
中的icon
就是element
。 - modifier :这是"修饰符"的名称,用于对块或元素进行进一步的描述,就像是给房间贴上不同的装饰标签🎨。比如
el-button--primary
中的primary
就是modifier
。
2.1.3.2 _bem 函数的生成逻辑
_bem
函数会根据传入的参数,按照一定的规则生成字符串。它的生成逻辑大致如下:
- 如果只传入了
block
,就直接返回block
加上命名空间的字符串,比如el-button
。 - 如果传入了
block
和element
,就会用双下划线__
连接它们,生成block__element
的形式,如el-button__icon
。 - 如果传入了
block
和modifier
,就会用双横线--
连接它们,生成block--modifier
的形式,如el-button--primary
。 - 如果传入了
block
、element
和modifier
,就会用双下划线和双横线组合起来,生成block__element--modifier
的形式,如el-button__icon--large
。
以下是一个简单的伪代码示例:
javascript
function _bem(block, element = '', modifier = '') {
let result = block;
if (element) {
result += `__${element}`;
}
if (modifier) {
result += `--${modifier}`;
}
return result;
}
2.2 is 函数:生成状态类名
2.2.2.1 is 函数的重载定义
is
函数就像是一个"状态化妆师"💄,它可以根据不同的情况生成不同的状态类名。它可能会有不同的重载定义,以适应不同的参数类型。例如:
- 当传入一个布尔值和一个状态名称时,如果布尔值为
true
,就生成带有statePrefix
的状态类名;如果为false
,则返回null
或空字符串。
javascript
function is(condition, stateName) {
if (condition) {
return `${statePrefix}${stateName}`;
}
return '';
}
- 还可能有其他的重载形式,比如处理更复杂的情况。
2.2.2 is 函数的使用示例
javascript
const isDisabled = true;
const disabledClass = is(isDisabled, 'disabled');
console.log(disabledClass); // 输出: 'is-disabled'
这里通过 is
函数,根据 isDisabled
的值生成了相应的状态类名。
2.3 useNamespace 核心 Hook
2.3.1 useNamespace 的参数与命名空间
useNamespace
函数就像是一个"命名空间管理员"👨💼,它接收一个参数来指定命名空间。如果没有传入参数,就会使用 DEFAULT_NAMESPACE
。例如:
javascript
function useNamespace(namespace = DEFAULT_NAMESPACE) {
// 函数内部逻辑
}
这里的 namespace
就是我们后续生成各种类名和 CSS 变量的基础。
2.3.2 生成不同类型 className 的子函数
2.3.2.1 b 函数:生成块 class
b
函数就像是一个"块生成器"🧱,它用于生成块级的类名。它会基于传入的命名空间和块名称来生成类名。例如:
javascript
function b(block) {
return `${namespace}-${block}`;
}
调用 b('button')
可能会返回 el-button
。
2.3.2.2 e 函数:生成元素 class
e
函数就像是一个"元素定位器"📍,它用于生成元素级的类名。它会结合块名和元素名,用双下划线连接。例如:
javascript
function e(element) {
return `${namespace}-${block}__${element}`;
}
调用 e('icon')
可能会返回 el-button__icon
。
2.3.2.3 m 函数:生成修饰符 class
m
函数就像是一个"修饰符添加器"🎊,它用于生成带有修饰符的类名。它会结合块名和修饰符名,用双横线连接。例如:
javascript
function m(modifier) {
return `${namespace}-${block}--${modifier}`;
}
调用 m('primary')
可能会返回 el-button--primary
。
2.3.2.4 be 函数:生成块 + 元素 class
be
函数就像是一个"块元素组合器"🧲,它会同时生成块和元素的类名。例如:
javascript
function be(block, element) {
return `${namespace}-${block}__${element}`;
}
调用 be('button', 'icon')
会返回 el-button__icon
。
2.3.2.5 em 函数:生成元素 + 修饰符 class
em
函数就像是一个"元素修饰符融合器"💫,它会生成元素和修饰符组合的类名。例如:
javascript
function em(element, modifier) {
return `${namespace}-${block}__${element}--${modifier}`;
}
调用 em('icon', 'large')
可能会返回 el-button__icon--large
。
2.3.2.6 bm 函数:生成块 + 修饰符 class
bm
函数就像是一个"块修饰符搭配师"👗,它会生成块和修饰符组合的类名。例如:
javascript
function bm(block, modifier) {
return `${namespace}-${block}--${modifier}`;
}
调用 bm('button', 'primary')
会返回 el-button--primary
。
2.3.2.7 bem 函数:生成块 + 元素 + 修饰符 class
bem
函数就像是一个"超级组合大师"🌟,它会生成包含块、元素和修饰符的完整类名。例如:
javascript
function bem(block, element, modifier) {
return `${namespace}-${block}__${element}--${modifier}`;
}
调用 bem('button', 'icon', 'large')
会返回 el-button__icon--large
。
2.3.3 生成 CSS 变量的子函数
2.3.3.1 cssVar 函数:全局 CSS 变量转换
cssVar
函数就像是一个"全局 CSS 变量转换器"🔄,它会将传入的名称转换为全局 CSS 变量的格式。例如:
javascript
function cssVar(name) {
return `--${namespace}-${name}`;
}
调用 cssVar('color')
可能会返回 --el-color
。
2.3.3.2 cssVarBlock 函数:块级 CSS 变量转换
cssVarBlock
函数就像是一个"块级 CSS 变量定制师"🧑🎨,它会结合块名生成块级的 CSS 变量。例如:
javascript
function cssVarBlock(block, name) {
return `--${namespace}-${block}-${name}`;
}
调用 cssVarBlock('button', 'background-color')
可能会返回 --el-button-background-color
。
2.3.4 获取 CSS 变量名的子函数
2.3.4.1 cssVarName 函数:全局变量名获取
cssVarName
函数就像是一个"全局变量名探测器"🔍,它用于获取全局 CSS 变量的名称。例如:
javascript
function cssVarName(name) {
return `${namespace}-${name}`;
}
调用 cssVarName('color')
会返回 el-color
。
2.3.4.2 cssVarBlockName 函数:块级变量名获取
cssVarBlockName
函数就像是一个"块级变量名定位器"📌,它用于获取块级 CSS 变量的名称。例如:
javascript
function cssVarBlockName(block, name) {
return `${namespace}-${block}-${name}`;
}
调用 cssVarBlockName('button', 'background-color')
会返回 el-button-background-color
。
2.3.5 useNamespace 的返回值
2.3.5.1 返回值的组成与作用
useNamespace
函数的返回值就像是一个"工具包"🛠️,它包含了前面提到的各种子函数,方便我们在不同的地方使用。返回值可能是一个对象,包含 b
、e
、m
等函数。这些函数可以帮助我们快速生成类名和 CSS 变量名。
2.3.5.2 各返回值的使用示例
javascript
const ns = useNamespace();
const blockClass = ns.b('button');
const elementClass = ns.e('icon');
const modifierClass = ns.m('primary');
const cssVarName = ns.cssVarName('color');
console.log(blockClass); // 输出: 'el-button'
console.log(elementClass); // 输出: 'el-button__icon'
console.log(modifierClass); // 输出: 'el-button--primary'
console.log(cssVarName); // 输出: 'el-color'
通过 useNamespace
返回的"工具包",我们可以轻松地生成所需的类名和 CSS 变量名。
第三章 useNamespace 的类型导出与总结
3.1 类型导出:UseNamespaceReturn
3.1.1 类型导出的作用
类型导出在编程中就像是给代码世界建立了一套清晰的"说明书"📖。当我们导出 UseNamespaceReturn
类型时,它为其他开发者提供了明确的使用指导。
- 增强代码可读性 :在大型项目中,代码往往非常复杂。通过导出类型,我们可以让其他开发者一眼就明白
UseNamespaceReturn
这个类型所代表的含义和用途。例如,在一个组件库中,如果有多个组件都使用了useNamespace
函数并返回UseNamespaceReturn
类型,开发者在阅读代码时就能迅速理解这些组件在命名空间处理上的逻辑。 - 提高代码可维护性 :当项目需要进行修改或扩展时,明确的类型导出可以减少错误的发生。如果没有类型导出,开发者可能会因为不清楚返回值的具体结构而误操作。而有了
UseNamespaceReturn
类型导出,开发者可以根据类型定义来正确使用返回值,避免了很多潜在的问题。 - 促进团队协作:在团队开发中,不同的开发者负责不同的模块。类型导出就像是一种通用的"语言",让团队成员之间的沟通更加顺畅。大家可以根据统一的类型定义来进行开发,减少了因为理解不一致而产生的冲突。
3.1.2 在 TS 类型推导中的应用
TypeScript(TS)的类型推导功能就像是一个智能的"侦探"🕵️,它可以根据代码的上下文自动推断出变量的类型。而 UseNamespaceReturn
类型导出在 TS 类型推导中有着重要的应用。
- 自动类型推断 :当我们在代码中使用
useNamespace
函数并将返回值赋值给一个变量时,TS 可以根据UseNamespaceReturn
类型导出自动推断出这个变量的类型。例如:
typescript
import { useNamespace } from './namespace';
const ns = useNamespace('button');
// TS 会根据 UseNamespaceReturn 类型导出自动推断出 ns 的类型
- 类型安全检查 :TS 会根据
UseNamespaceReturn
类型定义对使用该类型的代码进行严格的类型检查。如果我们在使用ns
变量时,调用了一个不存在于UseNamespaceReturn
类型中的属性或方法,TS 会立即给出错误提示,帮助我们及时发现并纠正代码中的错误。 - 代码补全 :在现代的开发工具中,如 Visual Studio Code,类型导出可以实现代码补全功能。当我们输入
ns.
时,开发工具会根据UseNamespaceReturn
类型定义自动列出所有可用的属性和方法,大大提高了开发效率。
3.2 useNamespace 总结
3.2.1 功能回顾
useNamespace
函数就像是一个"命名空间魔法师"🧙,它主要用于处理组件的命名空间。
- 生成命名空间 :
useNamespace
可以根据传入的组件名称生成唯一的命名空间。例如,当我们调用useNamespace('button')
时,它会生成一个与button
组件相关的命名空间,确保组件的类名、样式等在整个项目中具有唯一性。 - 处理类名前缀 :它可以为组件的类名添加统一的前缀,使组件的样式更加模块化和可管理。比如,通过
useNamespace
生成的类名可能会是el-button
这种形式,其中el-
就是统一的前缀。 - 提供便捷的类名生成方法 :
useNamespace
还提供了一些方法来方便地生成不同状态下的类名。例如,我们可以通过它生成激活状态的类名el-button--active
,简化了类名的生成过程。
3.2.2 对组件库开发的重要性
在组件库开发中,useNamespace
就像是一根"定海神针"⚓,发挥着至关重要的作用。
- 保证命名空间的唯一性 :在一个大型的组件库中,可能会有很多不同的组件。如果没有
useNamespace
来处理命名空间,很容易出现类名冲突的问题。而useNamespace
可以确保每个组件都有自己独立的命名空间,避免了样式的相互干扰。 - 提高组件的可复用性:通过统一的命名空间和类名前缀,组件在不同的项目中可以更加方便地复用。其他开发者在使用组件库时,不需要担心命名冲突的问题,只需要按照组件的使用说明来使用即可。
- 便于样式管理 :
useNamespace
使得组件的样式更加模块化,开发者可以更加方便地对组件的样式进行管理和修改。例如,如果需要修改某个组件的样式,只需要关注该组件命名空间下的样式规则即可,不会影响到其他组件。
3.2.3 未来的应用与拓展可能性
useNamespace
具有很大的未来应用和拓展潜力,就像是一片等待开发的"新大陆"🌎。
- 支持更多的命名规则 :未来可以对
useNamespace
进行扩展,使其支持更多的命名规则。例如,除了现有的类名前缀规则,还可以支持自定义的命名规则,满足不同项目的需求。 - 与其他工具集成 :可以将
useNamespace
与其他前端开发工具进行集成,如 CSS 预处理器、构建工具等。这样可以进一步提高开发效率,实现更加自动化的命名空间处理。 - 应用于新的前端框架 :随着前端技术的不断发展,新的前端框架不断涌现。
useNamespace
可以尝试应用于这些新的框架中,为新框架的组件开发提供命名空间处理的解决方案。