Vue 3 + Tailwind v4 组件化开发与 Pinia 状态管理(十六)
- Vue.js
- 1天前
- 5热度
- 0评论
在现代前端开发中,组件化是提高代码可维护性和复用性的关键。本文将带你深入了解如何在 Vue 3 和 Tailwind v4 的环境下进行组件化开发,并结合 Pinia 实现状态管理和数据持久化。通过本文,你将学会如何将复杂的单页应用拆分为多个独立的组件,并确保数据在组件间高效传递。
组件化开发
1. 拆分组件
首先,我们将现有的 App.vue 文件中的功能拆分为多个独立的子组件。这样做不仅有助于提高代码的可读性和可维护性,还能提升开发效率。我们将在 src/components 目录下创建以下四个文件:
- TaskHeader.vue:静态展示组件
- TaskInput.vue:输入逻辑组件
- TaskFilter.vue:状态切换组件
- TaskItem.vue:单条任务展示组件
TaskHeader.vue —— 静态展示组件
TaskHeader.vue 负责显示页面的头部信息,不需要任何 JavaScript 逻辑,仅用于展示 UI。在 Tailwind v4 中,我们继续使用新的渐变语法来增强视觉效果。
<template>
<header class="text-center text-2xl font-bold text-brand py-4">
今日待办事项
</header>
</template>TaskInput.vue —— 输入逻辑组件
TaskInput.vue 负责获取用户的输入,并在用户点击“添加”按钮时,通过 emit 事件通知父组件。这种方式确保了数据的单向流动,提高了代码的可维护性。
<script setup>
import { ref } from 'vue';
const emit = defineEmits(['add-task']);
const title = ref('');
const handleSubmit = () => {
const value = title.value.trim();
if (!value) return;
emit('add-task', value);
title.value = '';
};
</script>
<template>
<div class="flex gap-2 mb-8">
<input
v-model="title"
@keyup.enter="handleSubmit"
placeholder="今天要完成什么?"
class="flex-1 bg-slate-50 border-none rounded-2xl px-4 py-3 focus:ring-2 focus:ring-brand/50 outline-none transition-all"
/>
<button
@click="handleSubmit"
class="bg-brand text-white px-6 rounded-2xl font-bold hover:scale-105 active:scale-95 transition-all"
>
+
</button>
</div>
</template>TaskFilter.vue —— 状态切换组件
TaskFilter.vue 负责在不同任务状态之间进行切换。在 Vue 3.4+ 中,推荐使用 defineModel() 来实现双向绑定,这在 Tailwind v4 的 UI 切换中非常高效。
<script setup>
const modelValue = defineModel();
const filters = ['all', 'active', 'completed'];
</script>
<template>
<div class="flex p-1 bg-slate-100 rounded-xl mb-6">
<button
v-for="f in filters"
:key="f"
@click="modelValue = f"
:class="[
'flex-1 py-1.5 text-xs font-bold rounded-lg transition-all capitalize',
modelValue === f ? 'bg-white text-brand shadow-sm' : 'text-slate-500'
]"
>
{{ f }}
</button>
</div>
</template>TaskItem.vue —— 单条任务展示组件
TaskItem.vue 是最核心的子组件,负责展示单条任务并反馈修改/删除操作。通过 defineProps 接收父组件传递的任务对象,并通过 defineEmits 发出事件通知父组件。
<script setup>
defineProps({
task: {
type: Object,
required: true
}
});
const emit = defineEmits(['toggle', 'remove']);
</script>
<template>
<li
class="group flex items-center justify-between p-4 bg-slate-50 rounded-2xl border border-transparent hover:border-slate-200 hover:bg-white transition-all"
>
<div class="flex items-center gap-3">
<input
type="checkbox"
:checked="task.isCompleted"
@change="emit('toggle', task.id)"
class="w-5 h-5 accent-brand cursor-pointer"
/>
<span
:class="[
'text-slate-700 font-medium',
task.isCompleted ? 'line-through text-slate-400 opacity-50' : ''
]"
>
{{ task.title }}
</span>
</div>
<button
@click="emit('remove', task.id)"
class="opacity-0 group-hover:opacity-100 text-slate-300 hover:text-red-500 transition-all p-1"
>
✕
</button>
</li>
</template>2. 重构 App.vue
经过组件化拆分后,App.vue 成为了一个干净的逻辑中枢,只负责管理原始数据和处理子组件传来的指令。这样不仅提高了代码的可读性,还便于后续的扩展和维护。
<script setup>
import { ref, computed } from 'vue';
import TaskHeader from './components/TaskHeader.vue';
import TaskInput from './components/TaskInput.vue';
import TaskFilter from './components/TaskFilter.vue';
import TaskItem from './components/TaskItem.vue';
const tasks = ref([
{ id: '1', title: '体验 Tailwind v4 新特性', isCompleted: false },
{ id: '2', title: '掌握 Composition API', isCompleted: true }
]);
const filter = ref('all');
const handleAddTask = (title) => {
tasks.value.unshift({ id: crypto.randomUUID(), title, isCompleted: false });
};
const handleRemoveTask = (id) => {
tasks.value = tasks.value.filter(t => t.id !== id);
};
const handleToggleTask = (id) => {
const task = tasks.value.find(t => t.id === id);
if (task) task.isCompleted = !task.isCompleted;
};
const filteredTasks = computed(() => {
if (filter.value === 'active') return tasks.value.filter(t => !t.isCompleted);
if (filter.value === 'completed') return tasks.value.filter(t => t.isCompleted);
return tasks.value;
});
</script>
<template>
<div class="min-h-screen py-12 px-4">
<div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl shadow-slate-200 border border-slate-100 overflow-hidden">
<TaskHeader />
<main class="p-6">
<TaskInput @add-task="handleAddTask" />
<TaskFilter v-model="filter" />
<ul class="space-y-3">
<TransitionGroup name="list">
<TaskItem
v-for="task in filteredTasks"
:key="task.id"
:task="task"
@toggle="handleToggleTask"
@remove="handleRemoveTask"
/>
</TransitionGroup>
</ul>
<div v-if="filteredTasks.length === 0" class="text-center py-12 text-slate-400 text-sm">
暂无相关任务...
</div>
</main>
</div>
</div>
</template>
<style scoped>
.list-enter-active, .list-leave-active { transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28); }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
</style>核心知识点总结
- defineProps (父传子):TaskItem 通过 props 接收任务数据。在 Vue 中,Props 是只读的,子组件不应直接修改 props 的值,这是“单向数据流”的体现。
- defineEmits (子传父):当子组件需要改变数据时(如删除、勾选),它通过 emit 发出信号,由父组件执行真正的修改逻辑。这样保证了数据的修改源头只有一个,方便调试。
- defineModel (双向绑定神器):在 TaskFilter 中,我们使用了 Vue 3.4 引入的 defineModel。它极大地简化了父子组件之间状态同步的代码,不再需要手动写 props 和监听 @update:modelValue。
- Tailwind v4 的复用性:即使拆分了组件,Tailwind 的原子化类名依然生效。因为 Vite 插件会自动扫描 src 下所有的 .vue 文件并生成对应的样式。
Pinia 状态管理与数据持久化
为什么要用 Pinia?
在之前的代码中,数据都在 App.vue 里。如果项目变大(比如增加统计页面、用户中心),在组件间传递数据会变得非常复杂(Props Hell)。Pinia 提供了一个全局的数据仓库,任何组件都可以直接从仓库取数据或更新数据,大大简化了状态管理。
安装与初始化
首先,我们需要安装 Pinia 及其开发工具:
npm install pinia
npm install @vue/devtools-kit -D然后,在 src/main.js 中注册 Pinia:
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');创建 Task Store (仓库)
在 src 目录下创建 stores 目录,然后在 src/stores 目录下新建 taskStore.js。我们将把原本在 App.vue 里的逻辑全部搬过来。
import { defineStore } from 'pinia';
import { ref, computed, watch } from 'vue';
export const useTaskStore = defineStore('task-store', () => {
// --- 1. 状态 (State) ---
const savedTasks = localStorage.getItem('my-tasks');
const tasks = ref(savedTasks ? JSON.parse(savedTasks) : []);
const filter = ref('all');
// --- 2. 派生状态 (Getters) ---
const filteredTasks = computed(() => {
if (filter.value === 'active') return tasks.value.filter(t => !t.isCompleted);
if (filter.value === 'completed') return tasks.value.filter(t => t.isCompleted);
return tasks.value;
});
// --- 3. 动作 (Actions) ---
const addTask = (title) => {
tasks.value.unshift({ id: crypto.randomUUID(), title, isCompleted: false });
};
const removeTask = (id) => {
tasks.value = tasks.value.filter(t => t.id !== id);
};
const toggleTask = (id) => {
const task = tasks.value.find(t => t.id === id);
if (task) task.isCompleted = !task.isCompleted;
};
// --- 4. 持久化 (Persistence) ---
watch(tasks, (newVal) => {
localStorage.setItem('my-tasks', JSON.stringify(newVal));
}, { deep: true });
return {
tasks,
filter,
filteredTasks,
addTask,
removeTask,
toggleTask
};
});重构 App.vue (连接仓库)
现在,App.vue 不需要自己管理数据了,只需调用 Store 即可。我们使用 storeToRefs 保持数据的响应式解构,这样在模板里可以直接使用 filter 和 filteredTasks。
<script setup>
import { storeToRefs } from 'pinia';
import { useTaskStore } from '@/stores/taskStore';
import TaskHeader from './components/TaskHeader.vue';
import TaskInput from './components/TaskInput.vue';
import TaskFilter from './components/TaskFilter.vue';
import TaskItem from './components/TaskItem.vue';
const taskStore = useTaskStore();
const { filter, filteredTasks } = storeToRefs(taskStore);
const { addTask, removeTask, toggleTask } = taskStore;
</script>
<template>
<div class="min-h-screen py-12 px-4">
<div class="max-w-md mx-auto bg-white rounded-3xl shadow-xl shadow-slate-200 border border-slate-100 overflow-hidden">
<TaskHeader />
<main class="p-6">
<TaskInput @add-task="addTask" />
<TaskFilter v-model="filter" />
<ul class="space-y-3">
<TransitionGroup name="list">
<TaskItem
v-for="task in filteredTasks"
:key="task.id"
:task="task"
@toggle="toggleTask"
@remove="removeTask"
/>
</TransitionGroup>
</ul>
<div v-if="filteredTasks.length === 0" class="text-center py-12 text-slate-400 text-sm">
暂无相关任务...
</div>
</main>
</div>
</div>
</template>
<style scoped>
.list-enter-active, .list-leave-active { transition: all 0.4s cubic-bezier(0.18, 0.89, 0.32, 1.28); }
.list-enter-from, .list-leave-to { opacity: 0; transform: translateX(30px); }
</style>总结
通过本文,你学会了如何在 Vue 3 和 Tailwind v4 的环境中进行组件化开发,并结合 Pinia 实现状态管理和数据持久化。组件化开发不仅提高了代码的可读性和可维护性,还为项目的扩展提供了便利。Pinia 的引入则进一步简化了状态管理,使得数据在组件间高效传递。希望这些知识能帮助你在未来的项目中更加得心应手。