pid: 103585053
前言
相信大伙在写组件库项目时,大多数选择的都会是button组件,因为他很简单很基础。但也正因为他简单,我一直在思考应该如何写这篇文章才能让它有参考价值。既然button组件的js逻辑很简单,那么就从它的css样式入手,本篇文章主要介绍如何规范化书写css, 使得代码更加简化、语义化。
效果展示: Button 按钮 | SSS UI Plus (4everland.app) 可能会报错不安全,因为托管方被举报了:( 不过请放心,这只是纯文档的静态网站。
正文
先设想自己是使用组件库的开发者,当使用某一个组件时,组件往往会有一些默认样式, 而开发者通常会通过添加class类来修改默认样式。
这就要求我们写的样式优先级尽量低,能够被开发者自定义样式轻易覆盖、并且要保证组件样式不会影响全局。为此组件的样式要做到通过单个类来控制,比如以下css样式是合理的:
less
.s-button{
//css规则集
}
.s-button--primary{
//css规则集
}
而应该尽量减少以下css样式:
less
//非法,不应该出现会影响全局的样式
button{
//css规则集
}
//不合理, 组件样式不能采用常用单个单词作为类名, 需要加上特定前缀
.container{
//css规则集
}
//尽量避免, 这样会抬高样式的优先级, 导致开发者不能轻易覆盖这些css规则集
//在下文当中会提供避免方案
.s-button--primary .s-button--empty{
//css规则集
}
约定
为了简化说明,假设button需要实现的有:
-
可以指定不同的类型(type),比如primary、success、info、danger、warning...
具体表现为:
-
可以指定为虚幻(fantasy)按钮、幽灵(ghost)按钮、空(emoty)按钮
具体表现为:
- 可以指定为圆形(round)按钮或者圆角(circle)按钮
同类型prop合并
对于上述需要实现的效果, 当然需要通过props让用户选择,但能很明显的注意到:不同的表现(fantasy、ghost、empty)应该处于同一个prop, 不同的状态(round、circle)也应该处于同一个prop。
要避免用户写出这样的代码:
bash
<s-button type="primary" ghost fantasy round circle></s-button>
可以将某些明显是互斥的props合并为同一个prop避免冲突:
ts
//仅提供部分props用于展示说明
export const SButtonProps = {
/**
* @description 按钮的类型, 主要控制颜色
*/
type: String as PropType<buttonTypes>,
/**
* @description 按钮的主题, 主要控制交互上的表现
*/
variant:String as PropType<'ghost' | 'fantasy' | 'empty'>,
/**
* @description 按钮的状态, 主要控制外观
*/
status:String as PropType<'round' | 'circle'>,
} as const
遵循BEM规范
BEM规范非常适合用在组件库的项目中, 在避免全局样式冲突的情况下还使得类名具有实际意义,便于调试。
BEM(block、element、modifier)规范其实就是一种命名规范,一般情况下:
- 一个组件就是一个block(在vue中通常是template中的根元素),通常block不会单个单词,单词直接可以使用
-
进行链接, 比如s-message-box
- 组件内部的元素则是一个个element, 使用
__element
链接元素 - modifier则表示修饰符,使用
--modifier
链接修饰符
当然,一个组件内部可以不止一个block,比如某些html结构较为复杂的element可以单独命名为一个block。例如:s-message-header
, s-message-body
。(block element之间不是绝对的)
第一版template:
vue
<template>
<button
:class="[
's-button',
props.type? `s-button--${props.type}`:'',
props.variant? `s-button--${props.variant}`:'',
props.status? `s-button--${props.status}`:'',
{
'is-disabled':props.disabled
}
]"
>
<s-icon class="s-button__icon s-button__icon--prefix"></s-icon>
<slot></slot>
<s-icon class="s-button__icon s-button__icon--suffix"></s-icon>
</button>
</template>
egs:
html
<s-button
type="primary"
status="round"
variant="fantasy"
>
button 1
</s-button>
//将会被翻译为:
<button class="s-button s-button--primary s-button--fantasy s-button--round">
<label class="s-button__icon s-button__icon--prefix" target="edit"></label>
button 1
<label class="s-button__icon s-button__icon--suffix" target="edit"></label>
</button>
第二版template
第一版效果上符合了BEM规范,但有什么问题呢?
-
大量重复代码一定会带来维护上的不便:例如我组件前缀被迫修改,组件名字修改等操作,将会导致上述代码狠狠滴修改(血泪史啊!)
-
类名不直观: 例如下面一段代码,可能需要观察半天才能看明白
less:class="[ 's-button', props.type? `s-button--${props.type}`:'', props.variant? `s-button--${props.variant}`:'', props.status? `s-button--${props.status}`:'', { 'is-disabled':props.disabled }]"
为此我们引入命名空间的概念, 并通过hook实现它,先来看如何使用;
vue
<template>
<button
:class="kls"
>
<s-icon
:class="[ns.e('icon'), ns.em('icon', 'prefix')]"
>
</s-icon>
//其余元素省略
</button>
</template>
<script setup lang="ts">
import {useNS} from "@sss-ui-plus/hooks/useNS";
import {SButtonProps} from "@sss-ui-plus/components/SButton/src/button";
import {computed} from "vue";
defineOptions({
name: "s-button",
inheritAttrs: false
})
const ns = useNS('button');
const props = defineProps({...SButtonProps})
const kls = computed(() => {
return [
ns.namespace,
ns.m(props.type),
ns.m(props.size),
ns.m(props.variant),
ns.is(props.status),
ns.is(props.disabled, 'disabled'),
ns.is(props.loading, 'loading'),
]
})
</script>
上述代码中useNS('button')
- ns.namespace: 表示block命名空间
- ns.m(modifer:string | undifined): 返回modifier修饰后的类名
- ns.is: 根据参数不同实现不同效果,但最后都会返回类似于
is-disabled is-round
这样的类名
第二版相比于第一版不但避免了重复代码,而且类名命名上更加贴近BEM规范,更容易理解。
useNS的实现
useNS目的是为了简化代码,同时让代码更加语义化, 其实现无非是根据BEM规范完成字符串的拼接:
javascript
const componentPrefix = 's';
const variablePrefix = '--sss'
const statusPrefix = 'is';
export const useNS = function (name: string) {
const namespace = `${componentPrefix}-${name}`;
const b = (block?: string | number) => {
return block ? `${namespace}-${block}` : '';
}
const e = (element?: string | number) => {
return element ? `${namespace}__${element}` : '';
}
const m = (modifier?: string | number) => {
return modifier ? `${namespace}--${modifier}` : '';
}
const is: IS = (status?: boolean | string, s?: string | boolean) => {
if (isString(status) && isBoolean(s)) {
return s ? `${statusPrefix}-${status}` : '';
}
if (isString(status)){
return `${statusPrefix}-${status}`;
}
if (isBoolean(status) && isString(s)){
if (status){
return `${statusPrefix}-${s}`;
}
}
return '';
}
//other func...
return {
namespace,
b,
e,
m,
is,
}
}
除此之外,你还可以实现ns.bm、ns.be, ns.em ...之类套娃操作。总之遵循BEM规范进行字符串拼接就行。
tips: 既然需要ns.bm、ns.bem之类套娃操作, 你也可以考虑将useNS写成链式函数,将ns.bem简化为ns.b().e().m()这样语义化更优的方案!
如何书写less样式文件
下列less样式文件只用于说明用,可能不会写实际的样式
前面已经指出了,大量重复的会造成维护上的困难, 且我们的目的是为了写出简化、语义化的代码。我们不应该写出类似于这样的代码:
sql
@import "../mixn/util";
.s-button{
//css规则集
}
.s-button--primary{
.s-button--ghost{
//css规则集
}
.s-button--fantasy{
}
.s-button--empty{
}
}
为此我们要善于利用less的特性,先给出部分最终形态的代码:
less
@import "../../styles/mixn/_index";
@plugin "../../styles/plugin/index";
.@{componentPrefix}-button {
@ns: @{componentPrefix}-button;
@typeList: primary, success, info, warning, danger, cyan;
@themeList: ghost, fantasy, empty;
//var
& {
--sss-button-font-color: var(--sss-color-black);
--sss-button-bg-color: var(--sss-color-bg);
--sss-button-br-color: var(--sss-color-gray);
}
//base
& {
color: var(--sss-button-font-color);
background-color: var(--sss-button-bg-color);
border-color: var(--sss-button-br-color);
&:hover,
&:focus {
--sss-button-font-color: var(--sss-color-primary);
}
&:hover {
--sss-button-bg-color: var(--sss-color-gray-deep-fade);;
}
&:active {
--sss-button-br-color: var(--sss-color-primary);
}
}
//modifier types
each(@typeList, .(@type) {
.m(@type, {
--sss-button-font-color: getClrVar(white);
--sss-button-bg-color: getClrVar(@type);
&:hover,
&:focus {
--sss-button-font-color: getClrVar(white);
--sss-button-bg-color: getClrVar(@type, light);
}
&:active {
--sss-button-bg-color: getClrVar(@type, dark);
}
})
})
//modifier empty theme
.m(empty, {
--sss-button-bg-color: var(--sss-color-bg);
&:hover,
&:focus {
--sss-button-font-color: var(--sss-color-black);
--sss-button-bg-color: var(--sss-color-bg);
}
&:active {
--sss-button-br-color: var(--sss-color-gray-dark)
}
each(@typeList, .(@type) {
.with(@ns, {
.m(@type, {
--sss-button-bg-color: var(--sss-color-bg);
--sss-button-font-color: getClrVar(@type);
--sss-button-br-color: var(--sss-color-gray);
&:active {
--sss-button-br-color: getClrVar(@type, dark);
}
});
});
});
});
}
需要注意的只有两点: 利用css变量完成动态属性的提升, 利用less混合和变量完成代码简化和语义化。
动态css属性提升
为何需要这样做?
现在假设一个button, 默认情况下button的背景颜色为:
less
.s-button{ background-color:var(--sss-color-white); }
当设置button类型为primary时,背景颜色为priamry:
less
.s-button--primary{ background-color:var(--sss-color-primary); }
当设置button表现为fantasy时, 背景色淡化20%:
less
.s-button--fantasy{ background-color:lighten(var(--sss-color-white), 20%); }
当同时设置类型为primary, 表现为fantasy时,背景为primary淡化20%:
less
.s-button--primary .s-button--fantasy { background-color:lighten(var(--sss-color-primary), 20%); }
假设用户在使用button时,同时设置了类型和表现两个props, 那么势必会使用.s-button--type .s-button-variant {}
规则集的结果。 如果用户不满意这个颜色而想要自定义按钮背景色时, 单独使用一个class来尝试修改样式,但由于样式权重的问题,会导致修改失效。
为了解决这个问题,我们统一将这类会改变的样式使用css变量代替:
css
.s-button{
--sss-button-bg-color:var(--sss-button-bg-color);
background-color: var(--sss-button-bg-color);
}
.s-button--primary{
--sss-button-bg-color: var(--sss-color-primary);
&.s-button--fantasy{
--sss-button-bg-color: var(--sss-color-primary-light);
}
}
这样一来,真正设置按钮背景颜色的是在.s-button{}
选择器中, 而.s-button--type .s-button--variant {}
修改的只是css变量。用户通过单一的class也能修改按钮的默认样式。
有时你在使用某些ui组件库时,会发现该ui组件库的组件跟节点上面绑定了许多css变量,比如naive-ui:
这也是为了降低某些样式的权重,比如假设按钮的背景颜色是通过props.color设置的, 那么我可以:
ts
const sdl = computed(() => {
return{
[ns.cssVar('bg-color')]: props.color
}
})
然后将sdl变量绑定到组件根元素的style上,这样一来,用户也可以通过单一class来改变按钮背景颜色!
在less中写出符合BEM规范的代码
首先明确一点,组件的所有样式文件都是统一存放的,由index.less文件引入, 由于less本身对变量的处理,不同文件下的同名变量会在less编译期间整合到一起变成同一个变量,因此在less中一定要少使用全局变量.
简化上文最终代码为:
less
@import "../../styles/mixn/_index";
@plugin "../../styles/plugin/index";
.@{componentPrefix}-button {
@ns: @{componentPrefix}-button;
@typeList: primary, success, info, warning, danger, cyan;
//var 存放根节点css变量
& {}
//base 基础样式
& {}
//modifier types 不同类型下的样式
each(@typeList, .(@type) {
.m(@type, {//...})
})
//modifier empty theme empty表现下的样式
.m(empty, {
//...
//empty 和 type同时存在下的样式
each(@typeList, .(@type) { e
.with(@ns, {
.m(@type, {//...});
});
});
});
}
需要注意的点也就是 .m(), .with() .d() ...这类混合的实现了
在往期文章中,详细介绍了less的一些高级用法;less深入指南 - 掘金 (juejin.cn)
后记
总的来说,本篇文章主要是为了规范化有关css的书写。
组件库项目地址:lastertd/sss-ui-plus: 适用于vue3的组件库 (github.com)在这里求一个star✨
感谢看到最后💟💟💟