跳转至

Vue深度实践

Vue深度实践

📚 章节目标

本章节将深入讲解Vue框架的核心概念和高级特性,包括Composition API、状态管理、Nuxt.js全栈开发、性能优化等,帮助学习者掌握Vue框架的深度应用。

学习目标

  1. 深入理解Vue 3 Composition API
  2. 掌握Vuex和Pinia状态管理
  3. 熟练使用Nuxt.js进行全栈开发
  4. 掌握Vue性能优化技巧
  5. 理解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全栈开发、性能优化等。关键要点:

  1. Composition API:掌握setup函数和组合式API的使用
  2. 状态管理:掌握Vuex和Pinia的使用和最佳实践
  3. Nuxt.js:掌握全栈开发和SSR技术
  4. 性能优化:掌握Vue性能优化的各种技巧
  5. 响应式原理:理解Vue 3的响应式实现原理

下一步将深入学习Angular框架的深度实践。