前端vue3快速入门第二日——通过TodoList学习vue

🌈 Vue TodoList 2.0

学习目标: 从一个功能完整但逻辑集中在 App.vue 的 TodoList,逐步整理成一个结构清晰、职责分明、可以继续扩展的小型 Vue 项目。


一、今天完成了什么

今天主要完成了四件事:

  1. 组件拆分:把不同界面区域拆成独立组件。
  2. 逻辑抽离 :把 Todo 业务逻辑从 App.vue 抽到组合式函数。
  3. Pinia 状态管理:把 Todo 数据迁移到全局 Store。
  4. 理解模块化 :学习 exportimportpropsemitv-model、插槽等概念。

二、组件拆分

最开始,TodoList 的输入框、任务列表、编辑功能、统计区域和样式都写在 App.vue 中。

随着功能增多,App.vue 会越来越长,所以我们按照"一个组件负责一类界面功能"的原则进行了拆分。

当前组件结构

text 复制代码
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.js Todo 数据、统计结果、操作函数、本地存储
App.vue 组合页面和协调组件
main.js 创建 Vue 应用,安装 TDesign 和 Pinia

为什么要拆组件

拆组件不是为了让文件数量变多,而是为了让每个文件只负责一件相对明确的事情。

text 复制代码
App.vue
原来:既负责页面,又负责输入、任务、编辑、统计、存储

拆分后:
App.vue 只负责组合
TodoInput.vue 负责输入
TodoItem.vue 负责单条任务
TodoSummary.vue 负责统计
todoStore.js 负责数据和业务

拆分后的好处:

  • 更容易阅读;
  • 某个功能出错时,更容易定位;
  • 组件可以复用;
  • 样式可以跟着组件放置;
  • 后续增加新功能时,不需要一直修改一个超长文件。

三、props:父组件向子组件传数据

props 用来完成:

父组件 → 子组件

例如 App.vue 把单条任务传给 TodoItem.vue

vue 复制代码
<TodoItem :todo="todo" />

子组件接收:

javascript 复制代码
const props = defineProps({
  todo: {
    type: Object,
    required: true,
  },
})

数据方向:

text 复制代码
App.vue 中的 todo
        ↓ props
TodoItem.vue 中的 props.todo

四、emit:子组件向父组件报告事件

emit 用来完成:

子组件 → 父组件

例如 TodoItem.vue 中点击删除按钮:

javascript 复制代码
const emit = defineEmits(['remove'])

function removeTodo() {
  emit('remove', props.todo.id)
}

父组件监听:

vue 复制代码
<TodoItem @remove="handleRemoveTodo" />

完整过程:

text 复制代码
用户点击删除按钮
        ↓
TodoItem emit('remove', id)
        ↓
App.vue 接收 remove 事件
        ↓
App.vue 调用 handleRemoveTodo(id)

关键理解:

emit 本身不删除任务,它只是通知父组件"用户想删除这条任务"。


五、v-model:双向绑定

普通输入框中:

vue 复制代码
<t-input v-model="newTodo" />

可以理解为两件事的组合:

vue 复制代码
<t-input
  :model-value="newTodo"
  @update:model-value="newTodo = $event"
/>

也就是:

text 复制代码
父组件把数据传给输入框
        +
输入框变化后通知父组件更新数据

组件中的自定义 v-model

父组件:

vue 复制代码
<TodoInput v-model="newTodo" />

子组件需要接收:

javascript 复制代码
const props = defineProps({
  modelValue: String,
})

并发送:

javascript 复制代码
emit('update:modelValue', value)

具名 v-model

任务编辑使用了:

vue 复制代码
<TodoItem v-model:editing-text="editingText" />

它对应:

text 复制代码
prop:editingText
事件:update:editingText

六、插槽 slot

BaseCard.vue 使用了插槽,让父组件决定卡片里面显示什么。

默认插槽

子组件:

vue 复制代码
<div class="card-content">
  <slot />
</div>

父组件:

vue 复制代码
<BaseCard>
  <TodoInput />
  <TodoSummary />
</BaseCard>

具名插槽

子组件:

vue 复制代码
<header>
  <slot name="header" />
</header>

父组件:

vue 复制代码
<BaseCard>
  <template #header>
    <h1>我的 TodoList</h1>
  </template>
</BaseCard>

对应关系:

text 复制代码
#header
   ↓
slot name="header"

七、样式跟随组件迁移

我们没有单独把所有 CSS 随便搬到一个文件中,而是按照职责迁移:

text 复制代码
BaseCard.vue
→ 卡片外壳样式

TodoInput.vue
→ 输入区域样式

