ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

Vue 3 极简速成指南:从零构建任务管理器实战

Vue 3 极简速成指南:从零构建任务管理器实战 最近在带新人上手前端项目时发现很多同学对 Vue 3 望而却步觉得组合式 API、响应式系统这些概念太复杂。其实Vue 3 的核心思想非常直观掌握几个关键点就能快速上手开发。本文旨在用最短的路径带你从零构建一个可运行的 Vue 3 应用并附上完整的源码。无论你是想快速了解 Vue 3 的新特性还是需要为一个小型项目搭建前端这篇“极简速成”指南都能让你即学即用。1. Vue 3 核心概念速览为什么是现在在深入代码之前理解 Vue 3 的设计哲学至关重要。Vue 3 并非对 Vue 2 的简单升级而是一次围绕“更好的开发体验”和“更强的性能”的重构。1.1 组合式 API (Composition API)这是 Vue 3 最标志性的变化。在 Vue 2 的选项式 API 中一个组件的逻辑如数据、方法、计算属性被分散在data、methods、computed等不同选项中。当组件变得复杂时追踪一个功能的完整逻辑需要在这些选项间来回跳转代码可读性和可维护性下降。组合式 API 允许你将与同一个逻辑功能相关的代码响应式状态、计算属性、函数组织在一起放在一个setup函数或script setup语法糖中。这使得代码更易于理解和复用。1.2 响应式系统重构Vue 3 使用Proxy替代了 Vue 2 中的Object.defineProperty来实现响应式。这带来了两大好处更好的性能Proxy可以拦截对象的各种操作包括属性的添加和删除无需像 Vue 2 那样递归遍历对象并劫持每一个属性。更全面的响应性可以原生地监听数组索引和长度的变化以及动态添加的对象属性。1.3 更小的体积与更优的 Tree-shakingVue 3 的源代码采用了模块化设计许多非核心功能如过渡动画组件Transition、v-model指令的修饰符逻辑被设计为可选的。配合现代构建工具如 Vite、Webpack最终打包的产物只会包含你实际用到的代码有效减小了应用体积。1.4 更好的 TypeScript 支持Vue 3 的代码库完全使用 TypeScript 重写提供了更完善的类型定义。配合组合式 API开发者能获得近乎原生的 TypeScript 开发体验包括自动补全和类型推断。2. 环境准备极简起步方案我们选择Vite作为构建工具它由 Vue 作者尤雨溪开发启动速度极快提供了最优的 Vue 开发体验。2.1 环境要求Node.js: 请确保已安装 14.18 或 16 版本。你可以在终端运行node -v检查。包管理器: 可以使用npm随 Node.js 安装或yarn、pnpm。本文使用npm进行演示。2.2 创建项目打开你的终端命令行工具执行以下命令# 使用 npm 6.x npm create vitelatest my-vue3-app --template vue # 使用 npm 7需要额外的双横线 npm create vitelatest my-vue3-app -- --template vue # 使用 yarn yarn create vite my-vue3-app --template vue # 使用 pnpm pnpm create vite my-vue3-app --template vue命令执行后按照提示进入项目目录并安装依赖cd my-vue3-app npm install2.3 项目结构预览创建完成后你的项目结构大致如下my-vue3-app/ ├── node_modules/ # 项目依赖包 ├── public/ # 静态资源如图标 ├── src/ # 源代码目录 │ ├── assets/ # 图片、样式等资源 │ ├── components/ # 可复用组件 │ │ └── HelloWorld.vue │ ├── App.vue # 应用根组件 │ └── main.js # 应用入口文件 ├── index.html # 页面入口模板 ├── package.json # 项目配置和依赖声明 ├── vite.config.js # Vite 配置文件 └── ... # 其他配置文件2.4 启动开发服务器在项目根目录运行npm run dev终端会输出一个本地服务器地址通常是http://localhost:5173。在浏览器中打开它你将看到 Vue 3 的默认欢迎页面。至此开发环境已就绪。3. 核心语法从script setup开始Vue 3 推荐使用script setup语法糖它让组合式 API 的书写变得极其简洁。我们通过修改src/App.vue来学习。3.1 响应式状态ref与reactive在 Vue 3 中你需要显式地声明响应式数据。ref: 用于声明基本类型如字符串、数字、布尔值的响应式数据或引用任何值。在模板和setup作用域内需要通过.value来访问其值在模板中会自动解包无需.value。reactive: 用于声明一个响应式的对象。!-- src/App.vue -- script setup // 1. 导入所需的 API import { ref, reactive, computed, watch } from vue // 2. 使用 ref 声明响应式基本类型数据 const count ref(0) const message ref(Hello Vue 3!) // 3. 使用 reactive 声明响应式对象 const userInfo reactive({ name: 张三, age: 25, job: 前端工程师 }) // 4. 定义一个修改状态的方法 function increment() { count.value // 注意在 JavaScript 中需要 .value } function updateUser() { userInfo.age 1 // 直接修改 reactive 对象的属性 userInfo.job 高级 userInfo.job } /script template div h1{{ message }}/h1 !-- 模板中直接使用 count无需 .value -- p计数器的值是{{ count }}/p button clickincrement点我加一/button hr h2用户信息/h2 p姓名{{ userInfo.name }}/p p年龄{{ userInfo.age }}/p p职业{{ userInfo.job }}/p button clickupdateUser更新用户信息/button /div /template style scoped /* 样式可以写在这里scoped 表示样式只作用于当前组件 */ button { margin: 5px; padding: 8px 16px; } /style关键点script setup中的代码会被编译成组件setup()函数的内容。所有顶层的导入、变量和函数都会自动暴露给模板无需return。模板中的click是v-on:click的简写。3.2 计算属性与侦听器计算属性 (computed): 基于响应式依赖进行缓存的计算值。只有当依赖发生变化时它才会重新计算。侦听器 (watch): 用于在响应式数据变化时执行“副作用”操作如发起请求、操作 DOM。script setup import { ref, computed, watch } from vue const firstName ref(张) const lastName ref(三) // 计算属性全名 const fullName computed(() { return ${firstName.value}${lastName.value} }) // 侦听器监听 firstName 的变化 watch(firstName, (newValue, oldValue) { console.log(firstName 从 ${oldValue} 变成了 ${newValue}) // 这里可以执行一些异步操作比如根据名字搜索 }) // 立即执行的侦听器 watch( () userInfo.age, // 监听 reactive 对象的某个属性需要用一个 getter 函数 (newAge) { console.log(用户年龄更新为${newAge}) }, { immediate: true } // 选项立即以当前值执行一次回调 ) /script template div input v-modelfirstName placeholder姓 input v-modellastName placeholder名 p你的全名是{{ fullName }}/p /div /template3.3 条件渲染与列表渲染这部分语法与 Vue 2 基本一致但需要了解在script setup下的配合使用。script setup import { ref } from vue const showMessage ref(true) const todoList ref([ { id: 1, text: 学习 Vue 3, done: true }, { id: 2, text: 写一个 Demo, done: false }, { id: 3, text: 发布文章, done: false } ]) function toggleDone(todo) { todo.done !todo.done } /script template div !-- 条件渲染 -- button clickshowMessage !showMessage切换显示/button p v-ifshowMessage你好世界/p p v-else消息被隐藏了。/p !-- 列表渲染 -- h3待办事项列表/h3 ul !-- 为每个项提供一个唯一的 key这对 Vue 的高效更新至关重要 -- li v-fortodo in todoList :keytodo.id clicktoggleDone(todo) span :style{ textDecoration: todo.done ? line-through : none } {{ todo.text }} /span ({{ todo.done ? 已完成 : 未完成 }}) /li /ul /div /template4. 组件通信Props 与 Emits组件化是 Vue 的核心。组件间的数据流主要通过 Props父传子和 Emits子传父实现。4.1 子组件接收 Props 与触发事件我们创建一个ChildComponent.vue。!-- src/components/ChildComponent.vue -- script setup // 1. 使用 defineProps 编译器宏来声明 props // 编译器宏无需导入在 script setup 中自动可用 const props defineProps({ title: { type: String, required: true // 必传 }, count: { type: Number, default: 0 // 默认值 } }) // 2. 使用 defineEmits 编译器宏来声明要触发的事件 const emit defineEmits([updateCount, reset]) function handleClick() { // 触发事件并传递新值给父组件 const newCount props.count 1 emit(updateCount, newCount) } function handleReset() { emit(reset) } /script template div classchild h3子组件/h3 p接收到的标题{{ title }}/p p接收到的计数{{ count }}/p button clickhandleClick子组件按钮 (1)/button button clickhandleReset重置/button /div /template style scoped .child { border: 1px solid #ccc; padding: 20px; margin-top: 20px; } /style4.2 父组件传递 Props 与监听事件修改App.vue使用刚才创建的子组件。!-- src/App.vue -- script setup import { ref } from vue // 导入子组件在模板中可以直接使用 ChildComponent / import ChildComponent from ./components/ChildComponent.vue const parentCount ref(10) const childTitle ref(来自父组件的问候) // 处理子组件触发的事件 function handleUpdateCount(newCount) { parentCount.value newCount console.log(子组件传来了新的计数${newCount}) } function handleResetFromChild() { parentCount.value 0 console.log(子组件要求重置) } /script template div h1父组件/h1 p父组件自己的计数{{ parentCount }}/p !-- 使用子组件 -- !-- 传递 props -- !-- 监听子组件触发的自定义事件 -- ChildComponent :titlechildTitle :countparentCount update-counthandleUpdateCount resethandleResetFromChild / /div /template关键点在script setup中组件导入后无需注册即可在模板中使用。使用defineProps和defineEmits编译器宏来声明 props 和 emits它们具有完整的类型推断。父组件监听子组件事件时建议使用kebab-case如update-count与子组件中emit(updateCount)的camelCase形式对应这是 Vue 的约定。5. 生命周期与模板引用5.1 生命周期钩子组合式 API 提供了形如onMounted、onUpdated、onUnmounted等生命周期函数它们需要从vue中导入。script setup import { ref, onMounted, onUnmounted } from vue const timer ref(null) const seconds ref(0) onMounted(() { console.log(组件挂载完成开始计时) timer.value setInterval(() { seconds.value }, 1000) }) onUnmounted(() { console.log(组件即将卸载清除计时器) if (timer.value) { clearInterval(timer.value) } }) /script template p页面已加载 {{ seconds }} 秒/p /template5.2 模板引用有时你需要直接访问 DOM 元素或子组件实例。使用ref可以创建一个模板引用。script setup import { ref, onMounted } from vue // 1. 创建一个同名的 ref const inputRef ref(null) const childComponentRef ref(null) onMounted(() { // 3. 在组件挂载后ref 会被填充 if (inputRef.value) { inputRef.value.focus() // 让输入框自动获得焦点 console.log(输入框的值为, inputRef.value.value) } if (childComponentRef.value) { // 访问子组件实例如果子组件使用了 script setup默认是关闭的需通过 defineExpose 暴露 console.log(子组件实例, childComponentRef.value) } }) function handleClick() { inputRef.value.style.backgroundColor lightyellow } /script template div !-- 2. 通过 ref 属性绑定 -- input refinputRef typetext placeholder我会自动聚焦 button clickhandleClick高亮输入框/button !-- 引用子组件 -- ChildComponent refchildComponentRef / /div /template注意使用script setup的组件默认是“关闭”的即外部无法通过模板引用访问其内部声明的变量。如果需要在父组件中访问子组件需使用defineExpose编译器宏显式暴露。!-- 在子组件中 -- script setup import { ref } from vue const internalState ref(秘密) const publicMethod () { console.log(这是一个公开的方法) } // 暴露给父组件 defineExpose({ internalState, publicMethod }) /script6. 综合实战构建一个极简任务管理器现在我们将运用以上所有知识构建一个具备增、删、改、查、过滤和持久化功能的极简任务管理器。6.1 项目结构src/ ├── components/ │ └── TaskItem.vue # 单个任务项组件 ├── App.vue # 根组件 └── main.js6.2 核心逻辑实现 (App.vue)!-- src/App.vue -- script setup import { ref, computed, onMounted, onUnmounted } from vue import TaskItem from ./components/TaskItem.vue // 响应式状态 const newTaskText ref() const tasks ref([]) const filter ref(all) // all, active, completed // 从 localStorage 加载任务 function loadTasks() { const saved localStorage.getItem(vue3-tasks) if (saved) { tasks.value JSON.parse(saved) } } // 保存任务到 localStorage function saveTasks() { localStorage.setItem(vue3-tasks, JSON.stringify(tasks.value)) } // 添加新任务 function addTask() { const text newTaskText.value.trim() if (text) { tasks.value.push({ id: Date.now(), // 简单的时间戳作为ID text, completed: false }) newTaskText.value saveTasks() } } // 删除任务 function deleteTask(id) { tasks.value tasks.value.filter(task task.id ! id) saveTasks() } // 切换任务完成状态 function toggleTask(id) { const task tasks.value.find(t t.id id) if (task) { task.completed !task.completed saveTasks() } } // 计算属性过滤后的任务列表 const filteredTasks computed(() { switch (filter.value) { case active: return tasks.value.filter(t !t.completed) case completed: return tasks.value.filter(t t.completed) default: // all return tasks.value } }) // 计算属性未完成的任务数量 const activeTaskCount computed(() { return tasks.value.filter(t !t.completed).length }) // 清除所有已完成任务 function clearCompleted() { tasks.value tasks.value.filter(t !t.completed) saveTasks() } // 生命周期组件挂载时加载数据 onMounted(() { loadTasks() console.log(任务管理器已加载) }) // 生命周期组件卸载前可以做一些清理本例中不需要 onUnmounted(() { console.log(任务管理器已卸载) }) /script template div classtodo-app h1Vue 3 任务管理器/h1 !-- 新增任务 -- div classadd-task input v-modelnewTaskText keyup.enteraddTask placeholder输入新任务按回车添加 classtask-input button clickaddTask classadd-btn添加/button /div !-- 过滤选项 -- div classfilters button :class{ active: filter all } clickfilter all 全部 /button button :class{ active: filter active } clickfilter active 未完成 ({{ activeTaskCount }}) /button button :class{ active: filter completed } clickfilter completed 已完成 /button button clickclearCompleted classclear-btn 清除已完成 /button /div !-- 任务列表 -- ul classtask-list v-iffilteredTasks.length 0 TaskItem v-fortask in filteredTasks :keytask.id :tasktask toggletoggleTask deletedeleteTask / /ul p v-else classempty-tip {{ filter all ? 还没有任务快去添加一个吧 : 没有符合条件的任务。 }} /p !-- 状态提示 -- footer classapp-footer p总计 {{ tasks.length }} 个任务其中 {{ activeTaskCount }} 个未完成。/p p数据已自动保存到浏览器本地存储。/p /footer /div /template style scoped .todo-app { max-width: 500px; margin: 40px auto; padding: 20px; border: 1px solid #e0e0e0; border-radius: 8px; font-family: sans-serif; } .add-task { display: flex; margin-bottom: 20px; } .task-input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px; font-size: 16px; } .add-btn, .clear-btn { margin-left: 10px; padding: 10px 15px; background-color: #42b883; color: white; border: none; border-radius: 4px; cursor: pointer; } .clear-btn { background-color: #ff6b6b; } .filters { margin-bottom: 20px; } .filters button { margin-right: 10px; padding: 5px 10px; border: 1px solid #ddd; background: white; cursor: pointer; } .filters button.active { background-color: #35495e; color: white; } .task-list { list-style: none; padding: 0; } .empty-tip { text-align: center; color: #888; padding: 20px; } .app-footer { margin-top: 30px; padding-top: 15px; border-top: 1px dashed #eee; font-size: 0.9em; color: #666; } /style6.3 任务项子组件 (TaskItem.vue)!-- src/components/TaskItem.vue -- script setup // 定义接收的 props const props defineProps({ task: { type: Object, required: true } }) // 定义要触发的事件 const emit defineEmits([toggle, delete]) function handleToggle() { emit(toggle, props.task.id) } function handleDelete() { if (confirm(确定要删除任务“${props.task.text}”吗)) { emit(delete, props.task.id) } } /script template li classtask-item input typecheckbox :checkedtask.completed changehandleToggle classtask-checkbox span :class{ completed: task.completed } classtask-text {{ task.text }} /span button clickhandleDelete classdelete-btn title删除 × /button /li /template style scoped .task-item { display: flex; align-items: center; padding: 12px 10px; border-bottom: 1px solid #f0f0f0; } .task-item:hover { background-color: #f9f9f9; } .task-checkbox { margin-right: 12px; transform: scale(1.2); } .task-text { flex: 1; font-size: 16px; transition: color 0.2s; } .task-text.completed { color: #999; text-decoration: line-through; } .delete-btn { background: none; border: none; color: #ff6b6b; font-size: 24px; cursor: pointer; opacity: 0.6; padding: 0 8px; line-height: 1; } .delete-btn:hover { opacity: 1; } /style6.4 运行与效果保存所有文件后开发服务器会自动热重载。打开浏览器你将看到一个功能完整的任务管理器。你可以添加任务、标记完成/未完成、按状态过滤、删除任务并且所有数据都会自动保存在浏览器的 LocalStorage 中刷新页面也不会丢失。7. 常见问题与排查思路在学习和使用 Vue 3 的过程中你可能会遇到以下典型问题问题现象可能原因解决思路模板中显示[object Object]直接在模板中打印了整个对象而非其属性。使用{{ obj.property }}访问具体属性或使用JSON.stringify(obj)查看。修改了ref的值但视图不更新1. 直接修改了ref的.value属性内部的某个字段对于对象/数组。2. 使用了非响应式数据替换。1. 对于对象/数组应使用reactive包装或确保通过.value整体替换。2. 确保数据源是ref或reactive创建的。子组件方法或属性通过ref访问不到使用script setup的组件默认是关闭的。在子组件中使用defineExpose显式暴露需要访问的属性或方法。事件监听 (event) 不生效1. 子组件中事件名拼写错误。2. 子组件未正确触发事件 (emit)。3. 事件名大小写问题。1. 检查子组件defineEmits和emit调用。2. 父组件监听时使用kebab-case如my-event。使用v-for时控制台警告“Duplicate keys”列表项的:key绑定值不唯一或未绑定。确保v-for循环中每个项都有一个唯一且稳定的key通常使用数据中的id字段。开发服务器 (npm run dev) 无法启动或端口占用1. 默认端口 (5173) 被占用。2. 依赖安装不完整或损坏。1. 终止占用端口的进程或修改vite.config.js中的server.port配置。2. 删除node_modules和package-lock.json重新运行npm install。8. 最佳实践与工程建议当你掌握了基础准备将 Vue 3 用于更正式的项目时请关注以下实践8.1 代码组织逻辑关注点分离利用组合式 API 的优势将与同一功能相关的ref、computed、watch、函数等组织在一起。对于复杂逻辑可以提取到独立的组合式函数 (Composables)中实现逻辑复用。组合式函数示例将上述任务管理器的任务状态和操作提取到useTaskManager.js中。// src/composables/useTaskManager.js import { ref, computed, onMounted } from vue export function useTaskManager() { const tasks ref([]) const filter ref(all) const filteredTasks computed(() { switch (filter.value) { case active: return tasks.value.filter(t !t.completed) case completed: return tasks.value.filter(t t.completed) default: return tasks.value } }) function loadTasks() { /* ... */ } function saveTasks() { /* ... */ } function addTask(text) { /* ... */ } // ... 其他函数 onMounted(loadTasks) return { tasks, filter, filteredTasks, addTask, // ... 返回其他需要暴露的状态和方法 } }然后在组件中直接使用script setup import { useTaskManager } from ./composables/useTaskManager const { tasks, filter, filteredTasks, addTask } useTaskManager() /script8.2 状态管理对于简单的跨组件状态共享可以使用Provide/Inject或全局状态管理库如 Pinia。Pinia 是 Vue 官方推荐的状态管理库比 Vuex 更简洁且完美支持组合式 API。对于中小型应用优先考虑使用组合式函数配合 Provide/Inject仅在状态逻辑非常复杂时引入 Pinia。8.3 性能优化合理使用computed和watchcomputed适用于派生状态有缓存watch适用于执行副作用。避免在computed中进行异步操作或产生副作用。列表渲染务必加key这能帮助 Vue 高效地更新虚拟 DOM。使用v-once和v-memo对于永远不会改变的静态内容使用v-once。对于需要条件跳过多余更新的部分模板Vue 3.2 提供了v-memo。组件懒加载对于路由组件或非首屏关键组件使用defineAsyncComponent进行懒加载减小初始包体积。8.4 类型安全 (TypeScript)强烈建议在项目中使用 TypeScript。在创建 Vite 项目时可以选择vue-ts模板npm create vitelatest my-app -- --template vue-ts。使用script setup langts并配合defineProps和defineEmits的泛型参数可以获得完美的类型推断和检查。8.5 生产部署开发完成后运行npm run build命令进行构建。Vite 会将项目打包到dist目录。你可以将dist目录下的静态文件部署到任何静态文件服务器如 Nginx、Apache、Netlify、Vercel 等。如果应用需要后端 API确保配置正确的代理或 API 基础地址。在vite.config.js中可配置server.proxy解决开发环境跨域。通过以上步骤你不仅学会了 Vue 3 的基础用法还掌握了一个完整小项目的开发流程。接下来你可以尝试添加更多功能如任务编辑、分类标签、拖拽排序或者将其与后端 API 连接构建一个全栈应用。Vue 3 的生态非常丰富Vue Router 4 用于路由管理Pinia 用于状态管理Vite 用于构建它们共同构成了现代 Vue 开发的基石。动手实践是学习的最佳途径现在就基于这个速成指南开始你的 Vue 3 项目吧。如果在实践中遇到具体问题多查阅官方文档和社区讨论大部分坑都有成熟的解决方案。
返回列表