一、前言
css方案有很多,原生css以及传统的bootstrap框架、less、css in js,sass,以及原子化css Tailwind CSS以及unocss,最近两年盛行的原子化css的一些方案,个人认为本质上和bootstrap很相似,今天我们要来研究的是element plus组件库的css架构方案useNamespace。
二、不同css方案的适应场景以及优缺点
2.1 原生css以及传统的bootstrap框架
原生css以及bootstrap适应于以前前后端不分离时代,现在基本不用了。
2.2 less,sass等预处理器
这些预处理器封装了很多内置方法,在很多前端项目都会用到,相比较传统的css方案,他集成了变量、混入、嵌套、函数等特性,可以化繁为简,提升开发效率
2.3 原子化css方案
原子化css,诸如Tailwind CSS以及unocss,它可以帮助我们省去思考class名字的时间,与此同时,它的打包体积会减少一些,这里肯定会有人说,打包体积其实也就少了几kb,而且作为一个开发,给class取一个语意化名字也是基本的职责。原子化css的思想其实和bootstrap差不多,其实没有那么多新技术,无非都是站在巨人的肩膀上。
2.4 css in js
css in js这种方案在react里面比较常见,ant design源码底层也是用的css in js方案。
2.5 组件库css方案
不同组件库的css方案各有差异,其中element plus的css方案尤为特殊,element plus底层封装了一个hooks,接下来我们一起研究下element plus的技术方案。
三、useNamespace技术方案
这里我们从element plus的一个组件为切入点
如果所示,我们可以看到核心源码在button.vue中,其中style里面的css文件是用来按需引入用的,在button.vue里面可以看到调用了useNamespace这个方法
接下来我们细看这个源码
b代表block,用于生成块(Block)级别的类名
e代表element,用于生成元素(Element)级别的类名
m用于生成修饰符(Modifier)级别的类名
be用于同时生成块级后缀和元素级别的类名
em用于同时生成元素级别和修饰符级别的类名
bm用于同时生成块级后缀和修饰符级别的类名
is用于根据状态生成状态类名。
export const useNamespace = (
block: string,
namespaceOverrides?: Ref<string | undefined>
) => {
const namespace = useGetDerivedNamespace(namespaceOverrides)
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)
: ''
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,
em,
bm,
bem,
is,
// css
cssVar,
cssVarName,
cssVarBlock,
cssVarBlockName,
}
}
我们再细看下button组件里面对应的css样式,可以改文件中,结合sass的函数,它利用了Sass的高级功能,如变量、条件语句(mixins)、循环和函数,来创建一个可配置和可重用的按钮样式系统,为bem模式下不同类型类名封装了不同的方法,使用@include when()混合宏为按钮添加了特殊样式
总结
element plus是为数不多的将css bem架构落入到实践中的组件库,一方面可以实现css模块化以及命名清晰,更重要的是,这可以规避样式覆盖,尤其在开发和维护大型和复杂的前端项目变得更加容易和高效。