TodoItem.vue
→ 单条任务样式

TodoSummary.vue
→ 底部统计样式

App.vue
→ 整体页面布局样式

核心原则:

哪个组件负责哪块结构,就让对应样式尽量放在那个组件中。


八、组合式函数 useTodos

最开始,我们把 Todo 数据和操作逻辑抽到了:

text 复制代码
src/composables/useTodos.js

里面管理:

text 复制代码
todos
remainingCount
completedCount
addTodo
toggleTodo
removeTodo
updateTodo
clearCompleted
localStorage

组合式函数可以理解为:

把一组相关的响应式数据和函数封装起来,供组件使用。

javascript 复制代码
const {
  todos,
  addTodo,
  removeTodo,
} = useTodos()

九、为什么后来又学习 Pinia

useTodos() 已经能完成任务,但它更适合局部复用。

普通组合式函数中,如果状态写在函数内部:

javascript 复制代码
export function useTodos() {
  const todos = ref([])

  return {
    todos,
  }
}

不同组件分别调用:

javascript 复制代码
const a = useTodos()
const b = useTodos()

通常会创建两套状态。

Pinia 则保证同一个应用中:

javascript 复制代码
const a = useTodoStore()
const b = useTodoStore()

拿到的是同一个 Store。

text 复制代码
App.vue ───────────┐
                   ├── 同一个 Todo Store
TodoSummary.vue ───┘

十、Pinia 的三个核心概念

Pinia 概念 当前项目中的例子 类似 Vue 概念
state todos ref / data
getters remainingCount computed
actions addTodo() 普通函数 / methods

State

javascript 复制代码
const todos = ref([])

表示需要共享的正式数据。

Getter

javascript 复制代码
const remainingCount = computed(() => {
  return todos.value.filter((todo) => !todo.done).length
})

它不是独立数据,而是根据 todos 计算出来的结果。

Action

javascript 复制代码
function addTodo(text) {
  todos.value.push({
    id: Date.now(),
    text,
    done: false,
  })
}

负责修改 Store 数据。


十一、创建 Pinia Store

核心写法:

javascript 复制代码
export const useTodoStore = defineStore('todos', () => {
  const todos = ref([])

  const remainingCount = computed(() => {
    return todos.value.filter((todo) => !todo.done).length
  })

  function addTodo(text) {
    // 添加任务
  }

  return {
    todos,
    remainingCount,
    addTodo,
  }
})

其中:

javascript 复制代码
defineStore('todos', ...)

表示定义一个名称为 todos 的 Store。

javascript 复制代码
useTodoStore()

用于在组件中取得这个 Store。


十二、storeToRefs

从 Store 中获取响应式状态时使用:

javascript 复制代码
const todoStore = useTodoStore()

const {
  todos,
  remainingCount,
  completedCount,
} = storeToRefs(todoStore)

storeToRefs() 会把 Store 中的 state 和 getter 安全地转换成 ref。

操作函数可以直接解构:

javascript 复制代码
const {
  addTodo,
  removeTodo,
  clearCompleted,
} = todoStore

可以记成:

text 复制代码
状态和 getter
→ storeToRefs(todoStore)

action 函数
→ 直接从 todoStore 解构

十三、Pinia 的全局共享体验

最开始统计数据由 App.vue 通过 props 传给 TodoSummary.vue

text 复制代码
Todo Store
    ↓
App.vue
    ↓ props
TodoSummary.vue

后来改为:

text 复制代码
             Todo Store
              /      \
             /        \
        App.vue   TodoSummary.vue

TodoSummary.vue 自己访问 Store:

javascript 复制代码
const todoStore = useTodoStore()

const {
  remainingCount,
  completedCount,
} = storeToRefs(todoStore)

这样统计数字不再由 App.vue 逐层传递。


十四、并不是所有数据都要放 Pinia

今天明确了一个重要原则:

Pinia 适合共享业务数据,但不应该把所有临时状态都塞进去。

当前放在 Store 中:

text 复制代码
todos
任务统计
任务增删改查
localStorage

继续留在 App.vue

text 复制代码
newTodo
editingId
editingText

原因是:

  • todos 是正式业务数据;
  • newTodo 只是输入框里还没提交的草稿;
  • editingIdeditingText 是当前页面的临时编辑状态。

十五、exportimport

export

表示:

把当前文件中的变量或函数开放给其他文件使用。

javascript 复制代码
export const useTodoStore = defineStore(...)

它等价于:

javascript 复制代码
const useTodoStore = defineStore(...)

export {
  useTodoStore,
}

import

表示:

从其他文件中引入已经导出的内容。

