侧边栏壁纸
博主头像
洋洋的技术笔记

行动起来,活在当下

  • 累计撰写 13 篇文章
  • 累计创建 2 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录
VUE

Class与Style绑定

洋洋的技术笔记
2026-11-04 / 0 评论 / 0 点赞 / 2 阅读 / 0 字

Class与Style绑定

数据不仅决定页面显示什么,也决定页面以什么状态呈现。用 Class 和 Style 绑定表达状态,才能让交互反馈清晰、稳定且易于维护。

在上一篇文章中,我们通过事件处理与 v-model 让表单具备了交互能力。本文继续完善状态反馈,学习 Vue 如何动态管理元素类名和内联样式,并将它们应用到表单校验、按钮状态和列表项中。

写作约定:本系列以 Vue 3 <script setup> 为主要写法。示例默认使用 Vue 3.2+,重点理解“由状态计算样式”,而不是在事件处理器中直接修改 DOM。


一、为什么需要 Class 与 Style 绑定

原生 JavaScript 中,我们可以通过 classList 或 element.style 修改元素外观:

button.classList.toggle('disabled', !canSubmit)
message.style.color = isValid ? 'green' : 'red'

在 Vue 中,更推荐把类名和样式声明为响应式状态的结果:

<template>
  <button :class="{ disabled: !canSubmit }">提交</button>
  <p :style="{ color: isValid ? 'green' : 'red' }">校验结果</p>
</template>

: 是 v-bind: 的缩写。以下两种写法等价:

<button v-bind:class="{ active: isActive }">完整写法</button>
<button :class="{ active: isActive }">简写</button>

这种方式让模板直接表达“什么状态对应什么外观”。状态变化后,Vue 负责更新 DOM。


二、Class 的对象语法

2.1 单个动态类名

对象语法中,键是类名,值是布尔值或可以转换为布尔值的表达式。值为真时添加类名,为假时移除:

<template>
  <button :class="{ active: isActive }" @click="isActive = !isActive">
    切换状态
  </button>
</template>

<script setup>
import { ref } from 'vue'
const isActive = ref(false)
</script>

<style>
.active {
  color: #42b883;
  font-weight: 600;
}
</style>

2.2 同时绑定多个类名

一个对象可以包含多个类名:

<template>
  <div
    :class="{
      'status-card': true,
      'is-success': submitState === 'success',
      'is-error': submitState === 'error',
      'is-loading': submitState === 'loading'
    }"
  >
    提交状态
  </div>
</template>

<script setup>
import { ref } from 'vue'
const submitState = ref('idle')
</script>

包含连字符的类名必须加引号。始终存在的静态类名,通常直接写在 class 中,模板会更简洁:

<div
  class="status-card"
  :class="{
    'is-success': submitState === 'success',
    'is-error': submitState === 'error',
    'is-loading': submitState === 'loading'
  }"
>
  提交状态
</div>

Vue 会合并静态 class 与动态 :class,不会互相覆盖。

2.3 用计算属性整理复杂条件

当类名依赖多个字段时,不建议把完整判断堆进模板:

<template>
  <input
    :class="titleTouched && title.length === 0 ? 'invalid' : titleTouched ? 'valid' : ''"
    placeholder="标题"
  >
</template>

可以改为计算属性:

<template>
  <input v-model.trim="title" :class="titleClass" @blur="titleTouched = true">
</template>

<script setup>
import { computed, ref } from 'vue'
const title = ref('')
const titleTouched = ref(false)
const titleClass = computed(() => {
  if (!titleTouched.value) return ''
  return title.value.length >= 3 ? 'valid' : 'invalid'
})
</script>

模板负责展示,计算属性负责解释状态,后续修改校验规则时也更容易定位。


三、Class 的数组语法

3.1 绑定类名列表

数组语法适合按顺序组合多个类名:

<template>
  <button :class="[sizeClass, themeClass]">保存</button>
</template>

<script setup>
import { ref } from 'vue'
const sizeClass = ref('btn-large')
const themeClass = ref('btn-primary')
</script>

数组元素可以是变量,也可以直接写字符串:

<div :class="['panel', sizeClass, themeClass]">内容</div>

3.2 在数组中使用条件表达式

<template>
  <button
    class="btn"
    :class="[
      isPrimary ? 'btn-primary' : 'btn-secondary',
      { 'is-loading': submitting, 'is-disabled': submitting }
    ]"
  >
    {{ submitting ? '提交中' : '提交' }}
  </button>
</template>

<script setup>
import { ref } from 'vue'
const isPrimary = ref(true)
const submitting = ref(false)
</script>

数组中的对象适合处理条件类名。对象与数组可以嵌套组合,但嵌套层级不宜过深;如果表达式已经难以一眼读懂,就应改为计算属性。

3.3 计算属性返回数组

