Appearance
章节4 — Vue3 组件化开发
一、章节概述
Vue 3 是目前最流行的前端框架之一。本章聚焦组合式 API(Composition API),深入讲解响应式原理(Proxy)、组件通信方式、Vue Router 路由配置与导航守卫,以及 Pinia 状态管理,帮助学员建立现代组件化开发思维。
二、学习目标
| 目标分类 | 具体目标 |
|---|---|
| 知识目标 | 理解组合式 API 与选项式 API 的区别;理解 Proxy 响应式原理与 Vue2 的差异 |
| 技能目标 | 能使用 ref / reactive / computed 开发组件;能配置 Vue Router 并实现路由守卫;能用 Pinia 管理全局状态 |
| 素质目标 | 培养组件化、声明式编程思维,理解单向数据流原则 |
三、核心知识点
3.1 Vue3 组合式 API(Composition API)
3.1.1 setup 函数
setup 是组合式 API 的入口,在组件创建之前执行。
vue
<script>
import { ref, reactive, computed, onMounted } from "vue";
export default {
// setup 接收 props 和 context
setup(props, { emit, slots, attrs }) {
// 在这里定义响应式状态和方法
// 返回的对象暴露给模板
return { /* ... */ };
},
};
</script>3.1.2 <script setup> 语法糖(推荐)
Vue 3.2+ 推荐的简化写法,顶层所有变量和方法自动暴露给模板。
vue
<script setup>
import { ref, reactive, computed, onMounted } from "vue";
// 顶层 await — 组件自动成为异步组件
const data = await fetch("/api/data").then(r => r.json());
</script>
<template>
<div>{{ data }}</div>
</template>3.1.3 ref 与 reactive
| API | 类型支持 | 访问方式 | 适用场景 |
|---|---|---|---|
ref(value) | 任意类型 | 脚本中 .value,模板自动解包 | 基本类型、单个值 |
reactive(obj) | 仅对象/数组 | 直接访问(深层响应式) | 复杂的嵌套对象 |
vue
<script setup>
import { ref, reactive } from "vue";
// ref — 基本类型
const count = ref(0);
const message = ref("你好");
function increment() {
count.value++; // 脚本中必须 .value
}
// ref — 对象(底层也是 reactive)
const user = ref({ name: "Alice", age: 25 });
console.log(user.value.name); // 需要 .value
// reactive — 对象
const state = reactive({
list: [],
loading: false,
error: null,
});
// reactive 直接访问
state.loading = true;
state.list.push({ id: 1, title: "学习 Vue3" });
</script>
<template>
<!-- 模板中自动解包,不用 .value -->
<p>{{ count }} — {{ message }}</p>
<p>{{ state.loading }} — {{ state.list.length }}</p>
<button @click="increment">+1</button>
</template>提醒:
reactive不可重新赋值(会失去响应式);ref可重新赋值。对象首选ref更灵活。
3.1.4 computed 计算属性
vue
<script setup>
import { ref, computed } from "vue";
const todos = ref([
{ text: "学习 HTML", done: true },
{ text: "学习 CSS", done: false },
{ text: "学习 JavaScript", done: false },
]);
// 只读计算属性
const doneCount = computed(() => todos.value.filter(t => t.done).length);
const progress = computed(() =>
`${doneCount.value} / ${todos.value.length}`
);
// 可写计算属性(极少用)
const allDone = computed({
get: () => todos.value.every(t => t.done),
set: (value) => {
todos.value.forEach(t => (t.done = value));
},
});
</script>
<template>
<p>进度:{{ progress }}</p>
<label>
<input type="checkbox" v-model="allDone" />
全部完成
</label>
</template>3.1.5 生命周期钩子
| Vue2 选项式 | Vue3 组合式 | 时机 |
|---|---|---|
beforeCreate | —(直接写在 setup 中) | 组件初始化 |
created | —(直接写在 setup 中) | 组件创建 |
beforeMount | onBeforeMount | DOM 挂载前 |
mounted | onMounted | DOM 挂载后 |
beforeUpdate | onBeforeUpdate | 数据更新前 |
updated | onUpdated | 数据更新后 |
beforeUnmount | onBeforeUnmount | 卸载前 |
unmounted | onUnmounted | 卸载后 |
vue
<script setup>
import { onMounted, onUnmounted, ref } from "vue";
const data = ref(null);
onMounted(async () => {
data.value = await fetch("/api/data").then(r => r.json());
});
onUnmounted(() => {
console.log("组件已卸载,清理工作");
});
</script>3.2 响应式原理(Proxy)
3.2.1 Vue2 vs Vue3 响应式
| 对比维度 | Vue2(Object.defineProperty) | Vue3(Proxy) |
|---|---|---|
| 拦截方式 | 逐个定义属性 | 代理整个对象 |
| 新增/删除属性 | ❌ 无法检测(需 $set / $delete) | ✅ 自动检测 |
| 数组索引/长度 | ❌ 无法检测 | ✅ 自动检测 |
| 性能 | 初始化递归遍历 | 懒代理(使用时才代理) |
| 兼容性 | IE11+ | 现代浏览器 |
3.2.2 Proxy 基本原理
javascript
// 模拟 Vue3 响应式
const handler = {
get(target, key, receiver) {
console.log(`读取 ${String(key)}`);
// 收集依赖(track)
return Reflect.get(target, key, receiver);
},
set(target, key, value, receiver) {
console.log(`设置 ${String(key)} =`, value);
const result = Reflect.set(target, key, value, receiver);
// 触发更新(trigger)
return result;
},
deleteProperty(target, key) {
console.log(`删除 ${String(key)}`);
// 触发更新
return Reflect.deleteProperty(target, key);
},
};
const state = new Proxy({ count: 0 }, handler);
state.count; // 读取 count
state.count = 10; // 设置 count = 10
delete state.count; // 删除 count理解即可:Vue3 内部通过
reactive对对象使用Proxy,ref对基本类型包装为{ value: ... }再代理。
3.3 组件通信
3.3.1 父子通信:props / emit
vue
<!-- Child.vue -->
<script setup>
// 定义 props
const props = defineProps({
title: { type: String, required: true },
count: { type: Number, default: 0 },
items: { type: Array, default: () => [] },
});
// 定义 emits
const emit = defineEmits(["update", "delete"]);
function handleUpdate() {
emit("update", { id: props.items[0]?.id });
}
</script>
<template>
<div class="child">
<h3>{{ title }}</h3>
<p>数量:{{ count }}</p>
<button @click="handleUpdate">更新</button>
<button @click="emit('delete', 1)">删除</button>
</div>
</template>vue
<!-- Parent.vue -->
<script setup>
import { ref } from "vue";
import Child from "./Child.vue";
const items = ref([{ id: 1, name: "项目A" }]);
function onUpdate(payload) {
console.log("收到更新:", payload);
}
</script>
<template>
<Child
title="子组件标题"
:count="items.length"
:items="items"
@update="onUpdate"
@delete="(id) => items = items.filter(i => i.id !== id)"
/>
</template>3.3.2 依赖注入:provide / inject
适合深层嵌套的祖先 ↔ 后代通信,避免逐层传 props。
vue
<!-- Ancestor.vue -->
<script setup>
import { provide, ref } from "vue";
const theme = ref("light");
const user = ref({ name: "管理员" });
// 提供响应式数据
provide("theme", theme);
provide("user", user);
// 提供修改方法
provide("toggleTheme", () => {
theme.value = theme.value === "light" ? "dark" : "light";
});
</script>vue
<!-- DeepChild.vue -->
<script setup>
import { inject } from "vue";
const theme = inject("theme", "light"); // 第二个参数为默认值
const user = inject("user");
const toggleTheme = inject("toggleTheme");
</script>
<template>
<div :class="`app-${theme}`">
<p>当前用户:{{ user?.name }}</p>
<button @click="toggleTheme">切换主题</button>
</div>
</template>3.3.3 v-model 双向绑定(组件版)
vue
<!-- InputComponent.vue -->
<script setup>
const model = defineModel(); // Vue 3.4+ 语法糖
// 等价于:
// const props = defineProps(['modelValue']);
// const emit = defineEmits(['update:modelValue']);
// const model = computed({
// get: () => props.modelValue,
// set: (v) => emit('update:modelValue', v),
// });
</script>
<template>
<input v-model="model" placeholder="输入内容..." />
</template>vue
<!-- Parent -->
<script setup>
import { ref } from "vue";
import InputComponent from "./InputComponent.vue";
const text = ref("");
</script>
<template>
<InputComponent v-model="text" />
<p>你输入了:{{ text }}</p>
</template>3.4 Vue Router 路由配置与导航守卫
3.4.1 基本配置
javascript
// router/index.js
import { createRouter, createWebHistory } from "vue-router";
import Home from "@/views/Home.vue";
const routes = [
{
path: "/",
name: "Home",
component: Home,
meta: { title: "首页" },
},
{
path: "/about",
name: "About",
component: () => import("@/views/About.vue"), // 懒加载
},
{
path: "/user/:id",
name: "User",
component: () => import("@/views/User.vue"),
props: true, // 将 params 作为 props 传入组件
},
{
path: "/:pathMatch(.*)*",
name: "NotFound",
component: () => import("@/views/NotFound.vue"), // 404
},
];
const router = createRouter({
history: createWebHistory(), // HTML5 模式(需要服务端支持)
routes,
});
export default router;vue
<!-- App.vue -->
<template>
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-link :to="`/user/${userId}`">用户</router-link>
</nav>
<router-view /> <!-- 路由出口 -->
</template>3.4.2 编程式导航
vue
<script setup>
import { useRouter, useRoute } from "vue-router";
const router = useRouter();
const route = useRoute();
function goToUser(id) {
// 方式1
router.push(`/user/${id}`);
// 方式2:命名的路由
router.push({ name: "User", params: { id } });
// 方式3:带查询参数
router.push({ path: "/search", query: { q: "vue" } });
}
function goBack() {
router.back(); // 后退
}
</script>3.4.3 导航守卫
javascript
// 全局前置守卫 — 常用(权限控制、页面标题)
router.beforeEach(async (to, from) => {
// 设置页面标题
document.title = to.meta?.title || "默认标题";
// 登录验证
const token = localStorage.getItem("token");
if (to.meta.requiresAuth && !token) {
// 重定向到登录页,并保存原始路径
return { name: "Login", query: { redirect: to.fullPath } };
}
});
// 全局后置守卫
router.afterEach((to, from) => {
console.log(`从 ${from.path} 跳转到 ${to.path}`);
});
// 路由独享守卫
const routes = [
{
path: "/admin",
component: () => import("@/views/Admin.vue"),
beforeEnter: (to, from) => {
const role = localStorage.getItem("role");
if (role !== "admin") return { name: "Home" };
},
},
];3.5 Pinia 状态管理
3.5.1 定义 Store
Pinia 是 Vue3 官方推荐的状态管理库,替代 Vuex。TypeScript 支持友好,无 mutations。
javascript
// stores/counter.js
import { defineStore } from "pinia";
import { ref, computed } from "vue";
export const useCounterStore = defineStore("counter", () => {
// state
const count = ref(0);
const history = ref([]);
// getters
const doubleCount = computed(() => count.value * 2);
const recentHistory = computed(() => history.value.slice(-5));
// actions(支持 async)
function increment() {
count.value++;
history.value.push(`+1 → ${count.value}`);
}
async function fetchAndSet() {
const res = await fetch("/api/count");
const data = await res.json();
count.value = data.count;
}
return { count, history, doubleCount, recentHistory, increment, fetchAndSet };
});3.5.2 在组件中使用
vue
<script setup>
import { useCounterStore } from "@/stores/counter";
const counter = useCounterStore();
// 直接解构会丢失响应式!
// ❌ const { count, increment } = useCounterStore();
// ✅ 用 storeToRefs 解构
import { storeToRefs } from "pinia";
const { count, doubleCount } = storeToRefs(counter);
const { increment, fetchAndSet } = counter; // 方法不用解构
</script>
<template>
<div>
<p>计数:{{ count }}</p>
<p>双倍:{{ doubleCount }}</p>
<p>直接访问:{{ counter.count }}</p>
<button @click="increment">+1</button>
<button @click="counter.$reset">重置</button>
<button @click="fetchAndSet">从 API 读取</button>
</div>
</template>3.5.3 Pinia 模块化
javascript
// stores/user.js
export const useUserStore = defineStore("user", () => {
const user = ref(null);
const token = ref(null);
const isLoggedIn = computed(() => !!token.value);
async function login(email, password) {
const res = await fetch("/api/login", {
method: "POST",
body: JSON.stringify({ email, password }),
});
const data = await res.json();
token.value = data.token;
user.value = data.user;
localStorage.setItem("token", data.token);
}
function logout() {
token.value = null;
user.value = null;
localStorage.removeItem("token");
}
return { user, token, isLoggedIn, login, logout };
});javascript
// 在组件中跨 store 调用
import { useCounterStore } from "./counter";
import { useUserStore } from "./user";
export const useAppStore = defineStore("app", () => {
const counter = useCounterStore();
const user = useUserStore();
const summary = computed(() => ({
user: user.user?.name,
count: counter.count,
}));
function resetAll() {
counter.$reset();
user.logout();
}
return { summary, resetAll };
});四、小结
| 知识点 | 核心要点 |
|---|---|
| 组合式 API | <script setup> + ref/reactive/computed 是 Vue3 标准写法 |
| 响应式原理 | Proxy 代理整个对象,支持增删属性和数组变化检测 |
| 组件通信 | props 向下传,emit 向上发;provide/inject 跨层级;v-model 双向绑定 |
| Vue Router | createRouter + createWebHistory;导航守卫做权限控制 |
| Pinia | defineStore + storeToRefs;无 mutations,支持 async actions |
五、练习
基础题
- 创建
Counter.vue组件,使用ref实现计数功能,包含「+1」「-1」「重置」三个按钮。 - 创建
TodoItem.vue接收title(String)和done(Boolean)两个 props,并通过emit向外触发toggle和delete事件。
进阶题
- 配置 Vue Router:实现
Home、About、User/:id三条路由,并在全局前置守卫中设置页面document.title。 - 使用 Pinia 创建一个
todoStore,包含todos数组、addTodo(text)和toggleTodo(id)方法,在组件中展示列表并支持添加和切换完成状态。
挑战题
- 实现一个用户登录流程:Login 页面 → 调用 Pinia action 登录成功 → 路由守卫自动跳转到之前访问的页面(或首页);未登录时访问
/profile自动重定向到/login?redirect=/profile。用provide+inject在全局 header 组件中显示当前用户名。