javascript 复制代码
import { useTodoStore } from './stores/todoStore.js'

可以记成:

text 复制代码
export:从当前文件送出去
import:从其他文件拿进来

具名导出

javascript 复制代码
export const useTodoStore = ...

导入时使用花括号:

javascript 复制代码
import { useTodoStore } from './todoStore.js'

默认导出

javascript 复制代码
export default App

导入时不使用花括号:

javascript 复制代码
import App from './App.vue'

十六、模板 ref 的理解

模板 ref

vue 复制代码
<h1 ref="titleRef">
  {{ title }}
</h1>

它的作用不是显示标题。

真正显示标题的是:

vue 复制代码
{{ title }}

模板 ref 的作用是:

让 JavaScript 获取页面中的真实 DOM 元素或子组件。

常见用途:

text 复制代码
自动聚焦输入框
滚动到某个位置
测量元素大小
控制 video/audio
调用子组件公开的方法

但是当前标题没有直接操作 DOM 的需求,因此:

vue 复制代码
ref="titleRef"

没有必要,可以删除。

重要原则:

普通页面更新优先通过响应式数据完成,不要为了使用模板 ref 而直接操作 DOM。


十七、今天形成的完整数据流

添加任务

text 复制代码
TodoInput
    ↓ emit submit
App.vue 执行 submitTodo()
    ↓
调用 Store 的 addTodo()
    ↓
todos 更新
    ↓
localStorage 保存
    ↓
页面和统计自动更新

切换任务状态

text 复制代码
TodoItem
    ↓ emit toggle
App.vue 调用 toggleTodo(id)
    ↓
Store 修改 todo.done
    ↓
remainingCount / completedCount 重新计算
    ↓
TodoSummary 自动更新

删除任务

text 复制代码
TodoItem
    ↓ emit remove
App.vue 执行 handleRemoveTodo(id)
    ↓
Store 删除任务
    ↓
必要时 App.vue 退出编辑状态

清除已完成

text 复制代码
TodoSummary
    ↓ emit clear-completed
App.vue 检查当前编辑状态
    ↓
调用 Store 的 clearCompleted()
    ↓
已完成任务被清除

十八、当前项目能力清单

完成今天的学习后,已经接触并实际使用:

  • Vue 单文件组件;
  • <script setup>
  • ref
  • computed
  • watch
  • v-if / v-else
  • v-forkey
  • props
  • emit
  • 普通 v-model
  • 具名 v-model
  • 默认插槽;
  • 具名插槽;
  • 组件拆分;
  • 样式跟随组件;
  • Composable;
  • localStorage
  • Pinia;
  • storeToRefs
  • export / import
  • 区分全局状态和局部状态。

十九、今天最重要的五句话

1. props 用于父传子,emit 用于子传父。
2. v-model 本质上是"传值 + 接收更新事件"。
3. 组件拆分要按职责进行,样式也应该跟随组件。
4. Pinia 让不同组件访问同一份响应式数据。
5. 不是所有状态都要放 Pinia,临时界面状态应该保留在组件中。


二十、下一阶段

按照当前计划,接下来进入:

text 复制代码
TypeScript 基础

重点会学习:

text 复制代码
基本类型
数组和对象类型
interface
函数参数和返回值类型
Todo 数据类型
Vue props 类型
Pinia Store 类型

最终目标是把当前:

text 复制代码
todoStore.js
App.vue
组件 props

逐步改造成带类型检查的 Vue + TypeScript 项目。

相关推荐
IT_陈寒15 小时前
Vite静态资源路径这个坑差点让我加班到凌晨
前端·人工智能·后端
掘金酱15 小时前
「TRAE Work 实战帮」征文启动!你沉淀的经验,值得被看见!
前端·人工智能·后端
橙子家15 小时前
Windows 上同时安装多个 node 版本
前端
晓说前端15 小时前
TypeScript 高级特性 —— 类型断言与泛型
前端·typescript
大龄秃头程序员15 小时前
iOS 客户端视角扫盲:WKWebView 里 window、messageHandlers 与 Native 回调到底怎么工作?
javascript
爱勇宝15 小时前
DeepSeek V4-Flash 更新:代码与 Agent 能力全面增强
前端·后端·deepseek
GuWenyue15 小时前
90%前端写React+TS都踩坑!从组件类型、单向数据流到本地存储完整实战
前端·react.js
妙码生花16 小时前
从 PHP 到 AI + Golang,程序员自救转型手记(四十五):前端远程下拉输入组件
前端·javascript·vue.js
明月_清风16 小时前
全栈工程师必会技术栈:从入门到架构的完整成长地图 🗺️
前端·后端·全栈