<template>
  <button :class="submitButtonClasses">提交</button>
</template>

<script setup>
import { computed, ref } from 'vue'
const submitting = ref(false)
const canSubmit = ref(true)
const submitButtonClasses = computed(() => [
  'btn',
  canSubmit.value ? 'btn-primary' : 'btn-disabled',
  { 'is-loading': submitting.value }
])
</script>

返回对象还是数组没有绝对优劣。关键是让类名来源清晰,避免在模板中重复同一组条件。


四、Style 的对象语法

4.1 绑定单个样式对象

:style 可以直接接收 JavaScript 对象:

<template>
  <div :style="{ color: textColor, fontSize: '18px', fontWeight: 600 }">
    动态文本
  </div>
</template>

<script setup>
import { ref } from 'vue'
const textColor = ref('#42b883')
</script>

CSS 属性可以使用小驼峰命名:

<div :style="{ backgroundColor: '#f3f4f6', borderRadius: '8px' }">卡片</div>

也支持带引号的短横线命名:

<div :style="{ 'background-color': '#f3f4f6', 'border-radius': '8px' }">卡片</div>

同一项目中建议统一一种写法。数值型 CSS 值建议显式携带单位,避免不同属性对单位的要求造成歧义。

4.2 使用样式对象变量

样式条件较多时,可以把对象放入响应式数据或计算属性:

<template>
  <div :style="progressStyle">{{ percent }}%</div>
</template>

<script setup>
import { computed, ref } from 'vue'
const percent = ref(60)
const progressStyle = computed(() => ({
  width: `${percent.value}%`,
  backgroundColor: percent.value === 100 ? '#16a34a' : '#42b883',
  transition: 'width 0.2s ease'
}))
</script>

不要把整个计算属性写成每次都拼接的大段 CSS 字符串。对象结构更容易检查,也能减少无效样式覆盖。

4.3 绑定 CSS 自定义属性

Vue 的 :style 同样支持 CSS 自定义属性:

<template>
  <div class="progress" :style="{ '--progress': `${percent}%` }">
    <div class="progress-bar"></div>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const percent = ref(60)
</script>

<style>
.progress {
  height: 8px;
  background: #e5e7eb;
}
.progress-bar {
  width: var(--progress);
  height: 100%;
  background: #42b883;
}
</style>

自定义属性名必须以 -- 开头并加引号。它适合把少量动态数值传给 CSS,其余布局与颜色规则仍保留在样式表中。


五、Style 的数组语法与合并规则

数组语法可以合并多个样式对象,后面的对象会覆盖前面的同名属性:

<template>
  <div :style="[baseStyle, statusStyle]">状态文本</div>
</template>

<script setup>
import { computed, ref } from 'vue'
const isError = ref(true)
const baseStyle = {
  padding: '12px',
  borderRadius: '6px',
  color: '#374151'
}
const statusStyle = computed(() => ({
  color: isError.value ? '#dc2626' : '#059669',
  backgroundColor: isError.value ? '#fef2f2' : '#ecfdf5'
}))
</script>

对象展开也能表达覆盖关系:

<template>
  <div :style="{ ...baseStyle, ...statusStyle }">状态文本</div>
</template>

数组写法的合并顺序更直观,尤其适合“基础样式 + 状态样式 + 临时覆盖样式”的场景。


六、用状态映射替代散落的条件判断

当状态固定为几种枚举值时,可以使用映射表集中管理样式:

<template>
  <span class="status-tag" :class="statusClass">{{ statusText }}</span>
</template>

<script setup>
import { computed, ref } from 'vue'
const status = ref('pending')
const statusMap = {
  pending: { text: '待处理', className: 'tag-warning' },
  success: { text: '已完成', className: 'tag-success' },
  failed: { text: '失败', className: 'tag-error' }
}
const currentStatus = computed(() => statusMap[status.value] || statusMap.pending)
const statusText = computed(() => currentStatus.value.text)
const statusClass = computed(() => currentStatus.value.className)
</script>

状态映射让文案与类名保持一致,新增状态时只需修改一处。不过,class 是 JavaScript 的保留字,因此示例使用 className 作为对象字段名。


七、综合实战:带状态反馈的任务表单

下面把 Class 对象、Class 数组、Style 对象、计算属性和表单状态组合起来。示例包含标题校验、优先级选择、完成进度、提交状态和重置功能。

