学习笔记--Vue3 知识总结

103 阅读17分钟

一、Vue3 核心基础篇

1. Vue3 相比 Vue2 有哪些核心优势和重大变更?

核心优势

  • 性能大幅提升:虚拟DOM重写、编译优化(静态提升、PatchFlags)、内存占用减少约50%,渲染速度提升约1.3-2倍。
  • 更好的TypeScript支持:源码使用TS重构,提供完整的类型定义,开发中类型推导更精准,减少运行时错误。
  • 组合式API(Composition API):替代Options API的部分场景,更好地组织复用复杂逻辑,解决大型组件的"选项碎片化"问题。
  • 更小的打包体积:支持Tree Shaking,未使用的API不会被打包,默认打包体积比Vue2小约40%。
  • 新增更多实用特性:Teleport(瞬移组件)、Suspense(异步加载兜底)、多根节点组件(Fragment)等。

重大变更

  • 移除new Vue(),改用createApp()创建应用实例,隔离应用上下文,避免全局污染。
  • 移除Vue.prototype,改用app.config.globalProperties挂载全局属性/方法。
  • 过滤器(Filter)被移除,推荐使用计算属性或工具函数替代。
  • v-model语法变更,移除.sync修饰符,统一为v-model+参数的形式。
  • 生命周期钩子调整,新增setup(),移除beforeCreatecreated(被setup()替代)。

2. Vue3 的两种 API 风格分别是什么?各自的适用场景是什么?

Vue3 支持两种官方API风格,且两者可以在项目中共存(推荐统一风格):

(1)选项式API(Options API)

  • 写法:以datamethodscomputedwatch等选项组织代码,是Vue2的传统写法。
  • 示例:
<template>
  <div>{{ count }} {{ doubleCount }}</div>
  <button @click="increment">+1</button>
</template>

<script>
export default {
  data() {
    return { count: 0 }
  },
  computed: {
    doubleCount() {
      return this.count * 2
    }
  },
  methods: {
    increment() {
      this.count++
    }
  }
}
</script>
  • 适用场景:
    • 小型项目、快速原型开发。
    • 新手入门Vue,更容易理解和上手(符合"面向对象"的思维习惯)。
    • 简单组件(逻辑少、代码量少,无需复杂复用)。

(2)组合式API(Composition API)

  • 写法:以setup()入口(或<script setup>语法糖)为核心,使用refreactive等函数组织逻辑,按功能维度聚合代码。
  • 示例(<script setup>语法糖):
<template>
  <div>{{ count }} {{ doubleCount }}</div>
  <button @click="increment">+1</button>
</template>

<script setup>
import { ref, computed } from 'vue'

// 响应式数据
const count = ref(0)
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 方法
const increment = () => {
  count.value++
}
</script>
  • 适用场景:
    • 中大型项目、复杂组件(逻辑多、需要按功能拆分)。
    • 逻辑需要跨组件复用(可提取为Composables工具函数)。
    • 追求更好的TypeScript支持,希望获得更精准的类型推导。
    • 团队协作项目,便于代码维护和迭代。

3. Vue3 中如何创建响应式数据?refreactive 的区别是什么?

Vue3 提供refreactive两个核心API创建响应式数据,底层基于Proxy(替代Vue2的Object.defineProperty)实现响应式。

(1)创建响应式数据的方式

  • ref:用于创建基本类型(String、Number、Boolean等)的响应式数据,也可用于引用类型。
    import { ref } from 'vue'
    
    // 基本类型响应式
    const name = ref('Vue3')
    const age = ref(3)
    
    // 引用类型响应式
    const user = ref({
      id: 1,
      username: 'admin'
    })
    
    // 修改ref数据:需要通过 .value 访问(模板中可省略 .value)
    name.value = 'Vue3 Composition API'
    user.value.username = 'root'
    
  • reactive:用于创建引用类型(Object、Array等)的响应式数据,无法直接处理基本类型。
    import { reactive } from 'vue'
    
    // 对象响应式
    const user = reactive({
      id: 1,
      username: 'admin'
    })
    
    // 数组响应式
    const list = reactive([1, 2, 3, 4])
    
    // 修改reactive数据:直接访问属性/索引即可,无需 .value
    user.username = 'root'
    list.push(5)
    

(2)refreactive 的核心区别

