Vue深度实践¶
📚 章节目标¶
本章节将深入讲解Vue框架的核心概念和高级特性,包括Composition API、状态管理、Nuxt.js全栈开发、性能优化等,帮助学习者掌握Vue框架的深度应用。
学习目标¶
- 深入理解Vue 3 Composition API
- 掌握Vuex和Pinia状态管理
- 熟练使用Nuxt.js进行全栈开发
- 掌握Vue性能优化技巧
- 理解Vue 3响应式原理
🎯 Vue 3 Composition API¶
1. 核心概念¶
1.1 setup函数¶
JavaScript
import { ref, reactive, computed, watch, onMounted } from 'vue';
export default {
setup() {
// 响应式状态
const count = ref(0);
const user = reactive({
name: 'John',
age: 30,
});
// 计算属性
const doubleCount = computed(() => count.value * 2);
// 方法
const increment = () => {
count.value++;
};
// 监听器
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`);
});
// 生命周期钩子
onMounted(() => {
console.log('Component mounted');
});
// 返回给模板使用
return {
count,
user,
doubleCount,
increment,
};
},
};
1.2 script setup语法糖¶
Vue
<script setup>
import { ref, computed, onMounted } from 'vue';
// 响应式状态
const count = ref(0);
// 计算属性
const doubleCount = computed(() => count.value * 2);
// 方法
const increment = () => {
count.value++;
};
// 生命周期钩子
onMounted(() => {
console.log('Component mounted');
});
// Props
const props = defineProps({
title: {
type: String,
required: true,
},
});
// Emits
const emit = defineEmits(['update']);
// 暴露给父组件
defineExpose({
increment,
});
</script>
<template>
<div>
<h1>{{ title }}</h1>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
</div>
</template>
2. 响应式API¶
2.1 ref vs reactive¶
JavaScript
import { ref, reactive } from 'vue';
// ref - 用于基本类型
const count = ref(0);
console.log(count.value); // 0
count.value++;
// reactive - 用于对象
const user = reactive({
name: 'John',
age: 30,
});
console.log(user.name); // John
user.age++;
// ref在模板中自动解包
const message = ref('Hello');
// 模板中可以直接使用 {{ message }}
// reactive中的ref会自动解包
const state = reactive({
count: ref(0),
});
console.log(state.count); // 0 (不需要.value)
2.2 computed¶
JavaScript
import { ref, computed } from 'vue';
const firstName = ref('John');
const lastName = ref('Doe');
// 只读计算属性
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`;
});
// 可写计算属性
const fullNameWritable = computed({
get() {
return `${firstName.value} ${lastName.value}`;
},
set(value) {
[firstName.value, lastName.value] = value.split(' ');
}
});
// 使用
fullNameWritable.value = 'Jane Smith';
console.log(firstName.value); // Jane
console.log(lastName.value); // Smith
2.3 watch和watchEffect¶
JavaScript
import { ref, reactive, watch, watchEffect } from 'vue';
const count = ref(0);
const user = reactive({
name: 'John',
age: 30,
});
// watch - 明确指定监听源
watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`);
});
// 监听多个源
watch([count, () => user.age], ([newCount, newAge], [oldCount, oldAge]) => {
console.log(`Count: ${oldCount} -> ${newCount}, Age: ${oldAge} -> ${newAge}`);
});
// 深度监听
watch(
user,
(newVal, oldVal) => {
console.log('User changed');
},
{ deep: true }
);
// 立即执行
watch(
count,
(newVal) => {
console.log(`Current count: ${newVal}`);
},
{ immediate: true }
);
// watchEffect - 自动追踪依赖
watchEffect(() => {
console.log(`Count is ${count.value}, user is ${user.name}`);
});
// 停止监听
const stopWatch = watch(count, (newVal) => {
console.log(newVal);
});
// 稍后停止
stopWatch();
3. 生命周期钩子¶
JavaScript
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted,
} from 'vue';
export default {
setup() {
onBeforeMount(() => {
console.log('Before mount');
});
onMounted(() => {
console.log('Mounted');
});
onBeforeUpdate(() => {
console.log('Before update');
});
onUpdated(() => {
console.log('Updated');
});
onBeforeUnmount(() => {
console.log('Before unmount');
});
onUnmounted(() => {
console.log('Unmounted');
});
},
};
4. 依赖注入¶
JavaScript
// 父组件
import { provide, ref } from 'vue';
export default {
setup() {
const theme = ref('light');
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light';
};
provide('theme', {
theme,
toggleTheme,
});
return {};
},
};
// 子组件
import { inject } from 'vue';
export default {
setup() {
const { theme, toggleTheme } = inject('theme');
return {
theme,
toggleTheme,
};
},
};
// 提供默认值
const theme = inject('theme', 'light');
// 响应式注入
// 注意:toRefs 只能用于 reactive() 创建的对象,不能直接用于 inject() 返回的普通对象
// 正确做法:直接解构 inject 返回值(若 provide 传入的属性本身是 ref,解构后仍保持响应式)
const { theme, toggleTheme } = inject('theme');
// 若需要 toRefs,应在 provide 端使用 reactive() 包裹:
// provide('theme', reactive({ theme, toggleTheme }))
// 然后消费端:const { theme, toggleTheme } = toRefs(inject('theme'));
🗄️ 状态管理¶
1. Vuex¶
1.1 基础使用¶
JavaScript
// store/index.js
import { createStore } from 'vuex';
export default createStore({
state: {
count: 0,
user: null,
},
getters: {
doubleCount: (state) => state.count * 2,
isLoggedIn: (state) => !!state.user,
},
mutations: {
increment(state) {
state.count++;
},
setUser(state, user) {
state.user = user;
},
},
actions: {
async fetchUser({ commit }, userId) {
const user = await fetch(`/api/users/${userId}`).then(res => res.json());
commit('setUser', user);
},
},
modules: {
// 模块
},
});
// main.js
import { createApp } from 'vue';
import App from './App.vue';
import store from './store';
const app = createApp(App);
app.use(store);
app.mount('#app');
// 组件中使用
import { computed } from 'vue';
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
const count = computed(() => store.state.count);
const doubleCount = computed(() => store.getters.doubleCount);
const increment = () => {
store.commit('increment');
};
const fetchUser = (userId) => {
store.dispatch('fetchUser', userId);
};
return {
count,
doubleCount,
increment,
fetchUser,
};
},
};
1.2 模块化¶
JavaScript
// modules/user.js
export default {
namespaced: true,
state: {
user: null,
loading: false,
error: null,
},
getters: {
isLoggedIn: (state) => !!state.user,
userName: (state) => state.user?.name,
},
mutations: {
setUser(state, user) {
state.user = user;
},
setLoading(state, loading) {
state.loading = loading;
},
setError(state, error) {
state.error = error;
},
},
actions: {
async login({ commit }, credentials) {
commit('setLoading', true);
commit('setError', null);
try {
const user = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
}).then(res => res.json());
commit('setUser', user);
} catch (error) {
commit('setError', error.message);
} finally {
commit('setLoading', false);
}
},
},
};
// modules/todo.js
export default {
namespaced: true,
state: {
todos: [],
filter: 'all',
},
getters: {
filteredTodos: (state) => {
switch (state.filter) {
case 'active':
return state.todos.filter(todo => !todo.completed);
case 'completed':
return state.todos.filter(todo => todo.completed);
default:
return state.todos;
}
},
},
mutations: {
addTodo(state, todo) {
state.todos.push(todo);
},
removeTodo(state, todoId) {
state.todos = state.todos.filter(todo => todo.id !== todoId);
},
toggleTodo(state, todoId) {
const todo = state.todos.find(t => t.id === todoId);
if (todo) {
todo.completed = !todo.completed;
}
},
setFilter(state, filter) {
state.filter = filter;
},
},
actions: {
async fetchTodos({ commit }) {
const todos = await fetch('/api/todos').then(res => res.json());
todos.forEach(todo => commit('addTodo', todo));
},
},
};
// store/index.js
import { createStore } from 'vuex';
import user from './modules/user';
import todo from './modules/todo';
export default createStore({
modules: {
user,
todo,
},
});
// 组件中使用
import { computed } from 'vue';
import { useStore } from 'vuex';
export default {
setup() {
const store = useStore();
const user = computed(() => store.state.user.user);
const todos = computed(() => store.getters['todo/filteredTodos']);
const login = (credentials) => {
store.dispatch('user/login', credentials);
};
const addTodo = (text) => {
store.commit('todo/addTodo', {
id: Date.now(),
text,
completed: false,
});
};
return {
user,
todos,
login,
addTodo,
};
},
};
2. Pinia¶
2.1 基础使用¶
JavaScript
// stores/user.js
import { defineStore } from 'pinia';
export const useUserStore = defineStore('user', {
state: () => ({
user: null,
loading: false,
error: null,
}),
getters: {
isLoggedIn: (state) => !!state.user,
userName: (state) => state.user?.name,
},
actions: {
async login(credentials) {
this.loading = true;
this.error = null;
try {
const user = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
}).then(res => res.json());
this.user = user;
} catch (error) {
this.error = error.message;
} finally {
this.loading = false;
}
},
logout() {
this.user = null;
},
},
});
// stores/todo.js
import { defineStore } from 'pinia';
export const useTodoStore = defineStore('todo', {
state: () => ({
todos: [],
filter: 'all',
}),
getters: {
filteredTodos: (state) => {
switch (state.filter) {
case 'active':
return state.todos.filter(todo => !todo.completed); // map转换每个元素;filter筛选;reduce累积
case 'completed':
return state.todos.filter(todo => todo.completed);
default:
return state.todos;
}
},
},
actions: {
async fetchTodos() {
const todos = await fetch('/api/todos').then(res => res.json());
this.todos = todos;
},
addTodo(text) {
this.todos.push({
id: Date.now(),
text,
completed: false,
});
},
removeTodo(todoId) {
this.todos = this.todos.filter(todo => todo.id !== todoId);
},
toggleTodo(todoId) {
const todo = this.todos.find(t => t.id === todoId);
if (todo) {
todo.completed = !todo.completed;
}
},
setFilter(filter) {
this.filter = filter;
},
},
});
// main.js
import { createApp } from 'vue';
import { createPinia } from 'pinia';
import App from './App.vue';
const app = createApp(App);
app.use(createPinia());
app.mount('#app');
// 组件中使用
<script setup>
import { useUserStore } from '@/stores/user';
import { useTodoStore } from '@/stores/todo';
const userStore = useUserStore();
const todoStore = useTodoStore();
const login = async () => {
await userStore.login({
username: 'john',
password: 'password',
});
};
const addTodo = (text) => {
todoStore.addTodo(text);
};
</script>
<template>
<div>
<p v-if="userStore.isLoggedIn">Welcome, {{ userStore.userName }}</p>
<button v-else @click="login">Login</button>
<ul>
<li v-for="todo in todoStore.filteredTodos" :key="todo.id">
{{ todo.text }}
</li>
</ul>
</div>
</template>
2.2 Composition API风格¶
JavaScript
// stores/user.js
import { defineStore } from 'pinia';
import { ref, computed } from 'vue';
export const useUserStore = defineStore('user', () => {
const user = ref(null);
const loading = ref(false);
const error = ref(null);
const isLoggedIn = computed(() => !!user.value);
const userName = computed(() => user.value?.name);
async function login(credentials) {
loading.value = true;
error.value = null;
try { // try/catch捕获异常
const userData = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify(credentials),
}).then(res => res.json());
user.value = userData;
} catch (err) {
error.value = err.message;
} finally {
loading.value = false;
}
}
function logout() {
user.value = null;
}
return {
user,
loading,
error,
isLoggedIn,
userName,
login,
logout,
};
});
🚀 Nuxt.js全栈开发¶
1. Nuxt.js基础¶
1.1 项目结构¶
Text Only
nuxt-app/
├── .nuxt/ # Nuxt自动生成
├── assets/ # 未编译的静态资源
├── components/ # Vue组件
├── composables/ # Vue组合式函数
├── layouts/ # 布局组件
├── middleware/ # 中间件
├── pages/ # 页面
├── plugins/ # Vue插件
├── public/ # 静态文件
├── server/ # 服务端代码
├── stores/ # Pinia状态管理
├── types/ # TypeScript类型
├── utils/ # 工具函数
├── app.vue # 主应用组件
├── nuxt.config.ts # Nuxt配置文件
└── tsconfig.json # TypeScript配置
1.2 页面路由¶
Vue
<!-- pages/index.vue -->
<template>
<div>
<h1>Home Page</h1>
<NuxtLink to="/about">About</NuxtLink>
</div>
</template>
<!-- pages/about.vue -->
<template>
<div>
<h1>About Page</h1>
<NuxtLink to="/">Home</NuxtLink>
</div>
</template>
<!-- 动态路由 -->
<!-- pages/users/[id].vue -->
<template>
<div>
<h1>User {{ userId }}</h1>
<p>{{ user.name }}</p>
</div>
</template>
<script setup>
const route = useRoute();
const userId = route.params.id;
const { data: user } = await useFetch(`/api/users/${userId}`);
</script>
<!-- 嵌套路由 -->
<!-- pages/parent.vue -->
<template>
<div>
<h1>Parent Page</h1>
<NuxtPage />
</div>
</template>
<!-- pages/parent/child.vue -->
<template>
<div>
<h2>Child Page</h2>
</div>
</template>
2. 数据获取¶
2.1 useFetch和useAsyncData¶
Vue
<!-- 基础使用 -->
<template>
<div>
<h1>User Profile</h1>
<p v-if="pending">Loading...</p>
<p v-else-if="error">Error: {{ error.message }}</p>
<div v-else>
<p>Name: {{ data.name }}</p>
<p>Email: {{ data.email }}</p>
</div>
</div>
</template>
<script setup>
const { data, pending, error } = await useFetch('/api/user');
</script>
<!-- 带参数 -->
<script setup>
const route = useRoute();
const { data } = await useFetch(`/api/users/${route.params.id}`);
</script>
<!-- 自定义key -->
<script setup>
const { data } = await useFetch('/api/user', {
key: 'user-data',
});
</script>
<!-- 手动刷新 -->
<script setup>
const { data, refresh } = await useFetch('/api/user');
const handleRefresh = () => {
refresh();
};
</script>
<!-- useAsyncData -->
<script setup>
const { data } = await useAsyncData('user', () => $fetch('/api/user'));
</script>
2.2 服务端数据获取¶
Vue
<!-- 使用asyncData -->
<script setup>
const { data } = await useAsyncData('user', async () => {
const response = await $fetch('/api/user');
return response;
});
</script>
<!-- 使用useFetch -->
<script setup>
const { data } = await useFetch('/api/user', {
server: true, // 默认为true
});
</script>
<!-- 仅客户端获取 -->
<script setup>
const { data } = await useFetch('/api/user', {
server: false,
});
</script>
3. 服务端API¶
3.1 API路由¶
JavaScript
// server/api/hello.get.ts
export default defineEventHandler((event) => {
return {
message: 'Hello from Nuxt!',
};
});
// server/api/users/[id].get.ts
export default defineEventHandler(async (event) => { // async定义异步函数;await等待Promise完成
const id = getRouterParam(event, 'id');
const user = await $fetch(`https://api.example.com/users/${id}`);
return user;
});
// server/api/users.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event); // await等待异步操作完成
const user = await $fetch('https://api.example.com/users', {
method: 'POST',
body,
});
return user;
});
3.2 中间件¶
JavaScript
// middleware/auth.ts
export default defineNuxtRouteMiddleware((to, from) => {
const user = useUserStore().user;
if (!user) {
return navigateTo('/login');
}
});
// pages/protected.vue
<script setup>
definePageMeta({
middleware: 'auth',
});
</script>
4. 部署¶
JavaScript
// nuxt.config.ts
export default defineNuxtConfig({
nitro: {
preset: 'vercel', // or 'netlify', 'node-server', etc.
},
});
// 构建命令
npm run build
// 部署到Vercel
vercel --prod
⚡ Vue性能优化¶
1. 组件优化¶
1.1 v-once¶
Vue
<template>
<div>
<!-- 只渲染一次,不更新 -->
<h1 v-once>{{ title }}</h1>
<!-- 正常渲染 -->
<p>{{ message }}</p>
</div>
</template>
1.2 v-show vs v-if¶
Vue
<template>
<div>
<!-- 频繁切换使用v-show -->
<button v-show="isVisible">Click me</button>
<!-- 条件渲染使用v-if -->
<div v-if="isLoggedIn">
<p>Welcome back!</p>
</div>
</div>
</template>
1.3 computed vs methods¶
Vue
<script setup>
import { ref, computed } from 'vue';
const items = ref([
{ name: 'Item 1', price: 10 },
{ name: 'Item 2', price: 20 },
{ name: 'Item 3', price: 30 },
]);
// 使用computed - 有缓存
const totalPrice = computed(() => {
return items.value.reduce((sum, item) => sum + item.price, 0);
});
// 使用methods - 每次都重新计算
const calculateTotal = () => {
return items.value.reduce((sum, item) => sum + item.price, 0);
};
</script>
2. 列表优化¶
2.1 key的使用¶
Vue
<template>
<!-- 好的做法:使用唯一ID -->
<ul>
<li v-for="item in items" :key="item.id">
{{ item.name }}
</li>
</ul>
<!-- 不好的做法:使用index -->
<ul>
<li v-for="(item, index) in items" :key="index">
{{ item.name }}
</li>
</ul>
</template>
2.2 虚拟滚动¶
Vue
<script setup>
import { useVirtualList } from '@vueuse/core';
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
name: `Item ${i}`,
})));
const { list, containerProps, wrapperProps } = useVirtualList(items, {
itemHeight: 40,
});
</script>
<template>
<div v-bind="containerProps" style="height: 400px; overflow: auto;">
<div v-bind="wrapperProps">
<div
v-for="{ data: item } in list"
:key="item.id"
:style="{ height: '40px' }"
>
{{ item.name }}
</div>
</div>
</div>
</template>
3. 懒加载¶
3.1 组件懒加载¶
Vue
<script setup>
import { defineAsyncComponent } from 'vue';
const LazyComponent = defineAsyncComponent(() =>
import('./LazyComponent.vue')
);
</script>
<template>
<Suspense>
<template #default>
<LazyComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
3.2 路由懒加载¶
JavaScript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router';
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
component: () => import('@/views/Home.vue'),
},
{
path: '/about',
component: () => import('@/views/About.vue'),
},
],
});
export default router;
4. 响应式优化¶
4.1 shallowRef和shallowReactive¶
JavaScript
import { ref, shallowRef, reactive, shallowReactive } from 'vue';
// ref - 深度响应式
const deepRef = ref({
user: {
name: 'John',
},
});
// shallowRef - 浅层响应式
const shallowRefValue = shallowRef({
user: {
name: 'John',
},
});
// 触发更新
deepRef.value.user.name = 'Jane'; // 触发更新
shallowRefValue.value.user.name = 'Jane'; // 不触发更新
shallowRefValue.value = { user: { name: 'Jane' } }; // 触发更新
// reactive - 深度响应式
const deepReactive = reactive({
user: {
name: 'John',
},
});
// shallowReactive - 浅层响应式
const shallowReactiveValue = shallowReactive({
user: {
name: 'John',
},
});
// 触发更新
deepReactive.user.name = 'Jane'; // 触发更新
shallowReactiveValue.user.name = 'Jane'; // 不触发更新
4.2 markRaw¶
JavaScript
import { reactive, markRaw } from 'vue';
const state = reactive({ // const不可重新赋值;let块级作用域变量
user: markRaw({
name: 'John',
age: 30,
}),
});
// user对象不会被转为响应式
state.user.name = 'Jane'; // 不会触发更新
📝 练习题¶
1. 基础题¶
题目1:实现一个useCounter组合式函数
JavaScript
// composables/useCounter.js
import { ref } from 'vue';
export function useCounter(initialValue = 0) {
// 实现计数器组合式函数
// 返回 { count, increment, decrement, reset }
}
// 使用示例
<script setup>
import { useCounter } from '@/composables/useCounter';
const { count, increment, decrement, reset } = useCounter(0);
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
<button @click="reset">Reset</button>
</div>
</template>
2. 进阶题¶
题目2:实现一个useFetch组合式函数
JavaScript
// composables/useFetch.js
import { ref } from 'vue';
export function useFetch(url) {
// 实现fetch组合式函数
// 返回 { data, error, loading }
}
// 使用示例
<script setup>
import { useFetch } from '@/composables/useFetch';
const { data, error, loading } = useFetch('/api/user'); // 解构赋值:从对象/数组提取值
</script>
<template>
<div>
<p v-if="loading">Loading...</p>
<p v-else-if="error">Error: {{ error.message }}</p>
<div v-else>
<p>{{ data?.name }}</p> // ?.可选链:对象为null/undefined时安全返回undefined
</div>
</div>
</template>
3. 面试题¶
题目3:解释Vue 3的响应式原理
JavaScript
// 答案要点:
// 1. Vue 3使用Proxy实现响应式
// 2. Proxy可以监听对象和数组的变化
// 3. 通过依赖收集和触发更新
// 4. 实现数据变化自动更新视图
// 示例代码
const reactive = (obj) => { // 箭头函数:简洁的函数语法
return new Proxy(obj, {
get(target, key) {
track(target, key); // 依赖收集
return target[key];
},
set(target, key, value) {
target[key] = value;
trigger(target, key); // 触发更新
return true;
},
});
};
🎯 本章总结¶
本章节深入讲解了Vue框架的核心概念和高级特性,包括Composition API、状态管理、Nuxt.js全栈开发、性能优化等。关键要点:
- Composition API:掌握setup函数和组合式API的使用
- 状态管理:掌握Vuex和Pinia的使用和最佳实践
- Nuxt.js:掌握全栈开发和SSR技术
- 性能优化:掌握Vue性能优化的各种技巧
- 响应式原理:理解Vue 3的响应式实现原理
下一步将深入学习Angular框架的深度实践。