Vue 3 入门实战:从零构建响应式任务管理应用
1. 项目概述为什么现在学Vue依然是个好主意最近几年前端框架的“战火”似乎从未停歇React、Svelte、Solid.js等新秀层出不穷让很多刚入门前端的朋友感到迷茫现在学Vue还来得及吗它是不是过时了作为一个从Vue 2时代一路跟过来的老前端我的答案是不仅来得及而且现在可能是最好的时机。Vue 3的稳定和生态的成熟让它从一个“优雅的渐进式框架”变成了一个“开箱即用的生产力工具”。如果你是一名希望快速上手、构建现代化Web应用的新手或者是从jQuery时代转型过来的开发者Vue以其极低的学习曲线和直观的响应式体验依然是你的不二之选。简单来说Vue的核心是数据驱动视图。你不再需要手动操作DOM文档对象模型去更新页面上的一个数字或一段文本你只需要关心你的数据比如一个叫count的变量当数据变化时Vue会自动、高效地帮你更新对应的视图部分。这种开发模式极大地解放了生产力让你能更专注于业务逻辑本身。本篇文章我将带你从零开始绕过官方文档中那些可能让新手困惑的理论深水区直接进入实战通过构建一个简单的任务管理应用手把手让你感受Vue的魅力。你会发现入门Vue真的没有想象中那么难。1.1 核心需求解析我们到底要解决什么问题在动手写代码之前我们得先想清楚目标。我们计划构建一个个人任务管理工具Todo List。这个项目虽小但“麻雀虽小五脏俱全”它能覆盖Vue入门阶段你需要掌握的绝大多数核心概念数据与视图的绑定如何将JavaScript中的数据任务列表显示到页面上并且当数据变化时页面自动更新。用户交互处理如何响应用户的点击添加任务、标记完成、输入在文本框里打字等操作并触发相应的数据变更。条件与列表渲染如何根据数据状态如任务是否完成来决定显示或隐藏某些元素如何遍历一个数组任务列表并动态生成一组DOM元素。组件化初探如何将页面中可复用的部分如单个任务项抽离成一个独立的、可配置的单元。通过实现这个具体的功能你将直观地理解上述每一个抽象概念是如何落地为一行行代码的。这比单纯阅读概念定义要有效得多。1.2 环境准备与工具选型打造顺手的开发环境工欲善其事必先利其器。对于Vue开发我强烈推荐使用Vite作为构建工具而不是传统的Webpack。原因很简单快。Vite在启动速度和热更新HMR速度上具有碾压性优势能让你获得丝滑的开发体验。我们将采用目前最主流、最推荐的组合Vue 3 Vite TypeScript可选但推荐。操作步骤安装Node.js确保你的电脑上安装了Node.js版本16或以上。你可以在终端输入node -v和npm -v来检查是否安装成功。创建Vue项目打开终端进入你打算存放项目的目录执行以下命令npm create vuelatest这个命令会启动一个交互式的项目创建向导。配置项目选项跟随提示进行选择。对于纯新手我建议做如下配置Project name: 输入你的项目名例如my-vue-todo。Add TypeScript?建议选 Yes。TypeScript能提供更好的代码提示和类型安全是现代前端开发的趋势早接触早受益。Add JSX Support?选 No。我们使用Vue的单文件组件语法。Add Vue Router for Single Page Application?选 No。入门阶段暂不需要路由。Add Pinia for state management?选 No。入门阶段我们使用组件内状态管理即可。Add Vitest for Unit Testing?选 No。可后续再学习。Add an End-to-End Testing Solution?选 No。Add ESLint for code quality?建议选 Yes。它能帮助你保持代码风格一致。进入项目并安装依赖创建完成后按照提示进入项目目录并安装依赖cd my-vue-todo npm install启动开发服务器安装完成后运行npm run dev终端会输出一个本地地址通常是http://localhost:5173用浏览器打开它你会看到一个默认的Vue欢迎页面。恭喜你的Vue开发环境已经搭建成功注意如果在执行npm create vuelatest时速度很慢或失败可能是因为网络问题。你可以尝试配置npm的镜像源为国内镜像如淘宝镜像来加速。命令为npm config set registry https://registry.npmmirror.com2. 核心概念拆解理解Vue的“响应式魔法”在开始写Todo List之前我们需要先弄懂Vue最核心的“响应式系统”。这是Vue的立身之本理解了它你就理解了Vue大半的精髓。2.1 声明式渲染与模板语法传统的JavaScript或jQuery开发是“命令式”的。你需要明确地发出命令“找到那个id为‘app’的div”“清空它的内容”“创建一个新的p标签”“设置它的文本内容为‘Hello’”“把这个p标签插入到div里”。代码冗长且容易出错。Vue采用的是“声明式”渲染。你只需要在HTML模板中声明“这个div的内容应该和JavaScript中的一个叫message的变量保持一致”。至于怎么保持一致Vue帮你搞定。这就是模板语法template div idapp p{{ message }}/p /div /template script setup import { ref } from vue // 创建一个响应式变量 const message ref(Hello Vue!) // 2秒后改变数据视图会自动更新 setTimeout(() { message.value Hello, Updated Vue! }, 2000) /script双大括号{{ }}是Vue最基本的文本插值语法。它告诉Vue这里的内容是动态的需要去script里找对应的message变量来填充。当你修改message.value时p标签里的文本会自动、神奇地更新。你不需要写任何操作DOM的代码。2.2 响应式基础ref和reactive上面代码中的ref是Vue 3创建响应式数据的核心API之一。为什么需要它因为普通的JavaScript变量不是“响应式”的Vue无法追踪它的变化。ref用于创建一个包装了原始值如字符串、数字、布尔值或引用值的响应式引用。访问或修改其值时需要通过.value属性。const count ref(0) // 创建一个响应式数字 console.log(count.value) // 读取0 count.value // 修改Vue能检测到这个变化并更新视图在模板中Vue会自动“解包”.value你可以直接使用{{ count }}。reactive用于创建一个响应式的对象。与ref不同你可以直接访问和修改它的属性无需.value。const state reactive({ count: 0, name: Vue }) console.log(state.count) // 0 state.count // 修改属性同样能被Vue检测到如何选择一个简单的经验法则基础类型用ref对象类型用reactive。但在组合式API中ref因其统一性更受青睐即使是对象也可以用ref。2.3 指令系统给HTML添加超能力指令是带有v-前缀的特殊属性。它们是Vue模板中实现逻辑的主要方式。v-bind动态绑定属性。简写为:。!-- 将img的src属性绑定到变量imageUrl上 -- img :srcimageUrl !-- 根据isActive变量的真假动态添加或移除‘active’这个CSS类 -- div :class{ active: isActive }/divv-on监听DOM事件。简写为。!-- 当按钮被点击时调用increment函数 -- button clickincrement加1/button !-- 监听输入框的input事件事件触发时调用onInput函数 -- input inputonInputv-model在表单元素上创建双向数据绑定。它是v-bind和v-on的语法糖能极大简化表单处理。!-- 输入框的值和text变量双向绑定 -- input v-modeltext !-- 等价于 -- input :valuetext inputevent text event.target.valuev-if/v-else/v-else-if条件渲染。根据表达式的真假值来条件性地渲染一块内容。p v-ifscore 90优秀/p p v-else-ifscore 60及格/p p v-else不及格/pv-for列表渲染。基于一个数组或对象渲染一个列表。必须搭配唯一的:key属性这是Vue高效更新虚拟DOM的关键。ul li v-for(item, index) in items :keyitem.id {{ index }} - {{ item.name }} /li /ul实操心得v-for的:key非常重要务必使用唯一且稳定的标识如数据中的id避免使用索引index作为key除非列表是静态的。错误的key会导致列表更新时出现渲染错误或性能问题。3. 实战一步步构建Todo List应用理论说得再多不如动手写一行代码。现在让我们清空默认项目开始构建我们的任务管理应用。3.1 项目结构初始化与数据定义首先我们打开项目中的src/App.vue文件清空其内容替换为以下基础结构template div classapp-container h1我的任务清单/h1 !-- 后续内容将在这里添加 -- /div /template script setup langts // 这里将编写我们的JavaScript/TypeScript逻辑 /script style scoped .app-container { max-width: 600px; margin: 2rem auto; padding: 2rem; background-color: #f8f9fa; border-radius: 12px; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1); } /style接下来在script setup部分我们定义核心的响应式数据。一个任务todo通常包含id、内容、完成状态。我们将用一个数组来存储所有任务。script setup langts import { ref } from vue // 定义单个任务的类型TypeScript interface TodoItem { id: number text: string completed: boolean } // 响应式数据任务列表 const todos refTodoItem[]([ { id: 1, text: 学习Vue核心概念, completed: true }, { id: 2, text: 搭建开发环境, completed: true }, { id: 3, text: 构建第一个Vue应用, completed: false } ]) // 响应式数据用于绑定新增任务的输入框 const newTodoText ref() /script3.2 实现新增任务功能现在我们需要在模板中添加一个输入框和一个按钮让用户可以输入新任务并添加。template div classapp-container h1我的任务清单/h1 !-- 新增任务区域 -- div classinput-group input typetext v-modelnewTodoText keyup.enteraddTodo placeholder请输入新任务... classtodo-input button clickaddTodo classadd-btn添加/button /div !-- 任务列表区域稍后填充 -- /div /template script setup langts import { ref } from vue // ... 之前的类型和响应式数据定义 ... // 添加新任务的方法 const addTodo () { const trimmedText newTodoText.value.trim() if (!trimmedText) { alert(任务内容不能为空) return } // 向列表头部添加新任务 todos.value.unshift({ id: Date.now(), // 用时间戳生成一个简单唯一的id text: trimmedText, completed: false }) // 清空输入框 newTodoText.value } /script style scoped /* ... 之前的样式 ... */ .input-group { display: flex; gap: 10px; margin-bottom: 2rem; } .todo-input { flex: 1; padding: 12px 16px; border: 2px solid #dee2e6; border-radius: 8px; font-size: 1rem; transition: border-color 0.3s; } .todo-input:focus { outline: none; border-color: #4361ee; } .add-btn { padding: 12px 24px; background-color: #4361ee; color: white; border: none; border-radius: 8px; cursor: pointer; font-weight: bold; transition: background-color 0.3s; } .add-btn:hover { background-color: #3a56d4; } /style代码解析v-modelnewTodoText将输入框与newTodoText变量双向绑定。keyup.enteraddTodo监听输入框的键盘事件当按下回车键时触发addTodo方法。clickaddTodo按钮点击时也触发addTodo方法。在addTodo方法中我们检查输入是否有效然后使用unshift方法将新任务对象添加到todos数组的开头这样新任务会显示在最上面。最后清空输入框。3.3 渲染任务列表与状态切换接下来我们用v-for来渲染任务列表并为每个任务添加完成/未完成切换以及删除功能。template div classapp-container h1我的任务清单/h1 !-- ... 新增任务区域 ... -- !-- 任务列表区域 -- ul classtodo-list v-iftodos.length 0 li v-fortodo in todos :keytodo.id classtodo-item !-- 任务完成状态复选框 -- input typecheckbox :checkedtodo.completed changetoggleTodo(todo.id) classtodo-checkbox !-- 任务文本根据完成状态添加删除线 -- span :class{ completed: todo.completed } classtodo-text {{ todo.text }} /span !-- 删除按钮 -- button clickremoveTodo(todo.id) classdelete-btn×/button /li /ul p v-else classempty-tip暂无任务快去添加一个吧/p /div /template script setup langts import { ref } from vue // ... 之前的类型、数据定义和addTodo方法 ... // 切换任务完成状态 const toggleTodo (id: number) { const todo todos.value.find(t t.id id) if (todo) { todo.completed !todo.completed } } // 删除任务 const removeTodo (id: number) { // 过滤掉id不等于传入id的任务实现删除 todos.value todos.value.filter(todo todo.id ! id) } /script style scoped /* ... 之前的样式 ... */ .todo-list { list-style: none; padding: 0; } .todo-item { display: flex; align-items: center; padding: 16px; margin-bottom: 12px; background-color: white; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: transform 0.2s; } .todo-item:hover { transform: translateY(-2px); } .todo-checkbox { margin-right: 15px; width: 20px; height: 20px; cursor: pointer; } .todo-text { flex: 1; font-size: 1.1rem; } .todo-text.completed { text-decoration: line-through; color: #6c757d; } .delete-btn { background: none; border: none; color: #dc3545; font-size: 1.8rem; cursor: pointer; line-height: 1; padding: 0 8px; border-radius: 4px; } .delete-btn:hover { background-color: #f8d7da; } .empty-tip { text-align: center; color: #6c757d; font-style: italic; padding: 2rem; } /style代码解析v-iftodos.length 0和v-else条件渲染。当有任务时显示列表无任务时显示提示文本。v-fortodo in todos :keytodo.id遍历todos数组为每个任务生成一个li元素。:key绑定为每个任务唯一的id。:checkedtodo.completed将复选框的选中状态与todo.completed属性绑定。changetoggleTodo(todo.id)当复选框状态改变时调用toggleTodo方法并传入当前任务的id。:class{ completed: todo.completed }动态绑定CSS类。如果todo.completed为真则添加completed类实现删除线效果。在toggleTodo方法中我们通过find找到对应的任务对象然后取反其completed属性。由于该对象是响应式的视图会自动更新。在removeTodo方法中我们使用filter方法创建一个不包含指定id任务的新数组并赋值回todos.value。这是Vue中改变数组的推荐方式之一另一种是使用splice。3.4 功能增强筛选与数据持久化一个基本的Todo List已经完成了。但我们还可以让它更实用。3.4.1 添加任务筛选全部/未完成/已完成我们需要一个新的响应式数据来存储当前的筛选状态以及一个计算属性来根据筛选状态返回对应的任务列表。template div classapp-container h1我的任务清单/h1 !-- ... 新增任务区域 ... -- !-- 筛选按钮组 -- div classfilter-buttons button v-forfilter in filters :keyfilter.value clickcurrentFilter filter.value :class{ active: currentFilter filter.value } classfilter-btn {{ filter.label }} /button /div !-- 任务列表区域改为使用filteredTodos -- ul classtodo-list v-iffilteredTodos.length 0 li v-fortodo in filteredTodos :keytodo.id classtodo-item !-- ... 任务项内容保持不变 ... -- /li /ul p v-else classempty-tip {{ currentFilter all ? 暂无任务 : currentFilter active ? 暂无未完成任务 : 暂无已完成任务 }} /p /div /template script setup langts import { ref, computed } from vue // 引入 computed interface TodoItem { id: number text: string completed: boolean } const todos refTodoItem[]([ { id: 1, text: 学习Vue核心概念, completed: true }, { id: 2, text: 搭建开发环境, completed: true }, { id: 3, text: 构建第一个Vue应用, completed: false } ]) const newTodoText ref() // 筛选相关 type FilterType all | active | completed const filters: { label: string; value: FilterType }[] [ { label: 全部, value: all }, { label: 未完成, value: active }, { label: 已完成, value: completed } ] const currentFilter refFilterType(all) // 计算属性根据当前筛选条件返回过滤后的任务列表 const filteredTodos computed(() { switch (currentFilter.value) { case active: return todos.value.filter(todo !todo.completed) case completed: return todos.value.filter(todo todo.completed) default: // all return todos.value } }) // ... 之前的 addTodo, toggleTodo, removeTodo 方法保持不变 ... /script style scoped /* ... 之前的样式 ... */ .filter-buttons { display: flex; justify-content: center; gap: 10px; margin-bottom: 1.5rem; } .filter-btn { padding: 8px 16px; border: 1px solid #ced4da; background-color: white; border-radius: 20px; cursor: pointer; transition: all 0.3s; } .filter-btn.active { background-color: #4361ee; color: white; border-color: #4361ee; } /style代码解析我们定义了filters数组和currentFilter响应式变量来管理筛选状态。filteredTodos是一个计算属性。计算属性是Vue中一个非常重要的概念它基于其依赖的响应式数据这里是todos和currentFilter进行计算并返回一个新的值。只有当其依赖的数据发生变化时它才会重新计算。这比在模板中写复杂逻辑更高效、更清晰。在模板中我们用v-for渲染筛选按钮并通过:class动态添加active样式。列表渲染的数据源从todos改为filteredTodos。3.4.2 数据持久化存到浏览器本地目前刷新页面任务就没了。我们可以利用浏览器的localStorage来实现简单的数据持久化。script setup langts import { ref, computed, watch, onMounted } from vue // 引入 watch 和 onMounted interface TodoItem { id: number text: string completed: boolean } // 从localStorage中读取保存的任务如果没有则使用默认数据 const loadTodosFromStorage (): TodoItem[] { const saved localStorage.getItem(vue-todos) if (saved) { try { return JSON.parse(saved) } catch (e) { console.error(读取本地存储数据失败, e) } } return [ { id: 1, text: 学习Vue核心概念, completed: true }, { id: 2, text: 搭建开发环境, completed: true }, { id: 3, text: 构建第一个Vue应用, completed: false } ] } const todos refTodoItem[](loadTodosFromStorage()) const newTodoText ref() // 使用watch深度监听todos的变化并自动保存到localStorage watch( todos, (newTodos) { localStorage.setItem(vue-todos, JSON.stringify(newTodos)) }, { deep: true } // 深度监听确保对象内部属性变化也能触发 ) // ... 其余的 filters, currentFilter, filteredTodos, 以及各种方法保持不变 ... /script代码解析loadTodosFromStorage函数尝试从localStorage中读取键为vue-todos的数据如果存在且能正确解析就作为初始数据否则使用默认数据。watch是Vue的侦听器。我们用它来深度监听todos的变化。每当todos或其内部任何任务对象的属性发生变化时回调函数就会执行将最新的todos数组序列化成JSON字符串并保存到localStorage。onMounted是生命周期钩子表示组件被挂载到DOM后执行。这里我们用它来在组件初始化时从本地存储加载数据。至此一个功能相对完整、带有数据持久化的Vue Todo List应用就完成了它涵盖了响应式数据、模板语法、指令、事件处理、计算属性、侦听器、生命周期等Vue核心概念。4. 组件化初探让代码更清晰、更易复用我们的App.vue文件现在越来越长了。在实际项目中一个文件里塞太多逻辑会难以维护。Vue的核心优势之一就是组件化。我们可以把“任务项”TodoItem抽离成一个独立的组件。4.1 创建子组件在src/components目录下新建一个文件TodoItem.vue。!-- src/components/TodoItem.vue -- template li classtodo-item input typecheckbox :checkedtodo.completed change$emit(toggle, todo.id) classtodo-checkbox span :class{ completed: todo.completed } classtodo-text {{ todo.text }} /span button click$emit(remove, todo.id) classdelete-btn×/button /li /template script setup langts // 定义组件接收的props属性 interface Props { todo: { id: number text: string completed: boolean } } // 使用defineProps来声明props const props definePropsProps() // 定义组件可以触发的事件 const emit defineEmits{ toggle: [id: number] remove: [id: number] }() /script style scoped /* 样式可以从App.vue中复制过来或者根据需求调整 */ .todo-item { display: flex; align-items: center; padding: 16px; margin-bottom: 12px; background-color: white; border-radius: 8px; box-shadow: 0 2px 5px rgba(0,0,0,0.05); transition: transform 0.2s; } .todo-item:hover { transform: translateY(-2px); } .todo-checkbox { margin-right: 15px; width: 20px; height: 20px; cursor: pointer; } .todo-text { flex: 1; font-size: 1.1rem; } .todo-text.completed { text-decoration: line-through; color: #6c757d; } .delete-btn { background: none; border: none; color: #dc3545; font-size: 1.8rem; cursor: pointer; line-height: 1; padding: 0 8px; border-radius: 4px; } .delete-btn:hover { background-color: #f8d7da; } /style代码解析definePropsProps()在script setup中使用这个编译器宏来定义组件接收的属性。这里我们定义了一个todo属性类型是Props接口。defineEmits定义组件可以触发的自定义事件。这里定义了toggle和remove事件它们都会携带一个id参数。在模板中我们通过props.todo来访问传入的任务数据。当复选框变化或点击删除按钮时使用$emit来触发父组件监听的事件。4.2 在父组件中使用子组件现在修改App.vue使用我们新建的TodoItem组件。!-- App.vue (部分修改) -- template div classapp-container h1我的任务清单/h1 !-- ... 新增任务区域和筛选按钮 ... -- !-- 任务列表区域 -- ul classtodo-list v-iffilteredTodos.length 0 !-- 使用TodoItem组件替代原来的li -- TodoItem v-fortodo in filteredTodos :keytodo.id :todotodo toggletoggleTodo removeremoveTodo / /ul p v-else classempty-tip.../p /div /template script setup langts import { ref, computed, watch, onMounted } from vue // 导入TodoItem组件 import TodoItem from ./components/TodoItem.vue // ... 其余的数据、计算属性、方法定义全部保持不变 ... /script style scoped /* App.vue的样式可以移除.todo-item相关的部分只保留容器样式 */ .app-container { ... } .input-group { ... } .todo-input { ... } .add-btn { ... } .filter-buttons { ... } .filter-btn { ... } .todo-list { list-style: none; padding: 0; } .empty-tip { ... } /style代码解析通过import导入TodoItem组件。在模板中使用TodoItem标签。通过:todotodo将遍历的每个todo对象作为属性传递给子组件。通过toggletoggleTodo和removeremoveTodo监听子组件触发的事件。当子组件内$emit(toggle, id)时父组件的toggleTodo方法就会被调用并接收到子组件传递过来的id。通过组件化App.vue的模板变得非常简洁逻辑也更清晰。TodoItem组件成为一个独立的、可复用的单元。如果以后需要修改任务项的样式或交互只需要改这一个文件即可。5. 常见问题与排查技巧实录在学习和实践Vue的过程中你几乎一定会遇到下面这些问题。这里我整理了最常见的“坑”和解决方法。5.1 响应式数据更新了但视图不更新这是Vue新手最常遇到的问题之一。根本原因在于Vue的响应式系统无法检测到某些特定的数据变更方式。问题场景与解决方案直接通过索引修改数组元素// 错误视图不会更新 todos.value[index].text 新任务 // 正确做法 todos.value.splice(index, 1, { ...todos.value[index], text: 新任务 }) // 或者使用Vue.set (Vue 2) 或 直接替换整个数组 todos.value todos.value.map((item, i) i index ? { ...item, text: 新任务 } : item)直接修改数组长度// 错误 todos.value.length 0 // 正确做法 todos.value []给响应式对象添加新属性const state reactive({ count: 0 }) // 错误新属性name不是响应式的 state.name Vue // 正确做法 state.name Vue // 对于reactive创建的对象Vue 3可以检测到但为了清晰建议 // 或者使用 ref const state ref({ count: 0 }) state.value { ...state.value, name: Vue }核心原则对于ref始终通过.value来修改对于reactive创建的对象确保修改的是已存在的属性或者使用Object.assign或展开运算符返回一个新对象。对于数组使用push,pop,shift,unshift,splice,sort,reverse这些变更原数组的方法或者直接替换整个数组。5.2 控制台警告Component is missing template or render function这个错误通常意味着Vue没有找到组件的模板。检查以下几点单文件组件中确保有template标签。如果使用setup函数返回渲染函数确保函数有返回值。检查组件导入路径是否正确。5.3v-for列表渲染异常或性能低下务必提供唯一的:key。key是Vue识别节点身份的标识使用索引index作为key在列表顺序会变化时会导致渲染错误和性能问题。理想情况下key应该是数据中唯一且稳定的字段如id。避免在v-for内部使用v-if。因为v-for的优先级比v-if高这可能导致不必要的循环计算。如果需要条件渲染列表应该使用计算属性先过滤数据再渲染。5.4 样式不生效或样式污染使用style scoped在单文件组件的style标签上添加scoped属性可以使样式只作用于当前组件避免全局污染。这是Vue提供的非常实用的功能。深度选择器有时你需要修改子组件的样式比如使用第三方组件库。在scoped样式下你可以使用:deep()伪类。/* 假设 .el-input 是子组件内的元素 */ :deep(.el-input) { border-color: red; }5.5 开发服务器运行正常但生产构建后白屏或报错检查路径问题Vite生产构建后静态资源路径可能与开发环境不同。确保项目中的资源引用使用相对路径或Vite提供的import.meta.env.BASE_URL。检查浏览器兼容性Vue 3默认使用现代JavaScript语法。如果你的项目需要支持旧浏览器如IE需要在构建配置中指定目标浏览器或使用相应的polyfill。查看浏览器控制台生产环境构建后错误信息可能被压缩。打开浏览器开发者工具的控制台查看具体的错误信息通常能定位到问题。5.6 TypeScript类型错误如果你按照本文使用了TypeScript可能会遇到类型报错。defineProps或defineEmits报错确保在script标签上设置了langts。在Vue 3.3版本中使用基于类型的声明是推荐方式。导入.vue文件报错确保项目根目录下有env.d.ts文件通常由create-vue自动生成其中包含了对.vue文件的类型声明/// reference typesvite/client / declare module *.vue { import type { DefineComponent } from vue const component: DefineComponent{}, {}, any export default component }走完以上所有步骤你不仅完成了一个功能完整的Vue应用更重要的是你亲手实践了Vue最核心的响应式、指令、组件化等概念。从“知道”到“做到”这中间的鸿沟已经被你跨越。接下来你可以尝试为这个Todo List添加更多功能比如任务编辑、拖拽排序、按日期分类或者尝试将其改造成一个更复杂的应用例如博客管理后台、电商商品列表等。记住最好的学习方式就是不断动手去构建。Vue的官方文档cn.vuejs.org是你永远的最佳伙伴遇到问题时多去文档里找答案。祝你编码愉快