对比维度refreactive
支持数据类型基本类型、引用类型仅支持引用类型(Object/Array等)
访问/修改方式脚本中需通过 .value,模板中可省略直接访问/修改属性/索引,无需 .value
响应式原理底层封装了reactive,通过一个包含value属性的对象实现响应式直接基于Proxy代理整个对象,拦截属性的读取/修改
解构特性直接解构会丢失响应式(需使用toRefs/toRef直接解构会丢失响应式(需使用toRefs/toRef
使用场景优先用于基本类型,或简单引用类型优先用于复杂对象、数组,需要统一管理多个相关属性

(3)补充:toRefstoRef 的作用

当需要解构reactiveref(引用类型)的响应式数据,同时保留响应式时,可使用toRefs(批量转换)或toRef(单个转换):

import { reactive, toRefs, toRef } from 'vue'

const user = reactive({
  name: 'admin',
  age: 20
})

// 批量转换为ref,解构后仍保留响应式
const { name, age } = toRefs(user)

// 单个转换为ref
const nameRef = toRef(user, 'name')

二、组合式API 进阶篇

1. <script setup> 语法糖有哪些优势?它和普通 setup() 有什么区别?

<script setup> 是Vue3.2+推出的组合式API语法糖,是目前组合式API的推荐写法,相比普通setup()有显著优势。

(1)核心优势

  • 更少的样板代码:无需导出(export default),无需在setup()中返回变量/方法,直接声明即可在模板中使用。
  • 更好的语法体验:支持顶层await、自动注册组件(导入后直接使用,无需在components选项中注册)。
  • 更优的性能:编译阶段进行优化,比普通setup()运行效率更高。
  • 更好的TypeScript支持:无需额外配置,类型推导更简洁、精准。

(2)与普通 setup() 的区别

对比维度<script setup>普通 setup()
写法形式标签上添加 setup 属性,<script setup>作为组件选项,export default { setup() {} }
变量/方法暴露顶层声明直接暴露给模板,无需返回需在setup()函数中返回,模板才能访问
组件注册导入后直接使用,自动注册需手动在components选项中注册(或全局注册)
this 指向thisthisundefinedthis(与Vue2不同,setup()this不指向组件实例)
顶层await支持(自动将组件标记为异步组件)不支持(需返回Promise,配合Suspense使用)
额外API提供definePropsdefineEmits等编译宏,简化父子组件通信需通过setup()的参数(propscontext)获取props和emits

(3)<script setup> 简单示例

<template>
  <div>{{ msg }}</div>
  <ChildComponent @child-click="handleChildClick" />
</template>

<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

// 顶层声明,直接暴露给模板
const msg = ref('Hello <script setup>')

// 方法直接暴露
const handleChildClick = (data) => {
  console.log('接收子组件事件:', data)
}

// 顶层await 支持
const data = await fetch('/api/list').then(res => res.json())
</script>

2. Vue3 生命周期钩子有哪些变化?组合式API中如何使用生命周期?

(1)Vue3 生命周期钩子的变化

  1. 基于组合式API,新增setup()钩子,替代了Vue2的beforeCreatecreatedsetup()在这两个钩子之前执行,无需再单独声明这两个钩子)。
  2. 部分钩子名称调整:beforeDestroybeforeUnmountdestroyedunmounted
  3. 新增两个生命周期钩子:onRenderTracked(跟踪虚拟DOM渲染时的依赖)、onRenderTriggered(触发虚拟DOM重新渲染时的依赖),仅用于调试。
  4. 选项式API的生命周期钩子仍可使用(向下兼容),但组合式API中推荐使用对应的生命周期函数。

(2)生命周期钩子对应关系(Vue2 → Vue3)

Vue2 选项式APIVue3 选项式APIVue3 组合式API(需导入)执行时机
beforeCreatebeforeCreate-(被setup()替代)组件实例创建之前,数据未初始化、方法未挂载
createdcreated-(被setup()替代)组件实例创建完成,数据已初始化、方法已挂载,但DOM未生成
beforeMountbeforeMountonBeforeMount组件挂载之前,模板已编译完成,即将渲染DOM
mountedmountedonMounted组件挂载完成,DOM已生成,可访问/操作DOM
beforeUpdatebeforeUpdateonBeforeUpdate组件数据更新之前,虚拟DOM即将重新渲染
updatedupdatedonUpdated组件数据更新完成,虚拟DOM已重新渲染,DOM已更新
beforeDestroybeforeUnmountonBeforeUnmount组件卸载之前,组件实例仍可用,可清理定时器、事件监听等
destroyedunmountedonUnmounted组件卸载完成,组件实例被销毁,所有资源已释放

(3)组合式API中使用生命周期(<script setup> 示例)

<script setup>
import { ref, onBeforeMount, onMounted, onBeforeUpdate, onUnmounted } from 'vue'

const count = ref(0)
let timer = null

// 组件挂载前
onBeforeMount(() => {
  console.log('组件即将挂载')
})

// 组件挂载后
onMounted(() => {
  console.log('组件挂载完成')
  // 开启定时器
  timer = setInterval(() => {
    count.value++
  }, 1000)
})

// 组件更新前
onBeforeUpdate(() => {
  console.log('组件即将更新')
})

// 组件卸载前:清理资源
onUnmounted(() => {
  console.log('组件即将卸载')
  clearInterval(timer)
})
</script>

3. computedwatch 的使用场景分别是什么?watch 如何监听多个数据源?

(1)computed:计算属性(依赖缓存,同步推导)

  • 核心特性:基于依赖的响应式数据进行同步计算,具有缓存机制——只有依赖的数据发生变化时,才会重新计算,否则直接返回缓存结果。
  • 适用场景:
    1. 对现有响应式数据进行格式化、转换(如:将时间戳转为格式化日期、将数字转为金额格式)。
    2. 多个响应式数据的组合计算(如:总价 = 单价 × 数量)。
    3. 需要依赖缓存,避免重复计算的场景(如:复杂的列表筛选、排序)。
  • 使用示例:
import { ref, computed } from 'vue'

const price = ref(100)
const quantity = ref(5)

// 只读计算属性
const totalPrice = computed(() => {
  return price.value * quantity.value
})

// 可写计算属性(需传入get和set方法)
const fullName = computed({
  get() {
    return `${firstName.value} ${lastName.value}`
  },
  set(value) {
    const [first, last] = value.split(' ')
    firstName.value = first
    lastName.value = last
  }
})

(2)watch:监听器(响应式回调,支持异步)

  • 核心特性:监听一个或多个响应式数据的变化,触发自定义回调函数,支持异步操作(如:接口请求、定时器),无缓存机制——只要监听的数据源变化,就会触发回调。
  • 适用场景:
    1. 数据源变化后执行异步操作(如:输入框输入完成后请求搜索接口、ID变化后请求详情数据)。
    2. 数据源变化后执行复杂的业务逻辑(如:数据变化后更新本地存储、触发全局事件)。
    3. 监听数据的变化轨迹(如:记录数据的旧值和新值)。
  • 基本使用(监听单个数据源):
import { ref, watch } from 'vue'

const name = ref('Vue3')

// 监听ref数据
watch(name, (newVal, oldVal) => {
  console.log('名称变化:', oldVal, '→', newVal)
  // 支持异步操作
  fetch(`/api/search?name=${newVal}`).then(res => res.json())
})

// 监听reactive数据的单个属性
const user = reactive({ id: 1, name: 'admin' })
watch(() => user.id, (newId, oldId) => {
  console.log('用户ID变化:', oldId, '→', newId)
})

(3)watch 监听多个数据源

监听多个数据源时,将数据源放入数组中,回调函数的第一个参数为对应数据源的新值数组,第二个参数为旧值数组:

import { ref, reactive, watch } from 'vue'

// 场景1:监听多个ref数据
const name = ref('Vue3')
const age = ref(3)

watch([name, age], ([newName, newAge], [oldName, oldAge]) => {
  console.log('名称变化:', oldName, '→', newName)
  console.log('年龄变化:', oldAge, '→', newAge)
})

// 场景2:监听多个不同类型数据源(ref + reactive属性)
const user = reactive({ id: 1, username: 'admin' })
const keyword = ref('')

watch([() => user.id, keyword], ([newId, newKeyword], [oldId, oldKeyword]) => {
  console.log('用户ID变化:', oldId, '→', newId)
  console.log('搜索关键词变化:', oldKeyword, '→', newKeyword)
  // 执行异步业务逻辑
  fetch(`/api/user/${newId}?keyword=${newKeyword}`).then(res => res.json())
})

(4)补充:watchEffectwatch 的区别

watchEffect 是简化版watch,自动收集依赖,无需手动指定数据源,适用于监听多个不确定的数据源变化:

import { ref, watchEffect } from 'vue'

const name = ref('Vue3')
const age = ref(3)

watchEffect(() => {
  // 自动收集name和age作为依赖
  console.log(`名称:${name.value},年龄:${age.value}`)
})

三、组件开发与通信篇

1. Vue3 中父子组件如何通信?有哪些方式?

Vue3 中父子组件通信的核心逻辑与Vue2一致,只是部分API有调整,主要通信方式如下:

(1)父传子:props

  • 子组件:通过defineProps<script setup>)或props选项(选项式API)声明接收的属性。
  • 父组件:通过属性绑定的方式传递数据给子组件。
  • 示例(<script setup>):
    • 子组件(Child.vue):
    <template>
      <div>父组件传递的名称:{{ name }},年龄:{{ age }}</div>
    </template>
    
    <script setup>
    // 声明props(支持类型校验、默认值)
    const props = defineProps({
      name: {
        type: String,
        required: true,
        default: ''
      },
      age: {
        type: Number,
        default: 0
      }
    })
    
    // 访问props:直接使用 props.name / props.age
    console.log(props.name)
    </script>
    
    • 父组件(Parent.vue):
    <template>
      <Child :name="parentName" :age="parentAge" />
    </template>
    
    <script setup>
    import { ref } from 'vue'
    import Child from './Child.vue'
    
    const parentName = ref('Vue3 父子通信')
    const parentAge = ref(3)
    </script>
    

