基类+修饰类

“基类 + 修饰类”是 CSS 工程化中最经典、最优雅的命名与组织模式。它源自BEM(Block Element Modifier)方法论,但在 Vue 3 组件化开发中被发挥到了极致。

你可以把这种模式理解为“乐高积木设计法”

  • 基类(Base):定义一块积木的“通用骨架”(尺寸、字体、基础颜色)。
  • 修饰类(Modifier):定义这块积木的“特定皮肤”“特殊状态”(颜色变红、尺寸变大、是否禁用)。

这样做最大的好处是:永远不需要写重复的样式代码,一个类只干一件事。


一、命名规范(BEM 标准写法)

在 CSS 中,我们通常用---来区分:

类型命名格式示例含义
基类(Block).block.btn按钮的基础样式(宽高、内边距、边框圆角)
修饰类(Modifier).block--modifier.btn--primary
.btn--large
主题色变为主色
尺寸变大
状态类(State).is-状态.block.is-状态.is-disabled
.is-active
临时状态(禁用、激活中),通常配合 JS 控制

二、原生 CSS 中的写法(如何组合)

假设我们要做一个按钮组件:

<!-- 1. 只写基类:默认灰色普通按钮 --> <button class="btn">默认按钮</button> <!-- 2. 基类 + 修饰类:变成蓝色大按钮 --> <button class="btn btn--primary btn--large">主要大按钮</button> <!-- 3. 基类 + 状态类:变成禁用状态 --> <button class="btn is-disabled" disabled>禁用按钮</button>

对应的 CSS 代码(利用层叠覆盖):

/* 基类:定义所有按钮共有的骨架 */ .btn { display: inline-block; padding: 8px 16px; font-size: 14px; border-radius: 4px; border: 1px solid #dcdfe6; background: #fff; color: #606266; cursor: pointer; transition: all 0.3s; } /* 修饰类:只改颜色(基类里有的属性,覆盖掉即可) */ .btn--primary { background: #409eff; border-color: #409eff; color: #fff; } /* 修饰类:只改尺寸 */ .btn--large { padding: 12px 24px; font-size: 16px; } /* 状态类:只改交互反馈 */ .is-disabled { opacity: 0.6; cursor: not-allowed; pointer-events: none; /* 禁止点击事件 */ }

三、🔥 Vue 3 中的最佳实践(动态绑定修饰类)

在 Vue 中,你极少会在模板里硬编码写死class="btn btn--primary"(因为这样就失去响应式意义了)。正确做法是利用:class动态绑定,将修饰类与组件的props或响应式状态关联起来。

1. 封装一个通用的BaseButton.vue组件

这是你在 Vite 项目中实际会写的代码:

<!-- src/components/BaseButton.vue --> <template> <button class="btn" :class="[ `btn--${type}`, // 动态主题(primary / success / warning) { 'is-disabled': disabled, [`btn--${size}`]: size // 动态尺寸(large / small) } ]" :disabled="disabled" > <slot /> </button> </template> <script setup lang="ts"> // 定义组件的 API,让父组件通过传参来控制样式 type ButtonType = 'default' | 'primary' | 'success' | 'warning'; type ButtonSize = 'default' | 'large' | 'small'; const props = withDefaults(defineProps<{ type?: ButtonType; size?: ButtonSize; disabled?: boolean; }>(), { type: 'default', size: 'default', disabled: false, }); </script> <style scoped> /* 基类 */ .btn { padding: 8px 16px; font-size: 14px; border-radius: 4px; border: 1px solid transparent; cursor: pointer; transition: all 0.3s; /* 默认样式(default) */ background: #fff; border-color: #dcdfe6; color: #606266; } .btn:hover:not(.is-disabled) { opacity: 0.8; } /* 修饰类:主题 */ .btn--primary { background: #409eff; border-color: #409eff; color: #fff; } .btn--success { background: #67c23a; border-color: #67c23a; color: #fff; } .btn--warning { background: #e6a23c; border-color: #e6a23c; color: #fff; } /* 修饰类:尺寸 */ .btn--large { padding: 12px 24px; font-size: 16px; } .btn--small { padding: 6px 12px; font-size: 12px; } /* 状态类 */ .is-disabled { opacity: 0.6; cursor: not-allowed; } </style>
2. 父组件中使用(干净得像写 HTML 属性):
<template> <!-- 默认灰色 --> <BaseButton>提交</BaseButton> <!-- 主要蓝色,大号 --> <BaseButton type="primary" size="large">提交</BaseButton> <!-- 警告色,禁用 --> <BaseButton type="warning" disabled>已过期</BaseButton> </template>

四、配合 CSS 预处理器(SCSS)的进阶写法

如果你在 Vite 中安装了sasspnpm add -D sass),可以用 SCSS 的&父选择器语法,让修饰类嵌套在基类内部,逻辑更集中:

.btn { padding: 8px 16px; // ... // 使用 & 代表 .btn,生成 .btn--primary &--primary { background: #409eff; color: #fff; } &--large { padding: 12px 24px; } // 状态类也嵌套进去,生成 .btn.is-disabled &.is-disabled { opacity: 0.6; cursor: not-allowed; } }

五、基类 + 修饰类 vs 原子化 CSS(Tailwind)

既然你是 Vite 项目,可能会听到“Tailwind CSS(原子类)”。这两种模式不冲突,而是互补:

模式基类 + 修饰类(BEM)原子化 CSS(Tailwind)
写法.btn--primary { background: blue; }class="bg-blue-500"
优点逻辑清晰,语义强,适合组件库内部开发极快,不用想类名,适合业务页面
缺点需要频繁在 HTML 和 CSS 间切换类名太长,可读性依赖工具提示

大厂最佳实践(Vue 3 生态)

  • 组件内部(如 Element Plus / Naive UI):用基类+修饰类,提供语义化的type/sizeProps。
  • 业务页面(App.vue 或具体页面):用原子类(配合unocsstailwind)快速搭建布局。

给你一个无敌的“避坑原则”

  1. 永远不要在基类里写死.btn { color: red; },把颜色留给修饰类。
  2. 状态类(.is-*)不要单独使用,必须配合基类一起出现(class="btn is-disabled"),因为 CSS 权重靠组合提升。
  3. 修饰类只覆盖基类中已有的属性,如果基类没写border-radius,修饰类里也不要写,除非你想新增特性。

现在你可以在你的 Vite 项目里试着封装一个BaseButton了,把之前学的transition:hoverbox-sizing都结合进去。封装完这个组件,你对 CSS 模块化的理解直接上一个台阶。

接下来想学transition过渡动画(让按钮渐变消失/出现),还是想攻克position定位(做吸顶导航、浮动客服按钮)?你来挑。