<template>
  <form class="task-form" @submit.prevent="handleSubmit">
    <div class="form-header">
      <h2>新建任务</h2>
      <span :class="['status-tag', `tag-${submitState}`]">{{ stateText }}</span>
    </div>

    <label for="title">任务标题</label>
    <input
      id="title"
      v-model.trim="form.title"
      :class="titleClass"
      placeholder="至少输入 3 个字符"
      @blur="touched.title = true"
    >
    <p v-if="titleError" class="field-error">{{ titleError }}</p>

    <label for="priority">优先级</label>
    <select id="priority" v-model="form.priority">
      <option value="low">低</option>
      <option value="medium">中</option>
      <option value="high">高</option>
    </select>

    <label class="checkbox-row">
      <input v-model="form.completed" type="checkbox">
      创建时标记为已完成
    </label>

    <div class="progress-block">
      <span>完成进度</span>
      <div class="progress" :style="{ '--progress': `${progress}%` }">
        <div class="progress-bar"></div>
      </div>
      <strong>{{ progress }}%</strong>
    </div>

    <div
      class="preview-card"
      :class="{ 'is-completed': form.completed }"
      :style="[priorityStyle, { opacity: submitting ? 0.65 : 1 }]"
    >
      <small>任务预览</small>
      <h3>{{ form.title || '尚未填写标题' }}</h3>
      <p>{{ form.completed ? '已完成' : '待处理' }}</p>
    </div>

    <p v-if="message" :class="messageClass">{{ message }}</p>

    <div class="actions">
      <button type="button" :disabled="submitting" @click="resetForm">重置</button>
      <button
        type="submit"
        :class="['btn-primary', { 'is-loading': submitting }]"
        :disabled="submitting"
      >
        {{ submitting ? '提交中...' : '创建任务' }}
      </button>
    </div>
  </form>
</template>

<script setup>
import { computed, reactive, ref } from 'vue'

const initialForm = () => ({
  title: '',
  priority: 'medium',
  completed: false
})
const form = reactive(initialForm())
const touched = reactive({ title: false })
const submitting = ref(false)
const submitState = ref('idle')
const message = ref('')

const titleError = computed(() => {
  if (!touched.title) return ''
  if (!form.title) return '请输入任务标题'
  if (form.title.length < 3) return '标题至少包含 3 个字符'
  return ''
})
const titleClass = computed(() => {
  if (!touched.title) return ''
  return titleError.value ? 'is-invalid' : 'is-valid'
})
const isValid = computed(() => form.title.length >= 3)
const progress = computed(() => form.completed ? 100 : form.title ? 50 : 0)

const stateText = computed(() => ({
  idle: '编辑中',
  success: '创建成功',
  error: '创建失败'
}[submitState.value]))
const messageClass = computed(() => [
  'form-message',
  submitState.value === 'error' ? 'message-error' : 'message-success'
])
const priorityStyle = computed(() => ({
  borderLeft: `4px solid ${{ high: '#dc2626', medium: '#d97706', low: '#059669' }[form.priority]}`
}))

const handleSubmit = async () => {
  touched.title = true
  message.value = ''
  if (!isValid.value || submitting.value) return

  submitting.value = true
  submitState.value = 'idle'
  try {
    await new Promise(resolve => setTimeout(resolve, 400))
    submitState.value = 'success'
    message.value = `任务“${form.title}”已创建`
  } catch (error) {
    submitState.value = 'error'
    message.value = '创建失败,请稍后重试'
  } finally {
    submitting.value = false
  }
}

const resetForm = () => {
  Object.assign(form, initialForm())
  touched.title = false
  submitting.value = false
  submitState.value = 'idle'
  message.value = ''
}
</script>

<style>
.task-form {
  max-width: 560px;
  display: grid;
  gap: 12px;
}
.form-header,
.progress-block,
.actions {
  display: flex;
  align-items: center;
  gap: 12px;
}
.form-header {
  justify-content: space-between;
}
.task-form input:not([type='checkbox']),
.task-form select {
  padding: 8px 10px;
  border: 1px solid #d1d5db;
  border-radius: 6px;
}
.is-valid {
  border-color: #059669 !important;
}
.is-invalid {
  border-color: #dc2626 !important;
}
.field-error,
.message-error {
  color: #dc2626;
}
.message-success {
  color: #059669;
}
.status-tag {
  padding: 2px 8px;
  border-radius: 999px;
  background: #f3f4f6;
}
.tag-success {
  color: #059669;
  background: #ecfdf5;
}
.tag-error {
  color: #dc2626;
  background: #fef2f2;
}
.progress {
  flex: 1;
  height: 8px;
  background: #e5e7eb;
}
.progress-bar {
  width: var(--progress);
  height: 100%;
  background: #42b883;
  transition: width 0.2s ease;
}
.preview-card {
  padding: 16px;
  border-radius: 8px;
  background: #f9fafb;
}
.preview-card.is-completed h3 {
  color: #6b7280;
  text-decoration: line-through;
}
.btn-primary {
  color: white;
  background: #42b883;
}
.is-loading {
  cursor: wait;
}
</style>