(2)子传父:emits 自定义事件

  • 子组件:通过defineEmits声明触发的事件,通过emit方法触发事件并传递数据。
  • 父组件:通过@事件名绑定回调函数,接收子组件传递的数据。
  • 示例(<script setup>):
    • 子组件(Child.vue):
    <template>
      <button @click="handleClick">向父组件传递数据</button>
    </template>
    
    <script setup>
    // 声明要触发的事件
    const emit = defineEmits(['child-click', 'child-change'])
    
    const handleClick = () => {
      // 触发事件并传递数据(可传递多个参数)
      emit('child-click', { id: 1, msg: '子组件触发的事件' })
    }
    </script>
    
    • 父组件(Parent.vue):
    <template>
      <Child @child-click="handleChildClick" />
    </template>
    
    <script setup>
    import Child from './Child.vue'
    
    const handleChildClick = (data) => {
      console.log('接收子组件数据:', data) // { id: 1, msg: '子组件触发的事件' }
    }
    </script>
    

(3)其他补充通信方式

  1. v-model 双向绑定:简化父子组件双向通信,替代Vue2的.sync修饰符。
    // 子组件:声明modelValue props 和 update:modelValue 事件
    <script setup>
    const props = defineProps(['modelValue'])
    const emit = defineEmits(['update:modelValue'])
    </script>
    
    // 父组件:直接使用v-model绑定
    <Child v-model="parentValue" />
    
  2. provide / inject:跨层级组件通信(爷孙组件、深层组件),父组件通过provide提供数据,子孙组件通过inject注入数据。
    // 父组件:提供数据
    import { provide, ref } from 'vue'
    const theme = ref('dark')
    provide('theme', theme)
    
    // 子孙组件:注入数据
    import { inject } from 'vue'
    const theme = inject('theme')
    
  3. 全局事件总线:适用于任意组件通信,Vue3中可通过mitt库实现(替代Vue2的Vue.prototype.$bus)。

