1. 从“Hello World”到构建现代前端应用:为什么是Vue?
如果你刚踏入前端开发的大门,或者是从 jQuery 时代一路走来的开发者,面对如今 React、Vue、Angular 三大框架的“神仙打架”,可能会感到一丝迷茫。我最初接触 Vue 时,也是抱着试试看的心态,但很快就被它的“渐进式”理念和上手速度所吸引。简单来说,Vue 的核心价值在于:它允许你从一个简单的页面交互开始,平滑地演进为一个功能完整、结构清晰的大型单页应用(SPA)。这与一些“全家桶”式框架要求你一开始就接受其完整哲学和复杂工具链的做法截然不同。
看看那些热搜词:“vue安装及环境配置”、“vue项目实战”、“vue面试题”……这恰恰反映了 Vue 生态的两个核心面向:极低的学习曲线和强大的企业级应用能力。新手关心如何快速跑起来,而资深开发者则关注性能优化、架构设计和工程化实践。Vue 通过其清晰的核心库(只关注视图层)和灵活可选的官方配套工具(如 Vue Router、Vuex/Pinia、Vite),成功地覆盖了这两个群体。当你用{{ message }}在页面上渲染出第一个变量时,你已经踏入了响应式数据绑定的世界;而当你开始用单文件组件(.vue文件)组织代码,并搭配 Vue Router 管理路由时,你已经在构建一个现代化前端应用的路上。
2. 环境搭建与第一个Vue应用:不只是“npm install”
很多教程会把环境搭建一笔带过,但这里恰恰是新手容易卡住的地方。我们避开复杂的祖传历史,直接从现代工具链开始。
2.1 核心工具选型:Node.js、包管理器与构建工具
首先,你需要 Node.js 运行环境。这不是因为 Vue 本身依赖 Node.js(运行时并不依赖),而是因为现代前端开发离不开 npm(或 yarn、pnpm)这个包管理工具,以及像 Vite 这样的构建工具。去 Node.js 官网下载 LTS(长期支持)版本安装即可。
包管理器我强烈推荐pnpm。相比 npm 和 yarn,pnpm 通过硬链接和符号链接的方式存储依赖,能极大节省磁盘空间,并且安装速度更快,能有效避免“幽灵依赖”问题。安装完 Node.js 后,可以通过 npm 全局安装 pnpm:npm install -g pnpm。
接下来是构建工具。官方推荐并已默认使用的是Vite。它是一个基于原生 ES 模块的构建工具,开发服务器启动速度极快,热更新(HMR)效率也远超传统的 Webpack。对于新手而言,你不需要理解其底层原理,只需要享受它带来的流畅开发体验。
2.2 创建项目的两种姿势:CLI 与 手动初始化
最主流、最省心的方式是使用 Vue 官方的命令行工具(CLI):@vue/cli或更现代的create-vue。
方式一:使用create-vue(官方推荐)这是基于 Vite 的下一代项目脚手架。打开终端,执行:
pnpm create vue@latest这条命令会下载并执行create-vue,启动一个交互式的项目创建向导。你会被询问一系列问题:
- 项目名称
- 是否添加 TypeScript 支持?(对于新手,可以先选 No,但强烈建议后期学习并引入)
- 是否添加 JSX 支持?(通常用于需要灵活渲染逻辑的组件,初期可不选)
- 是否添加 Vue Router 进行单页面应用开发?(建议选 Yes,这是构建 SPA 的核心)
- 是否添加 Pinia 进行状态管理?(对于简单应用可不选,中大型应用建议选 Yes)
- 是否添加测试工具?(如 Vitest、Cypress,可根据需要选择)
- 是否添加 ESLint 用于代码规范?(建议选 Yes,保持代码风格统一)
向导结束后,按照提示进入项目目录,安装依赖并启动开发服务器:
cd your-project-name pnpm install pnpm dev此时,访问http://localhost:5173(Vite 默认端口),你就看到了你的第一个 Vue 应用。
方式二:手动初始化(用于深入理解)如果你想知道一个最简 Vue 项目是如何组成的,可以手动创建:
- 新建一个项目文件夹,初始化
package.json:pnpm init -y。 - 安装 Vue:
pnpm add vue。 - 安装 Vite 及 Vue 插件:
pnpm add -D vite @vitejs/plugin-vue。 - 创建
vite.config.js文件,配置 Vue 插件。 - 创建
index.html作为入口,通过<script type="module">引入你的主 JS 文件。 - 创建
src/main.js或src/main.ts,使用createApp函数创建 Vue 应用实例并挂载到 DOM。
这种方式步骤繁琐,但能让你清晰看到从零到一的过程。对于学习和调试底层问题有帮助,但对于日常开发,强烈推荐使用create-vue。
注意:很多教程会提到 Vue CLI(
@vue/cli),它基于 Webpack,功能强大且成熟,但配置相对复杂,启动和热更新速度不如 Vite。对于新项目,除非有特定的 Webpack 生态依赖,否则无脑选择 Vite +create-vue是更优解。
2.3 理解项目结构:文件与职责
使用create-vue创建的项目,其核心结构如下:
your-project/ ├── node_modules/ # 项目依赖包 ├── public/ # 静态资源(如图标),会被直接复制到构建输出目录 ├── src/ # 源代码目录 │ ├── assets/ # 模块化资源(如图片、样式),会经过构建处理 │ ├── components/ # 可复用 Vue 组件 │ ├── views/ # 页面级组件(通常与路由对应) │ ├── router/ # Vue Router 路由配置(如果选择了) │ ├── stores/ # Pinia 状态管理仓库(如果选择了) │ ├── App.vue # 应用根组件 │ └── main.js # 应用入口文件 ├── index.html # 页面模板,Vite 注入构建后的资源 ├── package.json # 项目依赖和脚本配置 ├── vite.config.js # Vite 构建配置文件 └── ... # 其他配置文件(如 ESLint, .gitignore)关键文件解读:
index.html:这是应用的“壳”,<div id="app"></div>是 Vue 应用的挂载点。注意,Vite 环境下,你可以直接使用 ES 模块语法引入src/main.js。src/main.js:应用入口。这里使用createApp工厂函数从根组件App.vue创建应用实例,并调用.mount(‘#app’)将其挂载到 DOM 上。如果配置了路由或状态管理,也会在这里进行安装。src/App.vue:根组件。它是所有其他组件的容器,通常包含顶层的布局(如导航栏)和<router-view>(用于渲染当前路由对应的页面组件)。
3. Vue 核心概念深度解析:不止于模板语法
很多人学 Vue 是从模板语法开始的,这没错,但理解其背后的响应式原理和组件化思想更为关键。
3.1 声明式渲染与响应式系统:数据如何驱动视图?
Vue 的核心是一个响应式系统。我们通过一个简单的例子来看:
<template> <div> <p>{{ message }}</p> <button @click="reverseMessage">反转消息</button> </div> </template> <script setup> import { ref } from 'vue'; const message = ref('Hello Vue!'); function reverseMessage() { message.value = message.value.split('').reverse().join(''); } </script>{{ message }}:这是“Mustache”语法(双大括号),用于数据绑定。它告诉 Vue:这里需要显示message的值。ref(‘Hello Vue!’):ref函数创建一个响应式引用。它接收一个内部值,返回一个响应式的、可变的 ref 对象,该对象只有一个.value属性指向这个内部值。在模板中,Vue 会自动“解包” ref,所以我们直接写{{ message }}而不是{{ message.value }}。但在 JavaScript 中,我们需要通过.value来访问或修改它。@click=“reverseMessage”:@是v-on指令的简写,用于监听 DOM 事件。这里监听了按钮的点击事件,并调用reverseMessage方法。- 当我们点击按钮,
reverseMessage函数修改了message.value。由于message是一个响应式 ref,Vue 能够追踪到这个变化,并自动更新所有依赖message的 DOM 部分(即那个<p>标签)。这就是“数据驱动视图”。
为什么是ref和reactive?Vue 3 提供了两种创建响应式数据的主要 API:ref和reactive。
ref:常用于定义基本类型(字符串、数字、布尔值)的响应式数据,也可以定义对象或数组。它返回一个带有.value属性的对象。在模板中自动解包,在 JS 中需要.value访问。reactive:用于定义对象类型(包括数组)的响应式数据。它返回一个原始对象的 Proxy 代理。访问和修改属性可以直接像普通对象一样操作,无需.value。
选择建议:
- 定义单个的、独立的基本类型数据,用
ref。 - 定义一组逻辑上相关联的数据(如表单对象、配置对象),用
reactive。 - 在组合式函数(Composables)中返回响应式数据时,通常使用
ref,因为它更容易在解构后保持响应性。
3.2 条件渲染与列表渲染:v-if与v-for的细节
条件渲染v-ifvsv-show:
v-if是“真正的”条件渲染。它会确保在切换过程中,条件块内的事件监听器和子组件适当地被销毁和重建。v-if也是惰性的:如果在初始渲染时条件为假,则什么也不做,直到条件第一次变为真时才会开始渲染条件块。v-show就简单得多——不管初始条件是什么,元素总是会被渲染,并且只是简单地基于 CSS 的display属性进行切换。- 如何选择:如果需要非常频繁地切换,使用
v-show较好,因为切换开销小(只是修改 CSS)。如果在运行时条件很少改变,或者需要确保条件块内的组件状态被销毁/重建,则使用v-if更合适。
列表渲染v-for:v-for指令基于一个数组或对象来渲染一个列表。必须使用:key这个特殊的 attribute 来给每个节点提供一个唯一的标识。
<template> <ul> <li v-for="item in items" :key="item.id"> {{ item.text }} </li> </ul> </template>:key是必须的。它帮助 Vue 识别每个节点的身份,从而重用和重新排序现有元素。key的值必须是唯一标识符,理想情况下是数据项本身的唯一 ID(如item.id)。切勿使用数组索引index作为key,除非你渲染的列表是静态的(永不重新排序或过滤),否则会导致渲染错误和性能问题。v-for和v-if避免在同一节点上使用。因为v-for的优先级比v-if更高,这意味着v-if将分别重复运行于每个v-for循环中,可能不是你想要的。如果目的是有条件地跳过循环的执行,可以将v-if置于外层元素(或<template>标签)上。
3.3 计算属性与侦听器:何时用哪个?
这是 Vue 响应式系统中处理衍生数据的两大利器。
计算属性 (computed): 用于声明式地描述一个依赖其他响应式数据的值。计算属性是基于它们的响应式依赖进行缓存的。只在相关响应式依赖发生改变时它们才会重新求值。
<script setup> import { ref, computed } from 'vue'; const firstName = ref('张'); const lastName = ref('三'); // 计算属性 const fullName = computed(() => { return firstName.value + ' ' + lastName.value; }); // 当 firstName 或 lastName 改变时,fullName 会自动更新 </script>使用场景:当你需要根据一个或多个响应式数据计算出一个新的值,并且这个计算可能比较耗时或者你希望缓存结果时。
侦听器 (watch和watchEffect): 用于在响应式数据变化时执行有“副作用”的代码(如发起异步请求、操作 DOM、打印日志等)。
watch:需要明确指定要侦听的数据源和一个回调函数。它更精确,可以获取变化前后的值。
<script setup> import { ref, watch } from 'vue'; const question = ref(''); const answer = ref(''); // 侦听一个 ref watch(question, async (newQuestion, oldQuestion) => { if (newQuestion.includes('?')) { answer.value = '思考中...'; try { // 模拟异步请求 const res = await fetch('https://yesno.wtf/api'); const data = await res.json(); answer.value = data.answer; } catch (error) { answer.value = '错误:' + error.message; } } }); </script>watchEffect:立即运行一个函数,同时响应式地追踪其依赖,并在依赖变更时重新执行。你不需要明确指定侦听源,它会自动追踪回调函数中用到的所有响应式属性。
<script setup> import { ref, watchEffect } from 'vue'; const count = ref(0); watchEffect(() => { console.log(`count 的值是:${count.value}`); // 这里如果还使用了其他 ref,也会被自动追踪 }); // 立即打印 “count 的值是:0” </script>如何选择:
- 如果你需要在数据变化时执行异步操作或开销较大的操作,并需要基于变化前后的值进行逻辑处理,用
watch。 - 如果你需要根据一些响应式状态自动应用副作用,并且不关心具体是哪个属性变了,或者副作用依赖多个属性,用
watchEffect更简洁。 - 一个重要的区别:
watch是惰性的,默认不会在侦听器创建时立即执行回调,除非你设置{ immediate: true }。而watchEffect会立即执行一次。
4. 组件化开发:构建可复用的砖块
组件是 Vue 应用的基石。理解如何创建、使用和通信组件至关重要。
4.1 单文件组件 (SFC):.vue文件的魔法
一个.vue文件(单文件组件)将模板、逻辑和样式封装在一个文件中,这是 Vue 的标志性特性。
<!-- MyComponent.vue --> <template> <div class="my-component"> <h2>{{ title }}</h2> <slot></slot> <!-- 插槽,用于接收父组件传入的内容 --> </div> </template> <script setup> // 使用 <script setup> 语法糖,更简洁 import { defineProps } from 'vue'; // 定义组件接收的属性 const props = defineProps({ title: { type: String, required: true } }); </script> <style scoped> /* `scoped` 属性使样式仅作用于当前组件 */ .my-component { border: 1px solid #ccc; padding: 1rem; } </style><template>:包含组件的 HTML 模板。所有 Vue 指令和绑定在这里生效。<script setup>:使用组合式 API 的编译时语法糖。在这个标签内声明的顶级变量、函数、import 都会自动暴露给模板。defineProps、defineEmits等编译器宏无需导入即可使用,代码非常简洁。<style scoped>:带scoped的 CSS。Vue 会通过给元素添加唯一属性(如><!-- 父组件 Parent.vue --> <template> <ChildComponent :user-name="name" :age="userAge" /> </template> <script setup> import ChildComponent from './ChildComponent.vue'; import { ref } from 'vue'; const name = ref('李四'); const userAge = ref(25); </script><!-- 子组件 ChildComponent.vue --> <template> <div>姓名:{{ userName }}, 年龄:{{ age }}</div> </template> <script setup> defineProps({ userName: String, age: Number }); </script>注意:Props 是只读的。子组件不应该直接修改接收到的 prop。如果子组件需要基于 prop 的值做出改变,应该定义一个本地的 data property 或计算属性。
2. 子传父:自定义事件 (Emits)子组件通过触发自定义事件,将数据传递回父组件。父组件监听这些事件并做出响应。
<!-- 子组件 ChildComponent.vue --> <template> <button @click="$emit('enlarge-text', 0.1)">放大文字</button> <!-- 或者在 <script setup> 中定义 --> <button @click="handleClick">另一种方式</button> </template> <script setup> // 定义组件可以触发的事件 const emit = defineEmits(['enlarge-text']); function handleClick() { emit('enlarge-text', 0.2); // 触发事件,并传递参数 } </script><!-- 父组件 Parent.vue --> <template> <ChildComponent @enlarge-text="onEnlargeText" /> <p :style="{ fontSize: postFontSize + 'em' }">一段文字</p> </template> <script setup> import { ref } from 'vue'; import ChildComponent from './ChildComponent.vue'; const postFontSize = ref(1); function onEnlargeText(enlargeAmount) { postFontSize.value += enlargeAmount; } </script>3. 跨层级组件通信:Provide / Inject对于深度嵌套的组件,如果层层传递 props 非常麻烦。这时可以用
provide和inject。一个父组件相对于其所有的后代组件,可以作为依赖提供者。任何后代组件,无论层级多深,都可以注入由父组件提供的依赖。<!-- 祖先组件 Ancestor.vue --> <script setup> import { provide, ref } from 'vue'; const theme = ref('dark'); provide('theme', theme); // 提供响应式数据 </script><!-- 深层后代组件 Descendant.vue --> <script setup> import { inject } from 'vue'; const theme = inject('theme'); // 注入 // 现在可以使用 theme.value 了 </script>提示:为了确保注入方和提供方之间的数据流向清晰,最好将
provide的值设置为一个响应式对象(如ref或reactive),这样后代组件注入后,当提供方的数据变化时,注入方也能响应式地更新。对于应用级的全局状态(如用户信息),更推荐使用专门的状态管理库 Pinia。4.3 插槽 (Slots):内容分发的艺术
插槽让组件变得更加灵活,允许父组件向子组件模板的指定位置插入任意内容。
- 默认插槽:子组件中用
<slot></slot>定义占位符,父组件传入的内容会替换它。 - 具名插槽:子组件中定义多个带名字的插槽(
<slot name=“header”>),父组件使用<template v-slot:header>或#header来指定内容插入哪个插槽。 - 作用域插槽:子组件可以将自身的数据通过插槽的 attribute 传递给父组件,父组件在插槽内容中使用这些数据。这是实现可复用渲染逻辑的强力工具,例如用于封装数据获取和遍历逻辑的列表组件。
<!-- 子组件 FancyList.vue --> <template> <ul> <li v-for="item in items" :key="item.id"> <!-- 将 item 对象作为 slotProps 传递出去 --> <slot name="item" :item="item">{{ item.defaultText }}</slot> </li> </ul> </template> <script setup> defineProps({ items: Array }); </script><!-- 父组件使用 --> <template> <FancyList :items="items"> <!-- 接收子组件传递的 slotProps,命名为 `props` --> <template #item="props"> <div class="fancy-item"> {{ props.item.text }} - 来自父组件的渲染 </div> </template> </FancyList> </template>5. 路由与状态管理:构建单页应用 (SPA) 的骨架
当应用变得复杂,多个视图和共享状态的管理就提上日程。
5.1 Vue Router:管理你的页面视图
Vue Router 是 Vue.js 官方的路由管理器。它和 Vue.js 的核心深度集成,让构建单页应用变得轻而易举。
基本配置: 在
src/router/index.js中:import { createRouter, createWebHistory } from 'vue-router'; import HomeView from '../views/HomeView.vue'; const routes = [ { path: '/', // 路径 name: 'home', // 路由名称,可用于编程式导航 component: HomeView // 对应的组件 }, { path: '/about', name: 'about', // 路由级代码分割,生成一个单独的 chunk component: () => import('../views/AboutView.vue') }, { path: '/user/:id', // 动态路由参数 name: 'user', component: () => import('../views/UserView.vue'), props: true // 将路由参数 `params.id` 作为 prop 传递给组件 } ]; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), // 使用 HTML5 History 模式 routes }); export default router;- History 模式 vs Hash 模式:
createWebHistory使用 HTML5history.pushStateAPI,URL 看起来更干净(如example.com/about)。但需要服务器配置支持,确保所有路径都返回index.html。createWebHashHistory使用 URL hash(如example.com/#/about),无需服务器额外配置,但 URL 中有#,不太美观。 - 动态路由匹配:路径中的冒号
:标记动态段(如:id)。在组件中,可以通过$route.params.id访问,或者如果设置了props: true,可以直接通过defineProps([‘id’])接收。 - 编程式导航:除了使用
<router-link>组件,还可以在 JS 中使用router.push(‘/about’)或router.push({ name: ‘user’, params: { id: 123 } })进行导航。 - 导航守卫:用于在路由跳转前、后或跳转过程中进行控制。例如,可以在全局前置守卫
router.beforeEach中进行用户身份验证。
5.2 Pinia:新一代的状态管理
Vuex 曾是 Vue 的官方状态管理库,但 Vue 3 时代更推荐使用Pinia。它更简洁,类型支持更好,且与组合式 API 配合得天衣无缝。
核心概念:
- Store:一个保存状态和业务逻辑的实体。每个 Store 都是独立的。
- State:Store 中保存的响应式数据。
- Getters:相当于 Store 的计算属性,用于派生状态。
- Actions:相当于组件中的 methods,用于封装业务逻辑,可以是异步的。
创建一个 Store(
src/stores/counter.js):import { defineStore } from 'pinia'; import { ref, computed } from 'vue'; // `defineStore` 的第一个参数是 Store 的唯一 ID export const useCounterStore = defineStore('counter', () => { // State const count = ref(0); // Getter const doubleCount = computed(() => count.value * 2); // Action function increment() { count.value++; } async function fetchData() { // 异步 action const response = await fetch('/api/data'); // ... 更新 state } return { count, doubleCount, increment, fetchData }; });在组件中使用:
<template> <div> <p>Count: {{ store.count }}</p> <p>Double: {{ store.doubleCount }}</p> <button @click="store.increment()">+1</button> </div> </template> <script setup> import { useCounterStore } from '@/stores/counter'; const store = useCounterStore(); // 或者使用解构,但为了保持响应性,需要使用 storeToRefs // import { storeToRefs } from 'pinia'; // const { count, doubleCount } = storeToRefs(store); </script>Pinia 的 Store 可以直接在组件中像使用一个组合式函数一样使用,非常直观。状态修改的逻辑集中在 Store 的 Actions 中,使得组件逻辑更清晰,也便于复用和测试。
6. 工程化、性能与进阶实践
掌握了核心概念和基本开发流程后,要写出健壮、可维护的应用,还需要关注工程化和性能。
6.1 代码组织与风格指南
随着项目变大,统一的代码风格和结构至关重要。
- 使用 ESLint + Prettier:在创建项目时选择它们。ESLint 负责检查代码质量问题,Prettier 负责代码格式化。配置保存时自动格式化,能极大提升团队协作效率。
- 组件命名:多单词命名(如
UserProfile),避免与 HTML 元素冲突。单文件组件文件名使用 PascalCase(大驼峰),如MyComponent.vue。 - Props 定义:尽量使用对象形式,定义类型和默认值,增加可读性和健壮性。
- 目录结构:按功能而非类型组织文件。例如,将某个功能相关的组件、Composable、Store 放在同一个功能文件夹下,而不是把所有组件都扔进一个巨大的
components目录。
6.2 性能优化要点
- 列表渲染 Key:重申一遍,为
v-for设置唯一且稳定的key,这是 Vue 高效更新虚拟 DOM 的基础。 - 计算属性缓存:对于依赖其他响应式数据且计算成本较高的值,务必使用
computed进行缓存。 - 避免不必要的组件渲染:使用
v-once渲染静态内容;对于复杂组件,如果其 props 在父组件更新时未变化,可以使用v-memo(Vue 3.2+)或手动实现shouldComponentUpdate逻辑(通过组合式 API 的watch和条件渲染)。 - 异步组件与路由懒加载:使用
defineAsyncComponent或路由配置中的component: () => import(‘…’)来分割代码,实现按需加载,减少初始包体积。 - 合理使用
ref和reactive:对于不需要响应式的数据,不要用它们包裹,减少不必要的依赖追踪开销。 - 侦听器的清理:在组件卸载前,如果使用了
watch或watchEffect侦听了外部资源(如定时器、事件总线、DOM 事件),记得在组件的生命周期钩子onUnmounted中清理它们,防止内存泄漏。
6.3 组合式 API (Composables) 封装
这是 Vue 3 组合式 API 的精髓所在。你可以将可复用的响应式逻辑抽取为“组合式函数”。
// src/composables/useMouse.js import { ref, onMounted, onUnmounted } from 'vue'; export function useMouse() { const x = ref(0); const y = ref(0); function update(event) { x.value = event.pageX; y.value = event.pageY; } onMounted(() => window.addEventListener('mousemove', update)); onUnmounted(() => window.removeEventListener('mousemove', update)); return { x, y }; }在组件中使用:
<script setup> import { useMouse } from '@/composables/useMouse'; const { x, y } = useMouse(); </script> <template> 鼠标位置:{{ x }}, {{ y }} </template>通过这种方式,可以像搭积木一样组合各种功能逻辑,极大提高了代码的复用性和可维护性。这也是解决“Vue 与 React 算法对比”这类问题的关键——Vue 的组合式 API 提供了与 React Hooks 类似的能力,但在依赖追踪和心智模型上有所不同,避免了 Hooks 的闭包陷阱和调用顺序限制。
7. 常见问题与避坑指南
这里汇总了开发中高频出现的问题和容易踩的坑。
7.1 响应式数据更新了,视图为什么不更新?
这是新手最常见的问题,根本原因在于你修改数据的方式没有触发 Vue 的响应式系统。
- 对于
reactive对象:- 直接给整个对象重新赋值会丢失响应性。
state = newObject❌ - 正确做法是修改属性:
state.someProp = newValue✅,或者使用Object.assign(state, newObject)来合并属性。 - 直接通过索引设置数组项或修改数组长度不会触发视图更新:
arr[0] = newValue❌,arr.length = 0❌。 - 正确做法是使用数组的变更方法(
push,pop,shift,unshift,splice,sort,reverse)或使用arr.splice(0, 1, newValue)。
- 直接给整个对象重新赋值会丢失响应性。
- 对于
ref:在 JS 中操作时,务必通过.value属性。count = 5❌,count.value = 5✅。 - 对于从
reactive或ref中解构出来的基本类型值:它们会失去响应性。如果需要解构并保持响应性,请使用toRefs或toRef。import { reactive, toRefs } from 'vue'; const state = reactive({ a: 1, b: 2 }); // 错误:解构后 a, b 不再是响应式 // let { a, b } = state; // 正确:使用 toRefs let { a, b } = toRefs(state); // 现在 a.value, b.value 是响应式的
7.2 生命周期钩子使用时机
组合式 API 的生命周期钩子需要显式导入,并以
onXxx的形式在setup中同步调用。<script setup> import { onMounted, onUpdated, onUnmounted } from 'vue'; onMounted(() => { // 组件挂载后执行,适合操作 DOM、发起异步请求、订阅事件 console.log('组件挂载了'); }); onUpdated(() => { // 组件更新后执行,小心无限循环! console.log('组件更新了'); }); onUnmounted(() => { // 组件卸载前执行,必须清理副作用!如清除定时器、取消事件监听、取消未完成的网络请求。 console.log('组件即将卸载'); }); </script>重要提示:
onMounted不会保证所有子组件也都挂载完毕。如果需要等待整个视图都渲染完毕,可以在nextTick中操作。- 在
onUpdated中修改响应式状态可能导致无限更新循环,务必小心。 - 清理副作用是必须的!在
onUnmounted中清理在onMounted或setup中创建的订阅、事件监听器、定时器等,是防止内存泄漏的关键。
7.3 开发与调试工具
- Vue Devtools:浏览器扩展,是开发 Vue 应用的必备神器。它可以检查组件树、查看组件状态和 props、追踪事件、进行时间旅行调试等。确保在开发环境中正确安装和启用。
- Vite 热更新 (HMR):Vite 提供了极快的热更新。但有时你可能遇到热更新失效的情况,通常是因为:
- 代码语法错误导致 HMR 更新失败。
- 状态管理库(如 Pinia)的 Store 不是通过
defineStore定义的,导致 HMR 无法识别。 - 某些特殊的代码模式可能超出了 Vite 的 HMR 处理范围。可以尝试手动刷新页面。
7.4 部署上线注意事项
- 构建命令:运行
pnpm run build(或npm run build),Vite 会在项目根目录生成一个dist文件夹,里面是优化和压缩后的静态资源。 - 路由 History 模式:如果使用了
createWebHistory,你需要配置生产服务器,将所有非静态文件请求重定向到index.html(Fallback)。对于 Nginx,配置如下:location / { try_files $uri $uri/ /index.html; } - 公共路径 (
publicPath):如果项目不是部署在域名的根路径下(例如https://example.com/my-app/),需要在vite.config.js中配置base: ‘/my-app/’。 - 环境变量:使用
.env文件管理不同环境(开发、生产)的变量。以VITE_开头的变量会在构建时被静态替换,并可以通过import.meta.env.VITE_XXX在客户端代码中访问。敏感信息切勿放在客户端可访问的环境变量中。
从“Hello World”到构建一个结构清晰、性能良好的 Vue 应用,这条路需要一步步扎实地走。Vue 的优雅和渐进性让它成为前端入门和进阶的绝佳选择。关键在于多动手,从一个小项目开始,逐步引入路由、状态管理、组合式函数,在实践中去理解和解决遇到的问题。遇到报错时,仔细阅读控制台信息,善用 Vue Devtools 进行调试,大部分问题都能找到答案。
- 默认插槽:子组件中用