这个示例有几个关键点:

  1. titleClass 根据失焦状态和校验结果返回类名,避免页面刚打开就显示错误。

  2. priorityStyle 只负责动态边框颜色,其余卡片样式仍由 CSS 类管理。

  3. progress 是业务状态,CSS 自定义属性只负责把进度数值传给样式表。

  4. submitState 同时驱动标签、提示文案和提示颜色,避免同一状态出现互相矛盾的外观。

  5. 重置时同时恢复表单、失焦状态、提交状态和提示信息。


八、列表项与组件中的样式绑定

8.1 列表项状态

在列表中,应使用每项自身的数据计算样式,而不是使用一个全局布尔值控制所有行:

<template>
  <ul>
    <li
      v-for="task in tasks"
      :key="task.id"
      :class="{ 'is-selected': task.id === selectedId, 'is-completed': task.completed }"
      @click="selectedId = task.id"
    >
      {{ task.title }}
    </li>
  </ul>
</template>

<script setup>
import { ref } from 'vue'
const selectedId = ref(1)
const tasks = ref([
  { id: 1, title: '学习 Class 绑定', completed: true },
  { id: 2, title: '学习 Style 绑定', completed: false }
])
</script>

task.id === selectedId 只影响当前遍历项;task.completed 则完全来自该项自身状态。

8.2 组件上的 Class 与 Style 透传

在单根元素组件上使用时,父组件传入的 class 和 style 默认会合并到组件根元素:

<!-- 父组件 -->
<StatusCard class="panel-highlight" :style="{ marginTop: '16px' }" />
<!-- StatusCard.vue -->
<template>
  <section class="status-card">
    <slot></slot>
  </section>
</template>

最终根元素会同时具有 status-card 和 panel-highlight,并应用父组件传入的上边距。父组件不需要修改子组件内部实现。

多根节点组件没有唯一根元素,无法自动决定透传目标。此时应明确绑定 $attrs:

<template>
  <header>标题</header>
  <section v-bind="$attrs">正文</section>
</template>

<script setup>
defineOptions({ inheritAttrs: false })
</script>

v-bind="$attrs" 会透传父组件传入的属性,包括 class 和 style。只在确实需要控制透传目标时关闭 inheritAttrs,不要把它当成所有组件的固定模板。


九、Class 与内联 Style 如何选择

场景

推荐方式

原因

成功、失败、加载中等固定状态

Class 绑定

样式可复用,便于统一维护

宽度、进度、坐标等连续数值

Style 绑定或 CSS 自定义属性

数值由运行时状态决定

主题色、间距等少量动态参数

CSS 自定义属性

保留 CSS 结构,仅传入必要变量

多种样式组合与覆盖

Class 数组或 Style 数组

合并顺序清晰

复杂校验与展示规则

计算属性

避免模板堆叠表达式

直接读取并修改 DOM 样式

通常不推荐

绕开状态驱动,后续难以同步

内联样式不是“性能更好”的通用方案,Class 也不是越少越好。选择标准是样式是否固定、是否需要复用,以及状态含义是否清楚。


十、常见问题自查

问题

推荐处理

动态类名没有生效

检查是否使用 :class,以及条件值是否按预期变化

带连字符的类名报错

在对象中使用 'is-active' 形式的字符串键

静态类名被动态绑定覆盖

同时保留 class 与 :class,Vue 会自动合并

font-size 写法不稳定

使用 fontSize 或带引号的 'font-size',并显式携带单位

所有列表项一起变色

使用当前遍历项的数据,或比较当前项 id 与选中 id

数组中的样式没有覆盖

检查对象顺序,后面的同名属性优先

CSS 变量不生效

确认变量名以 -- 开头,并在 CSS 中通过 var() 使用

多根组件样式未透传

设置 inheritAttrs: false,并为目标节点显式绑定 $attrs

模板表达式过长

提取计算属性或状态映射


十一、总结

Class 与 Style 绑定的核心是让外观跟随状态变化:

  1. :class 支持对象和数组语法,静态 class 会与动态类名合并。

  2. 对象语法适合条件类名,数组语法适合组合多个类名,两者可以嵌套使用。

  3. :style 支持对象、数组和 CSS 自定义属性,数组合并时后面的对象优先。

  4. 固定状态优先使用 Class,连续数值和少量动态参数再考虑内联 Style。

  5. 复杂条件应提取到计算属性或状态映射,避免模板承担过多业务逻辑。

  6. 单根组件默认透传 class 和 style,多根组件需要明确透传目标。

下一站预告

下一篇《组件基础与注册方式》将把页面拆成可复用组件,学习组件定义、注册、插值渲染和基本组织方式,为后续 Props 与组件通信打好基础。


作者:洋洋技术笔记 发布日期:2026-11-04 系列:Vue.js从入门到精通 - 第7篇


原文链接:洋洋的技术笔记

0

评论区