2. Vue3 中新增的 TeleportSuspense 组件的作用是什么?如何使用?

(1)Teleport:瞬移组件(组件DOM节点转移)

  • 核心作用:将组件的DOM节点脱离当前组件的DOM层级,挂载到页面的指定DOM节点下,同时保留组件的响应式和组件间通信能力(不受DOM层级影响)。
  • 解决的问题:
    • 弹窗、模态框等组件被父组件的overflow: hiddenz-index等样式限制,无法正常显示。
    • 避免组件DOM层级过深,导致样式调试复杂。
  • 使用示例:
<template>
  <div>
    <button @click="showModal = true">打开弹窗</button>

    <!-- Teleport:将弹窗挂载到body标签下 -->
    <Teleport to="body">
      <div v-if="showModal" class="modal">
        <div class="modal-content">
          <h3>这是一个瞬移弹窗</h3>
          <button @click="showModal = false">关闭弹窗</button>
        </div>
      </div>
    </Teleport>
  </div>
</template>

<script setup>
import { ref } from 'vue'
const showModal = ref(false)
</script>

<style scoped>
.modal {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: rgba(0, 0, 0, 0.5);
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 9999;
}

.modal-content {
  background: #fff;
  padding: 20px;
  border-radius: 8px;
}
</style>
  • 关键说明:to 属性指定挂载目标(可传入CSS选择器,如#app.modal-container,或直接传入DOM节点)。

(2)Suspense:异步组件兜底(等待异步内容加载)

  • 核心作用:用于包裹异步组件或包含顶层await的组件,在异步内容加载完成前显示兜底内容(如加载动画),加载完成后显示组件内容,提升用户体验。
  • 注意事项:
    • Suspense 是一个实验性特性(Vue3正式版已支持,但部分场景仍有优化空间)。
    • 仅支持组合式API中带顶层await的组件,或通过defineAsyncComponent定义的异步组件。
  • 使用示例:
    1. 定义异步组件(AsyncComponent.vue):
    <script setup>
    // 顶层await:模拟接口请求
    const data = await fetch('/api/list').then(res => res.json())
    </script>
    
    <template>
      <div>
        <h3>异步组件内容</h3>
        <ul>
          <li v-for="item in data" :key="item.id">{{ item.name }}</li>
        </ul>
      </div>
    </template>
    
    1. 父组件中使用Suspense
    <template>
      <div>
        <h2>父组件</h2>
        <!-- Suspense:default 显示异步组件,fallback 显示兜底内容 -->
        <Suspense>
          <template #default>
            <AsyncComponent />
          </template>
          <template #fallback>
            <div class="loading">加载中...</div>
          </template>
        </Suspense>
      </div>
    </template>
    
    <script setup>
    import AsyncComponent from './AsyncComponent.vue'
    </script>
    
    <style scoped>
    .loading {
      text-align: center;
      padding: 20px;
      color: #666;
    }
    </style>
    
  • 关键说明:Suspense 提供两个插槽——default(异步加载的组件内容)和fallback(加载中的兜底内容)。

3. Vue3 中如何实现组件的透传属性(attrs)和插槽(Slot)?

(1)透传属性(attrs

  • 核心概念:父组件传递给子组件的属性中,未被子组件props声明接收的属性,会被归类为透传属性(如classstyleid、自定义属性等)。
  • Vue3 中获取透传属性:
    • <script setup> 中:通过useAttrs() API获取透传属性。
    • 选项式API中:通过this.$attrs获取。
  • 特性:
    1. 透传属性会自动挂载到子组件的根元素上(无需手动处理)。
    2. 若子组件有多个根节点,透传属性不会自动挂载,需手动指定挂载目标。
  • 使用示例:
<!-- 子组件(Child.vue) -->
<template>
  <!-- 透传属性自动挂载到该根元素上 -->
  <div class="child">子组件</div>
</template>

<script setup>
import { useAttrs } from 'vue'

// 获取透传属性
const attrs = useAttrs()
console.log(attrs) // 输出父组件传递的未被props声明的属性(如id、data-*等)
</script>

<!-- 父组件(Parent.vue) -->
<template>
  <!-- 传递props声明的name,和透传属性id、data-type、class -->
  <Child 
    :name="parentName" 
    id="child-component" 
    data-type="test" 
    class="parent-child"
  />
</template>

(2)插槽(Slot):组件内容分发

Vue3 支持插槽的所有功能(默认插槽、具名插槽、作用域插槽),语法与Vue2基本一致,仅<script setup>中使用作用域插槽时有细微调整。

① 默认插槽:无名称的插槽,用于分发组件的默认内容
<!-- 子组件(Child.vue) -->
<template>
  <div class="child">
    <!-- 默认插槽出口 -->
    <slot>默认兜底内容(父组件未传递内容时显示)</slot>
  </div>
</template>

<!-- 父组件(Parent.vue) -->
<template>
  <Child>
    <!-- 默认插槽内容 -->
    <p>这是父组件传递给子组件的默认插槽内容</p>
  </Child>
</template>
② 具名插槽:有名称的插槽,用于分发组件的指定区域内容
<!-- 子组件(Child.vue) -->
<template>
  <div class="child">
    <header>
      <!-- 具名插槽:header -->
      <slot name="header"></slot>
    </header>
    <main>
      <slot></slot> <!-- 默认插槽 -->
    </main>
    <footer>
      <!-- 具名插槽:footer -->
      <slot name="footer"></slot>
    </footer>
  </div>
</template>

<!-- 父组件(Parent.vue) -->
<template>
  <Child>
    <!-- 具名插槽:通过v-slot:name 或 #name 绑定 -->
    <template #header>
      <h3>这是头部插槽内容</h3>
    </template>

    <!-- 默认插槽 -->
    <p>这是主体默认插槽内容</p>

    <!-- 具名插槽:footer -->
    <template #footer>
      <p>这是底部插槽内容</p>
    </template>
  </Child>
</template>
③ 作用域插槽:子组件向父组件插槽传递数据,父组件可接收并使用
<!-- 子组件(Child.vue) -->
<template>
  <div class="child">
    <!-- 作用域插槽:通过v-bind传递数据给父组件 -->
    <slot name="list" :list="dataList" :total="total"></slot>
  </div>
</template>

<script setup>
import { reactive, ref } from 'vue'

// 子组件的内部数据
const dataList = reactive([{ id: 1, name: 'Vue3' }, { id: 2, name: 'React' }])
const total = ref(2)
</script>

<!-- 父组件(Parent.vue) -->
<template>
  <Child>
    <!-- 作用域插槽:接收子组件传递的数据 -->
    <template #list="slotProps">
      <ul>
        <li v-for="item in slotProps.list" :key="item.id">{{ item.name }}</li>
      </ul>
      <p>总数量:{{ slotProps.total }}</p>
    </template>
  </Child>
</template>

四、状态管理与路由篇

1. Vue3 中如何使用 Pinia?Pinia 相比 Vuex 有哪些优势?

Pinia 是 Vue 官方推荐的新一代状态管理库,替代了 Vuex(Vuex 4 为 Vue3 适配版本,但后续不再维护),专门为 Vue3 和组合式API设计。

(1)Pinia 的基本使用步骤

  1. 安装 Pinia:
    npm install pinia
    # 或 yarn add pinia
    
  2. 在项目入口(main.js)中创建并挂载 Pinia 实例:
    import { createApp } from 'vue'
    import { createPinia } from 'pinia'
    import App from './App.vue'
    
    const app = createApp(App)
    const pinia = createPinia()
    
    app.use(pinia)
    app.mount('#app')
    
  3. 定义 Store(存储):
    // src/stores/user.js
    import { defineStore } from 'pinia'
    
    // 定义并导出Store(第一个参数为Store唯一ID,必须唯一)
    export const useUserStore = defineStore('user', {
      // 状态:返回初始状态的函数(类似Vue2的data)
      state: () => ({
        username: 'admin',
        token: '',
        roles: []
      }),
    
      // 计算属性:类似Vue的computed,具有缓存机制
      getters: {
        // 接收state作为参数
        isAdmin: (state) => state.roles.includes('admin'),
        // 访问其他getters:通过this(需指定返回值类型)
        userInfo: function() {
          return {
            username: this.username,
            isAdmin: this.isAdmin
          }
        }
      },
    
      // 方法:类似Vue的methods,用于修改状态(支持异步)
      actions: {
        // 修改状态:直接通过this访问state
        setToken(newToken) {
          this.token = newToken
        },
        // 异步操作:如接口请求
        async login(userForm) {
          const res = await fetch('/api/login', {
            method: 'POST',
            body: JSON.stringify(userForm)
          })
          const data = await res.json()
          this.token = data.token
          this.username = data.username
          return data
        }
      }
    })
    
  4. 在组件中使用 Store:
    <template>
      <div>
        <p>用户名:{{ userStore.username }}</p>
        <p>是否为管理员:{{ userStore.isAdmin }}</p>
        <button @click="handleLogin">登录</button>
      </div>
    </template>
    
    <script setup>
    import { useUserStore } from '@/stores/user'
    
    // 获取Store实例
    const userStore = useUserStore()
    
    // 调用Store的actions方法
    const handleLogin = async () => {
      await userStore.login({
        username: 'root',
        password: '123456'
      })
    }
    
    // 重置Store状态
    const resetUserStore = () => {
      userStore.$reset()
    }
    
    // 批量修改Store状态
    const updateUserInfo = () => {
      userStore.$patch({
        username: 'root',
        roles: ['admin', 'editor']
      })
    }
    </script>
    

(2)Pinia 相比 Vuex 的核心优势

  1. 更简洁的API:移除了 Vuex 的Mutation(同步修改状态),所有状态修改都在Actions中完成(支持同步/异步),减少了样板代码。
  2. 更好的 TypeScript 支持:无需额外配置,类型推导更精准,开发时获得更好的代码提示,减少类型错误。
  3. 无模块嵌套:Pinia 中没有modules概念,每个 Store 都是独立的,可直接跨 Store 访问,解决了 Vuex 模块嵌套复杂、命名空间繁琐的问题。
  4. 轻量化:体积更小(约1KB),打包后对项目体积影响极小。
  5. 更好的组合式API支持:与 Vue3 的<script setup>无缝集成,使用更灵活。
  6. 内置持久化支持:可通过pinia-plugin-persistedstate插件轻松实现状态持久化(无需手动处理localStorage)。

2. Vue Router 4 (Vue3 适配版)有哪些核心变更?如何实现路由守卫和动态路由?

Vue Router 4 是专门为 Vue3 设计的路由库,与 Vue Router 3(Vue2 适配版)相比有部分核心变更,同时保留了大部分原有API。

(1)Vue Router 4 的核心变更

  1. 创建路由实例的API变更:new VueRouter()createRouter()
  2. 路由模式配置变更:mode: 'history'history: createWebHistory()(需从vue-router导入对应方法)。
  3. 移除base配置:改为在createWebHistory()中传入基础路径(如createWebHistory('/admin/'))。
  4. 路由守卫的next()方法可选:Vue Router 4 中,守卫函数可直接返回true/false/路由地址,无需手动调用next()(兼容旧版next())。
  5. 支持组合式API:新增useRouter()useRoute() API,在<script setup>中获取路由实例和当前路由信息。
  6. 移除$router$route的类型扩展:需通过declare module 'vue-router'进行自定义类型扩展。

(2)Vue Router 4 的基本使用

  1. 安装:
    npm install vue-router@4
    # 或 yarn add vue-router@4
    
  2. 配置路由(src/router/index.js):
    import { createRouter, createWebHistory } from 'vue-router'
    import Home from '@/views/Home.vue'
    import About from '@/views/About.vue'
    
    // 路由规则
    const routes = [
      {
        path: '/',
        name: 'Home',
        component: Home
      },
      {
        path: '/about',
        name: 'About',
        component: About
        // 懒加载组件(推荐)
        // component: () => import('@/views/About.vue')
      }
    ]
    
    // 创建路由实例
    const router = createRouter({
      history: createWebHistory(), // history模式(替代旧版mode: 'history')
      // hash模式:createWebHashHistory()
      routes
    })
    
    export default router
    
  3. 挂载路由(src/main.js):
    import { createApp } from 'vue'
    import App from './App.vue'
    import router from './router'
    
    const app = createApp(App)
    app.use(router)
    app.mount('#app')
    
  4. 组件中使用路由:
    <template>
      <!-- 路由链接 -->
      <router-link to="/">首页</router-link>
      <router-link to="/about">关于</router-link>
    
      <!-- 路由出口 -->
      <router-view />
    </template>
    
    <script setup>
    import { useRouter, useRoute } from 'vue-router'
    
    // 获取路由实例
    const router = useRouter()
    // 获取当前路由信息
    const route = useRoute()
    
    // 编程式导航
    const toAbout = () => {
      router.push('/about')
      // 或带参数导航
      // router.push({ name: 'About', query: { id: 1 } })
    }
    
    // 访问当前路由参数
    console.log(route.query.id)
    </script>
    

(3)路由守卫:控制路由的跳转权限

Vue Router 4 支持三种路由守卫:全局守卫、路由独享守卫、组件内守卫。

① 全局守卫(全局生效,如登录校验)
// src/router/index.js
import router from './index'

// 全局前置守卫:路由跳转前触发
router.beforeEach((to, from) => {
  // to:目标路由对象
  // from:当前即将离开的路由对象

  // 示例:登录校验,未登录禁止进入/home
  const token = localStorage.getItem('token')
  if (to.path === '/home' && !token) {
    return '/' // 跳转到首页
  }
  return true // 允许跳转
})

// 全局后置守卫:路由跳转后触发(无拦截能力,用于统计、埋点等)
router.afterEach((to, from) => {
  console.log('路由跳转完成:', from.path, '→', to.path)
})
② 路由独享守卫(仅对当前路由生效)
const routes = [
  {
    path: '/home',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    // 路由独享前置守卫
    beforeEnter: (to, from) => {
      const token = localStorage.getItem('token')
      if (!token) {
        return '/'
      }
    }
  }
]
③ 组件内守卫(仅对当前组件生效)
<script setup>
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

// 组件内前置守卫:进入组件前触发(与路由独享守卫类似)
// 注:<script setup>中无法直接使用beforeRouteEnter,需通过路由元信息或其他方式替代

// 组件内更新守卫:当前路由参数变化时触发(如 /user/:id 从 /user/1 跳转到 /user/2)
onBeforeRouteUpdate((to, from) => {
  console.log('路由参数变化:', from.params.id, '→', to.params.id)
})

// 组件内离开守卫:离开组件时触发
onBeforeRouteLeave((to, from) => {
  const confirmLeave = window.confirm('确定要离开当前页面吗?')
  if (!confirmLeave) {
    return false // 阻止离开
  }
})
</script>

(4)动态路由:动态添加/删除路由规则

通过路由实例的addRoute()(添加路由)和removeRoute()(删除路由)方法实现动态路由,常用于权限控制(如根据用户角色动态生成路由)。

// src/router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: () => import('@/views/Home.vue') }
  ]
})

