前端vue3快速入门第二日——通过TodoList学习vue
Vue TodoList 2.0学习目标从一个功能完整但逻辑集中在App.vue的 TodoList逐步整理成一个结构清晰、职责分明、可以继续扩展的小型 Vue 项目。一、今天完成了什么今天主要完成了四件事组件拆分把不同界面区域拆成独立组件。逻辑抽离把 Todo 业务逻辑从App.vue抽到组合式函数。Pinia 状态管理把 Todo 数据迁移到全局 Store。理解模块化学习export、import、props、emit、v-model、插槽等概念。二、组件拆分最开始TodoList 的输入框、任务列表、编辑功能、统计区域和样式都写在App.vue中。随着功能增多App.vue会越来越长所以我们按照“一个组件负责一类界面功能”的原则进行了拆分。当前组件结构src ├── components │ ├── BaseCard.vue │ ├── TodoInput.vue │ ├── TodoItem.vue │ └── TodoSummary.vue │ ├── stores │ └── todoStore.js │ ├── App.vue └── main.js各文件职责如下文件主要职责BaseCard.vue卡片外壳、默认插槽和具名插槽TodoInput.vue新任务输入框和提交按钮TodoItem.vue单条任务展示、编辑、删除、状态切换TodoSummary.vue未完成/已完成统计、清除按钮todoStore.jsTodo 数据、统计结果、操作函数、本地存储App.vue组合页面和协调组件main.js创建 Vue 应用安装 TDesign 和 Pinia为什么要拆组件拆组件不是为了让文件数量变多而是为了让每个文件只负责一件相对明确的事情。App.vue 原来既负责页面又负责输入、任务、编辑、统计、存储 拆分后 App.vue 只负责组合 TodoInput.vue 负责输入 TodoItem.vue 负责单条任务 TodoSummary.vue 负责统计 todoStore.js 负责数据和业务拆分后的好处更容易阅读某个功能出错时更容易定位组件可以复用样式可以跟着组件放置后续增加新功能时不需要一直修改一个超长文件。三、props父组件向子组件传数据props用来完成父组件 → 子组件例如App.vue把单条任务传给TodoItem.vueTodoItem :todotodo /子组件接收constpropsdefineProps({todo:{type:Object,required:true,},})数据方向App.vue 中的 todo ↓ props TodoItem.vue 中的 props.todo四、emit子组件向父组件报告事件emit用来完成子组件 → 父组件例如TodoItem.vue中点击删除按钮constemitdefineEmits([remove])functionremoveTodo(){emit(remove,props.todo.id)}父组件监听TodoItem removehandleRemoveTodo /完整过程用户点击删除按钮 ↓ TodoItem emit(remove, id) ↓ App.vue 接收 remove 事件 ↓ App.vue 调用 handleRemoveTodo(id)关键理解emit本身不删除任务它只是通知父组件“用户想删除这条任务”。五、v-model双向绑定普通输入框中t-input v-modelnewTodo /可以理解为两件事的组合t-input :model-valuenewTodo update:model-valuenewTodo $event /也就是父组件把数据传给输入框 输入框变化后通知父组件更新数据组件中的自定义v-model父组件TodoInput v-modelnewTodo /子组件需要接收constpropsdefineProps({modelValue:String,})并发送emit(update:modelValue,value)具名v-model任务编辑使用了TodoItem v-model:editing-texteditingText /它对应propeditingText 事件update:editingText六、插槽slotBaseCard.vue使用了插槽让父组件决定卡片里面显示什么。默认插槽子组件div classcard-content slot / /div父组件BaseCard TodoInput / TodoSummary / /BaseCard具名插槽子组件header slot nameheader / /header父组件BaseCard template #header h1我的 TodoList/h1 /template /BaseCard对应关系#header ↓ slot nameheader七、样式跟随组件迁移我们没有单独把所有 CSS 随便搬到一个文件中而是按照职责迁移BaseCard.vue → 卡片外壳样式 TodoInput.vue → 输入区域样式 TodoItem.vue → 单条任务样式 TodoSummary.vue → 底部统计样式 App.vue → 整体页面布局样式核心原则哪个组件负责哪块结构就让对应样式尽量放在那个组件中。八、组合式函数useTodos最开始我们把 Todo 数据和操作逻辑抽到了src/composables/useTodos.js里面管理todos remainingCount completedCount addTodo toggleTodo removeTodo updateTodo clearCompleted localStorage组合式函数可以理解为把一组相关的响应式数据和函数封装起来供组件使用。const{todos,addTodo,removeTodo,}useTodos()九、为什么后来又学习 PiniauseTodos()已经能完成任务但它更适合局部复用。普通组合式函数中如果状态写在函数内部exportfunctionuseTodos(){consttodosref([])return{todos,}}不同组件分别调用constauseTodos()constbuseTodos()通常会创建两套状态。Pinia 则保证同一个应用中constauseTodoStore()constbuseTodoStore()拿到的是同一个 Store。App.vue ───────────┐ ├── 同一个 Todo Store TodoSummary.vue ───┘十、Pinia 的三个核心概念Pinia 概念当前项目中的例子类似 Vue 概念statetodosref/datagettersremainingCountcomputedactionsaddTodo()普通函数 / methodsStateconsttodosref([])表示需要共享的正式数据。GetterconstremainingCountcomputed((){returntodos.value.filter((todo)!todo.done).length})它不是独立数据而是根据todos计算出来的结果。ActionfunctionaddTodo(text){todos.value.push({id:Date.now(),text,done:false,})}负责修改 Store 数据。十一、创建 Pinia Store核心写法exportconstuseTodoStoredefineStore(todos,(){consttodosref([])constremainingCountcomputed((){returntodos.value.filter((todo)!todo.done).length})functionaddTodo(text){// 添加任务}return{todos,remainingCount,addTodo,}})其中defineStore(todos,...)表示定义一个名称为todos的 Store。useTodoStore()用于在组件中取得这个 Store。十二、storeToRefs从 Store 中获取响应式状态时使用consttodoStoreuseTodoStore()const{todos,remainingCount,completedCount,}storeToRefs(todoStore)storeToRefs()会把 Store 中的 state 和 getter 安全地转换成 ref。操作函数可以直接解构const{addTodo,removeTodo,clearCompleted,}todoStore可以记成状态和 getter → storeToRefs(todoStore) action 函数 → 直接从 todoStore 解构十三、Pinia 的全局共享体验最开始统计数据由App.vue通过 props 传给TodoSummary.vueTodo Store ↓ App.vue ↓ props TodoSummary.vue后来改为Todo Store / \ / \ App.vue TodoSummary.vueTodoSummary.vue自己访问 StoreconsttodoStoreuseTodoStore()const{remainingCount,completedCount,}storeToRefs(todoStore)这样统计数字不再由App.vue逐层传递。十四、并不是所有数据都要放 Pinia今天明确了一个重要原则Pinia 适合共享业务数据但不应该把所有临时状态都塞进去。当前放在 Store 中todos 任务统计 任务增删改查 localStorage继续留在App.vuenewTodo editingId editingText原因是todos是正式业务数据newTodo只是输入框里还没提交的草稿editingId和editingText是当前页面的临时编辑状态。十五、export和importexport表示把当前文件中的变量或函数开放给其他文件使用。exportconstuseTodoStoredefineStore(...)它等价于constuseTodoStoredefineStore(...)export{useTodoStore,}import表示从其他文件中引入已经导出的内容。import{useTodoStore}from./stores/todoStore.js可以记成export从当前文件送出去 import从其他文件拿进来具名导出exportconstuseTodoStore...导入时使用花括号import{useTodoStore}from./todoStore.js默认导出exportdefaultApp导入时不使用花括号importAppfrom./App.vue十六、模板ref的理解模板refh1 reftitleRef {{ title }} /h1它的作用不是显示标题。真正显示标题的是{{ title }}模板ref的作用是让 JavaScript 获取页面中的真实 DOM 元素或子组件。常见用途自动聚焦输入框 滚动到某个位置 测量元素大小 控制 video/audio 调用子组件公开的方法但是当前标题没有直接操作 DOM 的需求因此reftitleRef没有必要可以删除。重要原则普通页面更新优先通过响应式数据完成不要为了使用模板ref而直接操作 DOM。十七、今天形成的完整数据流添加任务TodoInput ↓ emit submit App.vue 执行 submitTodo() ↓ 调用 Store 的 addTodo() ↓ todos 更新 ↓ localStorage 保存 ↓ 页面和统计自动更新切换任务状态TodoItem ↓ emit toggle App.vue 调用 toggleTodo(id) ↓ Store 修改 todo.done ↓ remainingCount / completedCount 重新计算 ↓ TodoSummary 自动更新删除任务TodoItem ↓ emit remove App.vue 执行 handleRemoveTodo(id) ↓ Store 删除任务 ↓ 必要时 App.vue 退出编辑状态清除已完成TodoSummary ↓ emit clear-completed App.vue 检查当前编辑状态 ↓ 调用 Store 的 clearCompleted() ↓ 已完成任务被清除十八、当前项目能力清单完成今天的学习后已经接触并实际使用Vue 单文件组件script setuprefcomputedwatchv-if/v-elsev-for和keypropsemit普通v-model具名v-model默认插槽具名插槽组件拆分样式跟随组件ComposablelocalStoragePiniastoreToRefsexport/import区分全局状态和局部状态。十九、今天最重要的五句话1.props用于父传子emit用于子传父。2.v-model本质上是“传值 接收更新事件”。3.组件拆分要按职责进行样式也应该跟随组件。4.Pinia 让不同组件访问同一份响应式数据。5.不是所有状态都要放 Pinia临时界面状态应该保留在组件中。二十、下一阶段按照当前计划接下来进入TypeScript 基础重点会学习基本类型 数组和对象类型 interface 函数参数和返回值类型 Todo 数据类型 Vue props 类型 Pinia Store 类型最终目标是把当前todoStore.js App.vue 组件 props逐步改造成带类型检查的 Vue TypeScript 项目。