构建现代化 Vue 3 应用:从零到一实现功能完备的 Todo List
构建现代化 Vue 3 应用:从零到一实现功能完备的 Todo List
引言:为什么选择这个技术栈?
在当今快速发展的前端领域,选择一个高效、健壮且易于维护的技术栈是项目成功的关键。Vue 3 以其出色的性能、友好的 Composition API 和优秀的开发者体验,赢得了大量开发者的青睐。然而,仅靠 Vue 本身不足以应对复杂应用的状态管理挑战。
本文将带领你深入探索如何将 Vue 3 的组件化开发与 Pinia 状态管理库无缝结合,并辅以 TypeScript 的类型安全、Vite 的极速构建以及 Tailwind CSS 的原子化样式,共同打造一个功能完整、类型安全、样式美观的现代化 Todo List 应用。
我们将摒弃简单的示例代码,专注于实践中的最佳实践和核心概念。通过一步步的构建过程,你不仅能掌握每个技术的使用方法,更能理解它们如何协同工作,从而为构建更复杂的单页应用(SPA)打下坚实的基础。
第一部分:项目初始化与开发环境搭建
1.1 选择 Vite:下一代前端构建工具
在项目伊始,我们选择 Vite 而非传统的 Vue CLI 作为构建工具。Vite 利用了浏览器对 ES 模块的原生支持,实现了闪电般的冷启动和快速的热模块更新(HMR),这将极大提升我们的开发效率。
执行项目初始化命令:
打开你的终端或命令行工具,运行以下命令来创建项目骨架:
npm create vite@latest
接下来,你会看到一系列交互式提示,用于配置项目的基本信息:
• Project name: vue-pinia-todo-app (你可以根据自己的喜好命名,但建议使用有意义的名称)
• Select a framework: Vue
• Select a variant: TypeScript
Vite 会自动创建一个配置好 Vue 3 和 TypeScript 的基本项目结构。命令执行完毕后,进入项目目录并安装依赖:
cd vue-pinia-todo-app
npm install
此时,一个最基础的 Vue 3 + TypeScript 项目已经就绪。你可以通过 npm run dev 命令启动开发服务器,在浏览器中查看默认的应用页面。
1.2 引入状态管理库:Pinia
Pinia 是 Vue 官方推荐的状态管理库,可以看作是下一代 Vuex。它提供了更简洁的 API、完美的 TypeScript 支持,并且与 Vue DevTools 深度集成。
安装 Pinia:
在项目根目录下,执行以下命令:
npm install pinia
Pinia 的设计哲学是直观且类型安全的,它将彻底改变你管理应用状态的方式。
1.3 集成样式引擎:Tailwind CSS
Tailwind CSS 是一个功能类优先的 CSS 框架,它允许你通过组合预定义的原子类来快速构建自定义设计,而无需离开你的 HTML/模板文件。
安装 Tailwind CSS 及其 Vite 插件:
我们安装 Tailwind 及其专为 Vite 优化的插件,以确保最优的构建性能。
npm install -D tailwindcss @tailwindcss/vite
配置 Vite 以使用 Tailwind:
接下来,我们需要告诉 Vite 在处理 CSS 时使用 Tailwind。修改项目根目录下的 vite.config.ts 文件:
// vite.config.ts
import { defineConfig } from ‘vite’
import vue from ‘@vitejs/plugin-vue’
import tailwindcss from ‘@tailwindcss/vite’ // 引入 Tailwind Vite 插件
// https://vitejs.dev/config/
export default defineConfig({
plugins: [
vue(),
tailwindcss(), // 将 Tailwind 插件添加到 Vite 插件列表中
],
})
引入 Tailwind 样式:
最后,我们需要在主 CSS 文件中引入 Tailwind 的指令。根据你的项目模板,找到 src/style.css 或 src/assets/main.css 文件,清空其内容,并添加以下行:
/* src/style.css */
@import ‘tailwindcss’;
Tailwind v4 简化了引入流程,一个 @import ‘tailwindcss’ 指令就包含了基础样式、组件类和工具类。
至此,我们的开发环境已经全部配置完成。现在,让我们开始构建应用的核心逻辑。
第二部分:应用架构与类型安全设计
2.1 定义数据模型:使用 TypeScript 接口
在编写任何业务逻辑之前,我们先使用 TypeScript 来定义清晰的数据结构。这就像建筑师的蓝图,它能确保我们在整个开发过程中数据的形状都是可预测和安全的。
在 src 目录下创建 types 文件夹,并在其中创建 todo.ts 文件:
// src/types/todo.ts
/**
- 表示一个单一的待办事项项
/
export interface ITodoItem {
/* 事项的唯一标识符 /
id: number;
/* 事项的文本内容 /
text: string;
/* 事项的完成状态,true 表示已完成 /
isCompleted: boolean;
/* 事项的创建时间戳 */
createdAt: Date;
}
/**
- 表示待办事项列表的过滤状态
*/
export type TodoFilter = ‘all’ | ‘active’ | ‘completed’;
/**
- 表示 Todo Store 的完整状态
/
export interface ITodoState {
/* 所有的待办事项数组 /
items: ITodoItem[];
/* 用于生成下一个事项 ID 的计数器 /
nextId: number;
/* 当前应用的筛选条件 */
currentFilter: TodoFilter;
}
设计思路解析:
• ITodoItem: 我们为待办事项增加了 createdAt 字段,这在真实应用中非常有用,例如可以用于排序。
• TodoFilter: 使用联合类型明确定义可能的过滤状态,避免魔法字符串(magic strings),增强代码可读性和安全性。
• ITodoState: 状态结构不仅包含核心数据 items,还包含了 UI 状态 currentFilter,这体现了 Pinia 允许将部分 UI 状态集中管理的灵活性。
2.2 构建状态管理中心:Pinia Store
Pinia Store 是状态、业务逻辑和计算属性的容器。我们将在一个 Store 中管理整个 Todo 应用的状态。
在 src 目录下创建 stores 文件夹,并创建 todoStore.ts 文件:
2.2.1 定义 State 和 Getters
// src/stores/todoStore.ts
import { defineStore } from ‘pinia’;
import type { ITodoItem, ITodoState, TodoFilter } from ‘@/types/todo’; // 使用路径别名@
// 使用 defineStore 定义 Store,‘todo’ 是 Store 的唯一 ID
export const useTodoStore = defineStore(‘todo’, {
// State 是一个返回初始状态的函数
state: (): ITodoState => ({
items: [
{
id: 1,
text: ‘学习 Pinia 的核心概念’,
isCompleted: true,
createdAt: new Date(‘2024-01-01’),
},
{
id: 2,
text: ‘掌握 Composition API 的使用’,
isCompleted: false,
createdAt: new Date(‘2024-01-02’),
},
],
nextId: 3,
currentFilter: ‘all’,
}),
// Getters 类似于组件的计算属性,用于派生基于 State 的数据
getters: {
/**
* 根据当前过滤条件返回过滤后的待办事项
*/
filteredItems(state): ITodoItem[] {
switch (state.currentFilter) {
case ‘active’:
return state.items.filter(item => !item.isCompleted);
case ‘completed’:
return state.items.filter(item => item.isCompleted);
case ‘all’:
default:
return state.items;
}
},
/**
* 统计所有未完成事项的数量
*/
activeItemsCount(state): number {
// 使用 reduce 方法进行统计,是一种函数式编程的实践
return state.items.reduce((count, item) => count + (item.isCompleted ? 0 : 1), 0);
},
/**
* 统计所有已完成事项的数量
*/
completedItemsCount(state): number {
return state.items.filter(item => item.isCompleted).length;
},
/**
* 检查是否所有事项都已完成
*/
isAllCompleted(state): boolean {
return state.items.length > 0 && state.items.every(item => item.isCompleted);
},
},
});
Getters 深度解析:
• filteredItems: 这是核心的 Getter,它根据 currentFilter 状态动态返回不同的列表。这种设计将复杂的过滤逻辑从组件中抽离,使组件更专注于渲染。
• activeItemsCount: 展示了使用 reduce 方法进行累加计算的模式,这是一种高效且声明式的编程方式。
• isAllCompleted: 使用了数组的 every 方法,并添加了 length > 0 的条件,避免了空数组也返回 true 的逻辑错误。
2.2.2 定义 Actions:修改状态的唯一途径
Actions 相当于组件中的 methods,是修改 State 的唯一方式。它们可以包含任意异步操作。
// 在 todoStore.ts 的 defineStore 配置对象中继续添加 actions
actions: {
/**
* 添加一个新的待办事项
* @param text - 待办事项的文本内容
*/
addItem(text: string): void {
// 输入验证:确保文本非空
if (!text || text.trim() === ‘’) {
console.warn(‘尝试添加空待办事项’);
return;
}
const newItem: ITodoItem = {
id: this.nextId,
text: text.trim(),
isCompleted: false,
createdAt: new Date(),
};
// 将新事项添加到数组开头,使最新添加的显示在最上面
this.items.unshift(newItem);
this.nextId++;
},
/**
* 根据 ID 删除一个待办事项
* @param id - 要删除的事项的 ID
*/
removeItem(id: number): void {
// 使用 findIndex 找到目标项的索引
const index = this.items.findIndex(item => item.id === id);
if (index !== -1) {
this.items.splice(index, 1);
}
},
/**
* 切换单个待办事项的完成状态
* @param id - 要切换状态的事项的 ID
*/
toggleItem(id: number): void {
const item = this.items.find(item => item.id === id);
if (item) {
item.isCompleted = !item.isCompleted;
}
},
/**
* 批量更新所有事项的完成状态
* @param completed - 要设置的状态,true 为全部完成,false 为全部未完成
*/
toggleAllItems(completed: boolean): void {
this.items.forEach(item => {
item.isCompleted = completed;
});
},
/**
* 更新待办事项的文本内容
* @param id - 要更新的事项的 ID
* @param newText - 新的文本内容
*/
updateItemText(id: number, newText: string): void {
const item = this.items.find(item => item.id === id);
if (item && newText.trim() !== '') {
item.text = newText.trim();
}
},
/**
* 清除所有已完成的事项
*/
clearCompleted(): void {
this.items = this.items.filter(item => !item.isCompleted);
},
/**
* 设置当前的过滤条件
* @param filter - 新的过滤条件
*/
setFilter(filter: TodoFilter): void {
this.currentFilter = filter;
},
},
Actions 设计理念:
• 单一职责: 每个 Action 只负责一个明确的操作,如 addItem 只负责添加,toggleItem 只负责切换状态。
• 数据验证: 在 addItem 和 updateItemText 中,我们对输入进行了基本的验证,提高了应用的健壮性。
• 不可变更新: 在 clearCompleted 中,我们通过赋值一个新的数组 (this.items = …) 来触发响应式更新。虽然 Pinia 允许直接修改数组,但这种模式在某些场景下更清晰。
• 业务逻辑集中化: 所有与待办事项数据变更相关的逻辑都封装在此,组件只需调用相应的 Action,而无需关心内部实现。
第三部分:构建用户界面与组件化实践
3.1 配置应用根组件
首先,我们需要在应用的入口点注册 Pinia。
修改 src/main.ts:
// src/main.ts
import { createApp } from ‘vue’;
import { createPinia } from ‘pinia’;
import App from ‘./App.vue’;
import ‘./style.css’; // 确保 Tailwind CSS 被引入
// 创建应用和 Pinia 实例
const app = createApp(App);
const pinia = createPinia();
// 使用 Pinia 插件
app.use(pinia);
// 挂载到 DOM
app.mount(‘#app’);
接下来,我们重写主要的 App.vue 组件,将其作为整个应用的布局容器。
我的任务管理中心
高效管理你的每日任务,提升生产力
<!-- 主卡片区域 -->
<main class="bg-white rounded-2xl shadow-xl overflow-hidden">
<!-- 新增事项输入区 -->
<div class="p-6 border-b border-gray-200">
<div class="flex space-x-4">
<input
type="text"
v-model.trim="newTodoText"
@keypress="handleKeypress"
placeholder="接下来要做什么?"
class="flex-1 py-3 px-4 border border-gray-300 rounded-lg focus:ring-2 focus:ring-blue-500 focus:border-transparent outline-none transition duration-200"
/>
<button
@click="handleAddTodo"
:disabled="!newTodoText.trim()"
class="bg-blue-600 text-white font-semibold py-3 px-6 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 disabled:opacity-50 disabled:cursor-not-allowed transition duration-200"
>
添加任务
</button>
</div>
</div>
<!-- 过滤选项卡和统计信息 -->
<div class="px-6 py-4 bg-gray-50 border-b border-gray-200 flex justify-between items-center">
<div class="flex space-x-1">
<button
v-for="filter in ['all', 'active', 'completed']"
:key="filter"
@click="todoStore.setFilter(filter as any)"
:class="[
'px-3 py-1 rounded-full text-sm font-medium capitalize transition duration-200',
todoStore.currentFilter === filter
? 'bg-blue-100 text-blue-800'
: 'text-gray-600 hover:bg-gray-200'
]"
>
{{ filter }}
</button>
</div>
<div class="text-sm text-gray-500">
<span class="font-medium text-blue-600">{{ todoStore.activeItemsCount }}</span> 项待完成
</div>
</div>
<!-- 待办事项列表 -->
<ul v-if="todoStore.filteredItems.length > 0" class="divide-y divide-gray-200">
<li
v-for="todo in todoStore.filteredItems"
:key="todo.id"
class="px-6 py-4 hover:bg-gray-50 transition duration-150"
>
<!-- 我们将使用一个单独的组件来渲染每个 Todo 项 -->
<TodoItem :todo="todo" />
</li>
</ul>
<!-- 空状态提示 -->
<div v-else class="text-center py-12">
<div class="text-gray-400 mb-2">
<svg class="w-16 h-16 mx-auto" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<p class="text-gray-500 italic">
{{
todoStore.currentFilter === 'completed'
? '还没有完成的任务'
: todoStore.currentFilter === 'active'
? '恭喜!所有任务都已完成!'
: '还没有添加任何任务'
}}
</p>
</div>
<!-- 底部操作栏 -->
<footer v-if="todoStore.items.length > 0" class="px-6 py-4 bg-gray-50 flex justify-between items-center text-sm">
<span class="text-gray-600">
总计 <span class="font-semibold">{{ totalItemsCount }}</span> 项任务
</span>
<button
v-if="todoStore.completedItemsCount > 0"
@click="todoStore.clearCompleted()"
class="text-red-600 hover:text-red-800 font-medium transition duration-200"
>
清除已完成 ({{ todoStore.completedItemsCount }})
</button>
</footer>
</main>
</div>
组件设计分析:
• 布局与样式: 使用了 Tailwind CSS 的原子化类名构建了一个美观的卡片布局,包括渐变背景、阴影、圆角等。
• 条件渲染: 使用 v-if 和 v-else 优雅地处理了列表为空的状态。
• 事件处理: 为输入框绑定了键盘事件,提升了用户体验。
• 动态类名: 过滤按钮使用了 :class 的动态绑定,根据当前状态高亮显示。
• 组件拆分: 我们意识到每个待办事项项(
- )会包含独立的交互逻辑(如编辑、删除),因此计划将其拆分为独立的子组件 TodoItem。
-
3.2 创建可复用的子组件:TodoItem
在 src/components 目录下创建 TodoItem.vue 文件。这个组件将负责单个待办事项的渲染和交互。
子组件技术亮点:
• Props 类型定义: 使用 TypeScript 的接口明确定义了组件需要接收的 todo 属性。
• 双向编辑模式: 实现了常见的“双击编辑”和“点击图标编辑”两种模式,提供了良好的用户体验。
• 键盘交互: 在编辑模式下支持 Enter 保存和 Escape 取消,符合用户习惯。
• 条件样式: 使用 :class 动态绑定,为已完成的事项添加删除线和灰色文字。
• 悬停效果: 操作按钮默认隐藏,只在鼠标悬停在该事项上时显示(通过 group 和 group-hover 实现),保持了界面的简洁。
• 图标使用: 内联 SVG 图标,无需额外依赖图标库。
最后,别忘了在 App.vue 中导入并使用 TodoItem 组件。
import TodoItem from ‘@/components/TodoItem.vue’;
第四部分:高级技巧与最佳实践
4.1 状态持久化:使用 Pinia 插件
应用刷新后,状态会丢失。我们可以使用 pinia-plugin-persistedstate 插件轻松实现状态持久化。
安装插件:
npm install pinia-plugin-persistedstate配置插件:
修改 src/main.ts:
// src/main.ts
import { createApp } from ‘vue’;
import { createPinia } from ‘pinia’;
import { createPersistedState } from ‘pinia-plugin-persistedstate’; // 导入插件
import App from ‘./App.vue’;
import ‘./style.css’;const app = createApp(App);
const pinia = createPinia();// 使用持久化状态插件
pinia.use(createPersistedState({
storage: localStorage, // 默认使用 localStorage
// 可以在这里配置全局序列化/反序列化方法等
}));app.use(pinia);
app.mount(‘#app’);
更多推荐



所有评论(0)