// 动态添加路由(示例:登录后根据角色添加/admin路由)
export const addAdminRoute = () => {
  router.addRoute({
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),
    children: [
      { path: 'user', component: () => import('@/views/Admin/User.vue') }
    ]
  })
}

// 动态删除路由(通过路由name删除)
export const removeAdminRoute = () => {
  router.removeRoute('Admin')
}

export default router

在组件中调用动态路由方法:

<script setup>
import { addAdminRoute } from '@/router'

// 登录成功后添加动态路由
const handleLoginSuccess = () => {
  addAdminRoute()
  // 跳转到动态添加的路由
  router.push('/admin')
}
</script>

五、构建部署与优化篇

1. Vue3 项目的构建工具有哪些?如何使用 Vite 搭建 Vue3 项目?

(1)Vue3 支持的构建工具

  1. Vite:Vue 官方推荐的新一代构建工具,专为 Vue3、React 等前端框架设计,具有“快速冷启动、按需编译、模块热更新”等优势,是 Vue3 项目的首选构建工具。
  2. Vue CLI:Vue 官方的传统构建工具,支持 Vue3(需升级到 Vue CLI 5+),基于 Webpack 构建,配置成熟、生态完善,适合团队熟悉 Webpack 或需要复杂配置的项目。
  3. Webpack:手动配置 Webpack 搭建 Vue3 项目,灵活性最高,但配置复杂,适合有丰富 Webpack 经验的开发者。

(2)使用 Vite 搭建 Vue3 项目(推荐)

Vite 搭建 Vue3 项目步骤简单,无需额外配置即可快速启动。

  1. 初始化项目(Node.js 版本需 ≥ 14.18):

    # npm 6.x
    npm create vite@latest my-vue3-project --template vue
    
    # npm 7+(需额外加 --)
    npm create vite@latest my-vue3-project -- --template vue
    
    # yarn
    yarn create vite my-vue3-project --template vue
    
    # pnpm
    pnpm create vite my-vue3-project --template vue
    
    • 模板说明:
      • vue:Vue3 选项式API 模板。
      • vue-ts:Vue3 + TypeScript 选项式API 模板。
      • vue-jsx:Vue3 + JSX 模板。
      • vue-ts-jsx:Vue3 + TypeScript + JSX 模板。
  2. 进入项目目录并安装依赖:

    cd my-vue3-project
    npm install
    
  3. 启动开发服务器:

    npm run dev
    
    • 特性:启动速度极快(秒级启动),修改代码后模块热更新(HMR)即时生效,无需等待整个项目重新打包。
  4. 构建生产环境包:

    npm run build
    
    • 构建产物输出到dist目录,可直接部署到静态服务器。
  5. 预览生产环境包:

    npm run preview
    

2. Vue3 项目有哪些常见的性能优化手段?

Vue3 项目的性能优化可从代码层面、构建层面、运行时层面三个维度入手,结合 Vue3 的特性进行针对性优化。

(1)代码层面优化

  1. 合理使用组合式API:将复杂逻辑提取为 Composables 工具函数,避免组件体积过大,同时提高代码复用率。
  2. 减少不必要的响应式数据:仅对需要视图更新的数据使用ref/reactive,普通常量直接声明(无需响应式),减少 Proxy 代理的性能开销。
  3. 优化v-for渲染:
    • 必须添加唯一key(避免使用索引作为key)。
    • 避免在v-for中使用v-if(可通过计算属性先筛选数据,再渲染)。
    • 大数据列表使用虚拟滚动(如vue-virtual-scroller),只渲染可视区域的DOM节点。
  4. 合理使用computedwatch
    • 复杂的格式化逻辑使用computed(利用缓存减少重复计算)。
    • 仅在需要时使用watch,避免不必要的监听。
  5. 组件懒加载:路由组件使用懒加载(() => import('@/views/XXX.vue')),非路由组件按需导入,减少初始打包体积。
  6. 避免过度封装组件:简单功能无需封装为独立组件,减少组件通信和DOM层级的开销。

(2)构建层面优化(基于 Vite/Vue CLI)

  1. 开启 Tree Shaking:Vite/Vue CLI 已默认开启 Tree Shaking,自动移除未使用的代码,减少打包体积。
  2. 配置路径别名:简化模块导入路径,同时提高构建速度(避免相对路径查找)。
    // vite.config.js
    import { defineConfig } from 'vite'
    import vue from '@vitejs/plugin-vue'
    import path from 'path'
    
    export default defineConfig({
      plugins: [vue()],
      resolve: {
        alias: {
          '@': path.resolve(__dirname, './src')
        }
      }
    })
    
  3. 压缩资源文件:
    • 压缩JS/CSS:Vite 生产环境默认使用esbuild压缩,可配置terser获得更高压缩率。
    • 压缩图片:使用vite-plugin-imagemin插件压缩图片(JPG/PNG/GIF等)。
  4. 拆分第三方依赖:将vuepiniavue-router等第三方依赖拆分为单独的chunk,利用浏览器缓存(避免每次打包都更新第三方依赖的哈希值)。
  5. 开启Gzip/Brotli压缩:在vite.config.js中配置compression插件,生成压缩包,减少网络传输体积。

(3)运行时层面优化

  1. 优化首屏加载:
    • 首屏关键资源内联(如CSS内联到HTML中,避免额外网络请求)。
    • 使用Suspense+ 异步组件,首屏先加载核心内容,非核心内容异步加载。
  2. 缓存优化:
    • 利用浏览器强缓存(设置Cache-ControlExpires响应头)和协商缓存(ETagLast-Modified)。
    • Pinia 状态持久化(pinia-plugin-persistedstate),避免页面刷新后重新请求数据。
  3. 减少DOM操作:
    • 利用 Vue 的虚拟DOM,避免手动操作DOM。
    • 复杂DOM变更使用nextTick,确保DOM更新完成后再执行操作。
  4. 清理无用资源:
    • 在组件onUnmounted钩子中清理定时器、事件监听、WebSocket连接等,避免内存泄漏。
    • 避免全局挂载大量无用的属性和方法,减少全局上下文的开销。
  5. 开启 Vue3 的编译优化:Vue3 已默认开启静态提升、PatchFlags 等编译优化,无需额外配置,确保项目使用最新版本的 Vue3,获得最佳编译优化效果。