Vue 学习笔记

定位:从基础到进阶的 Vue 3 学习笔记,覆盖核心概念、组合式 API、生态工具及实战技巧。

适合人群:有 JavaScript 基础的前端初学者 / 想系统学习 Vue 3 的开发者。


目录导航

基础篇(Ch 1-5):Vue 概述 · 模板语法 · 响应式基础 · 计算属性与侦听器 · 条件与列表渲染 组件篇(Ch 6-9):组件基础 · Props 与 Emits · 插槽 · 依赖注入 进阶篇(Ch 10-14):组合式 API · 生命周期 · 模板引用 · Teleport 与 Suspense · 自定义指令 生态篇(Ch 15-18):Vue Router · Pinia 状态管理 · VueUse 工具库 · Vite 构建工具 实战篇(Ch 19-20):项目架构 · 性能优化


1 · Vue 概述

1.1 什么是 Vue

Vue 是一个用于构建用户界面的渐进式框架。与其它大型框架不同,Vue 被设计为可以自底向上逐层应用。

  • 核心库只关注视图层,易于上手
  • 生态系统(Vue Router、Pinia、Vite)提供完整的工程化能力
  • 响应式数据驱动:修改数据 → 视图自动更新

1.2 Vue 2 vs Vue 3

特性 Vue 2 Vue 3
响应式实现 Object.defineProperty Proxy
API 风格 选项式 API(Options API) 选项式 + 组合式 API(Composition API)
生命周期 created / mounted onMounted / onUnmounted(组合式)
多根节点 不支持 支持(Fragment)
TypeScript 支持较弱 原生支持
性能 良好 显著提升(打包体积更小、渲染更快)
TIP

📌 Vue 3 是当前推荐版本。新项目应直接使用 Vue 3 + Vite + Pinia + Vue Router 的技术栈。Vue 2 已于 2023 年 12 月 31 日停止维护。

1.3 创建第一个 Vue 项目

# 使用 Vite 创建 Vue 项目(推荐)
npm create vite@latest my-vue-app -- --template vue
cd my-vue-app
npm install
npm run dev

项目结构:

my-vue-app/ ├── index.html # 入口 HTML ├── package.json ├── vite.config.js # Vite 配置 ├── src/ │ ├── main.js # 应用入口 │ ├── App.vue # 根组件 │ ├── components/ # 组件目录 │ ├── assets/ # 静态资源 │ └── style.css # 全局样式 └── public/ # 公共静态资源

1.4 单文件组件(SFC)结构

<script setup>
// 逻辑:组合式 API
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <!-- 视图:模板语法 -->
  <button @click="count++">点击了 {{ count }} 次</button>
</template>

<style scoped>
/* 样式:scoped 表示仅作用于当前组件 */
button {
  color: #42b883;
}
</style>
TIP

📌 <script setup> 是 Vue 3 的语法糖,它是组合式 API 的编译时语法糖,让你更简洁地使用组合式 API。无需 return 暴露变量,所有顶层绑定自动暴露给模板。


2 · 模板语法

2.1 文本插值

<template>
  <p>{{ message }}</p>
  <!-- 使用 v-once 只渲染一次,后续数据变化不更新 -->
  <span v-once>{{ initialMessage }}</span>
</template>

<script setup>
import { ref } from 'vue'
const message = ref('Hello Vue!')
const initialMessage = ref('只渲染一次的内容')
</script>

2.2 属性绑定(v-bind / 简写 :)

<template>
  <!-- 完整写法 -->
  <div v-bind:id="dynamicId"></div>
  <!-- 简写 -->
  <div :id="dynamicId"></div>
  <!-- 布尔属性 -->
  <button :disabled="isDisabled">按钮</button>
  <!-- 绑定多个属性 -->
  <div v-bind="objectOfAttrs"></div>
</template>

<script setup>
import { ref } from 'vue'
const dynamicId = ref('my-id')
const isDisabled = ref(true)
const objectOfAttrs = {
  id: 'container',
  class: 'wrapper'
}
</script>

2.3 事件绑定(v-on / 简写 @)

<template>
  <!-- 完整写法 -->
  <button v-on:click="handleClick">点击</button>
  <!-- 简写 -->
  <button @click="handleClick">点击</button>
  <!-- 内联表达式 -->
  <button @click="count++">加 1</button>
  <!-- 事件修饰符 -->
  <a @click.prevent="handleClick">阻止默认行为</a>
  <div @click.stop="handleClick">阻止冒泡</div>
  <!-- 按键修饰符 -->
  <input @keyup.enter="submit" />
</template>

2.4 双向绑定(v-model)

<template>
  <input v-model="message" placeholder="输入内容" />
  <p>你输入了:{{ message }}</p>
</template>

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

📌 v-model 本质是 :value + @input 的语法糖:

<input :value="message" @input="message = $event.target.value" />

等价于 <input v-model="message" />。

2.5 指令速查

指令 简写 作用
v-bind : 绑定属性
v-on @ 绑定事件
v-model — 双向绑定
v-if / v-else / v-else-if — 条件渲染(DOM 创建/销毁)
v-show — 条件显示(CSS display 切换)
v-for — 列表渲染
v-html — 渲染 HTML(注意 XSS 风险)
v-text — 渲染纯文本
v-once — 只渲染一次
v-pre — 跳过编译

3 · 响应式基础

3.1 ref — 基本类型的响应式

import { ref } from 'vue'

const count = ref(0)       // 数字
const name = ref('Vue')    // 字符串
const isActive = ref(true) // 布尔

// 访问/修改:需要 .value
console.log(count.value)   // 0
count.value++              // 1

// 在模板中自动解包,不需要 .value
// <p>{{ count }}</p>

3.2 reactive — 对象的响应式

import { reactive } from 'vue'

const state = reactive({
  count: 0,
  user: { name: '张三', age: 25 }
})

// 直接访问,不需要 .value
state.count++
state.user.age = 26

// 注意:reactive 只对对象类型有效
// const num = reactive(0)  // 无效

3.3 ref vs reactive 选择

特性 ref reactive
适用类型 基本类型 + 对象 仅对象类型
访问方式 需要 .value 直接访问
解构 不会失去响应性(用 toRefs) 解构会失去响应性
模板中 自动解包 直接使用
推荐场景 简单值、需要"整体替换" 表单、复杂对象状态
TIP

📌 最佳实践:

  • 简单值(数字、字符串、布尔)→ 用 ref
  • 表单数据、复杂对象 → 用 reactive
  • 不确定就用 ref,它对对象也有效(内部会自动用 reactive 包装) :::

3.4 响应式工具函数

import { ref, reactive, readonly, computed, watch, toRefs, toRef } from 'vue'

const state = reactive({ count: 0, name: 'Vue' })

// toRefs:解构 reactive 对象而不丢失响应性
const { count, name } = toRefs(state)

// toRef:将 reactive 的某个属性转为 ref
const countRef = toRef(state, 'count')

// readonly:创建只读代理
const readOnlyState = readonly(state)

4 · 计算属性与侦听器

4.1 计算属性(computed)

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

const firstName = ref('张')
const lastName = ref('三')

// 计算属性:有缓存,依赖不变时不会重新计算
const fullName = computed(() => `${firstName.value}${lastName.value}`)

// 可写计算属性
const fullName2 = computed({
  get: () => `${firstName.value}${lastName.value}`,
  set: (newValue) => {
    [firstName.value, lastName.value] = newValue.split(' ')
  }
})
</script>

4.2 computed vs methods

特性 computed methods
缓存 有缓存(依赖不变不重算) 每次调用都执行
调用 像属性一样访问(不加括号) 像函数一样调用
适用 基于响应式数据的派生值 需要参数或不需要缓存的逻辑

4.3 侦听器(watch / watchEffect)

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

const count = ref(0)
const user = reactive({ name: '张三', age: 25 })

// watch:明确指定监听目标,可获取新旧值
watch(count, (newVal, oldVal) => {
  console.log(`count: ${oldVal} → ${newVal}`)
})

// 监听 reactive 属性需要用 getter 函数
watch(() => user.age, (newVal, oldVal) => {
  console.log(`age: ${oldVal} → ${newVal}`)
})

// deep: 深度监听对象内部变化
watch(user, (newVal) => {
  console.log('user 变化了', newVal)
}, { deep: true })

// immediate: 立即执行一次
watch(count, (newVal) => {
  console.log('立即执行', newVal)
}, { immediate: true })

// watchEffect:自动追踪依赖,立即执行
watchEffect(() => {
  console.log(`count = ${count.value}, name = ${user.name}`)
})

:::tip 📌 watch vs watchEffect:

  • watch:明确指定监听目标,可获取新旧值,默认不立即执行
  • watchEffect:自动追踪回调中的响应式依赖,立即执行一次,无法获取旧值

选择:需要旧值 → watch;只需副作用 → watchEffect


5 · 条件与列表渲染

5.1 条件渲染

<template>
  <!-- v-if:DOM 创建/销毁 -->
  <div v-if="type === 'A'">A</div>
  <div v-else-if="type === 'B'">B</div>
  <div v-else>C</div>

  <!-- v-show:CSS display 切换(DOM 始终存在) -->
  <div v-show="isVisible">始终在 DOM 中</div>
</template>
特性 v-if v-show
实现方式 DOM 创建/销毁 CSS display 切换
初始渲染 条件为 false 时不渲染 始终渲染
切换开销 高(DOM 操作) 低(CSS 切换)
适用 不频繁切换 频繁切换

5.2 列表渲染

<template>
  <!-- v-for 遍历数组 -->
  <li v-for="(item, index) in items" :key="item.id">
    {{ index }}: {{ item.name }}
  </li>

  <!-- v-for 遍历对象 -->
  <li v-for="(value, key) in object" :key="key">
    {{ key }}: {{ value }}
  </li>

  <!-- v-for 遍历数字 -->
  <span v-for="n in 10" :key="n">{{ n }}</span>
</template>

<script setup>
import { ref } from 'vue'
const items = ref([
  { id: 1, name: '苹果' },
  { id: 2, name: '香蕉' },
  { id: 3, name: '橙子' }
])
</script>
TIP

v-for 必须加 :key!

  • key 应该是唯一且稳定的(如 item.id),不要用 index
  • 用 index 做 key 会导致数据错乱和性能问题(尤其在列表增删时)
  • key 帮助 Vue 高效复用 DOM 节点 :::

5.3 列表更新检测

// 这些操作 Vue 能检测到:
items.value.push(newItem)        // 末尾添加
items.value.pop()                // 末尾删除
items.value.shift()              // 开头删除
items.value.unshift(newItem)     // 开头添加
items.value.splice(2, 1)         // 任意位置删除
items.value.sort()               // 排序
items.value.reverse()            // 反转

// 直接通过索引修改不会触发更新:
// items.value[0] = newItem

// 正确做法:
items.value[0] = newItem  // ref 包裹的数组在 Vue 3 中可以直接修改
// 或
items.value.splice(0, 1, newItem)

6 · 组件基础

6.1 定义组件

<!-- MyComponent.vue -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
</script>

<template>
  <button @click="count++">点击了 {{ count }} 次</button>
</template>

6.2 使用组件

<!-- App.vue -->
<script setup>
import MyComponent from './MyComponent.vue'
</script>

<template>
  <MyComponent />
  <!-- 或 PascalCase / kebab-case 均可 -->
  <my-component />
</template>

6.3 组件命名规范

  • PascalCase(推荐):MyComponent.vue,模板中 <MyComponent />
  • kebab-case:my-component.vue,模板中 <my-component />
  • 组件名应多词组合,避免与 HTML 原生标签冲突

7 · Props 与 Emits

7.1 Props — 父传子

<!-- 子组件 Child.vue -->
<script setup>
// 方式 1:简单声明
defineProps(['title', 'count'])

// 方式 2:带类型和默认值(推荐)
const props = defineProps({
  title: String,
  count: {
    type: Number,
    default: 0,
    required: true,
    validator: (value) => value >= 0
  }
})
</script>

<template>
  <h2>{{ title }}</h2>
  <p>计数:{{ count }}</p>
</template>
<!-- 父组件 Parent.vue -->
<script setup>
import Child from './Child.vue'
import { ref } from 'vue'
const title = ref('Hello')
</script>

<template>
  <!-- 静态 prop -->
  <Child title="静态标题" :count="5" />
  <!-- 动态 prop -->
  <Child :title="title" :count="10" />
</template>

7.2 Emits — 子传父

<!-- 子组件 Child.vue -->
<script setup>
// 声明自定义事件
const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', { id: 1, name: '新名称' })
}
</script>

<template>
  <button @click="handleClick">更新</button>
</template>
<!-- 父组件 Parent.vue -->
<script setup>
import Child from './Child.vue'

function handleUpdate(data) {
  console.log('收到更新', data)
}
</script>

<template>
  <Child @update="handleUpdate" />
</template>

7.3 v-model 在组件上

<!-- 子组件 CustomInput.vue -->
<script setup>
const modelValue = defineModel()
// defineModel() 是 Vue 3.4+ 的宏,等价于:
// const props = defineProps(['modelValue'])
// const emit = defineEmits(['update:modelValue'])
</script>

<template>
  <input v-model="modelValue" />
</template>
<!-- 父组件 -->
<template>
  <CustomInput v-model="message" />
  <!-- 等价于 -->
  <CustomInput :model-value="message" @update:model-value="message = $event" />
</template>

8 · 插槽(Slots)

8.1 默认插槽

<!-- 子组件 Card.vue -->
<template>
  <div class="card">
    <slot>默认内容(父组件不传时显示)</slot>
  </div>
</template>
<!-- 父组件 -->
<template>
  <Card>
    <p>这是传入的内容</p>
  </Card>
</template>

8.2 具名插槽

<!-- 子组件 Layout.vue -->
<template>
  <header><slot name="header" /></header>
  <main><slot /></main>
  <footer><slot name="footer" /></footer>
</template>
<!-- 父组件 -->
<template>
  <Layout>
    <template #header>
      <h1>标题</h1>
    </template>
    <p>主体内容</p>
    <template #footer>
      <p>页脚</p>
    </template>
  </Layout>
</template>

8.3 作用域插槽

<!-- 子组件 List.vue -->
<script setup>
const items = ['苹果', '香蕉', '橙子']
</script>

<template>
  <ul>
    <li v-for="(item, index) in items" :key="index">
      <slot :item="item" :index="index" />
    </li>
  </ul>
</template>
<!-- 父组件 -->
<template>
  <List v-slot="{ item, index }">
    {{ index + 1 }}. {{ item }}
  </List>
</template>

9 · 依赖注入(Provide / Inject)

9.1 基本用法

<!-- 祖先组件 -->
<script setup>
import { provide, ref } from 'vue'

const theme = ref('dark')
const user = ref({ name: '张三' })

// provide(key, value)
provide('theme', theme)
provide('user', user)
</script>
<!-- 后代组件(任意层级) -->
<script setup>
import { inject } from 'vue'

// inject(key, defaultValue)
const theme = inject('theme', 'light')
const user = inject('user')
</script>

9.2 响应式 provide/inject

// 祖先:提供 readonly 版本 + 修改方法
import { provide, ref, readonly } from 'vue'

const count = ref(0)
const increment = () => count.value++

provide('count', readonly(count))
provide('increment', increment)
// 后代:只能读,不能直接改
import { inject } from 'vue'

const count = inject('count')       // readonly
const increment = inject('increment') // 修改方法

:::tip 📌 最佳实践:provide 响应式数据时,提供 readonly 版本 + 修改方法,避免后代组件直接修改状态,保持数据流向清晰。


10 · 组合式 API(Composition API)

10.1 为什么需要组合式 API

选项式 API 的问题: ├── 相关逻辑分散在不同选项中(data / methods / watch / computed) ├── 组件变大后,同一个功能的代码跳来跳去 └── 逻辑复用困难(mixin 有命名冲突、来源不清晰等问题) 组合式 API 的优势: ├── 同一功能的逻辑集中在一起 ├── 自由组织代码结构 ├── 通过自定义 Composable 函数实现逻辑复用 └── 更好的 TypeScript 类型推导

10.2 Composable 函数 — 逻辑复用

// useCounter.js — 可复用的计数器逻辑
import { ref } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  const increment = () => count.value++
  const decrement = () => count.value--
  const reset = () => count.value = initialValue

  return { count, increment, decrement, reset }
}
<!-- 在组件中使用 -->
<script setup>
import { useCounter } from './useCounter'

const { count, increment, reset } = useCounter(10)
</script>

<template>
  <button @click="increment">{{ count }}</button>
  <button @click="reset">重置</button>
</template>

10.3 常用 Composable 模式

// useFetch.js — 数据请求
import { ref, onMounted } from 'vue'

export function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(true)

  async function fetchData() {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(url)
      data.value = await res.json()
    } catch (e) {
      error.value = e.message
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchData)

  return { data, error, loading, refresh: fetchData }
}
TIP

📌 Composable 命名规范:以 use 开头,如 useCounter、useFetch、useMousePosition。这是社区约定俗成的规则。


11 · 生命周期

11.1 生命周期钩子对照

选项式 API 组合式 API(<script setup>) 说明
beforeCreate —(setup 替代) 组件实例创建前
created —(setup 替代) 组件实例创建后
beforeMount onBeforeMount 挂载前
mounted onMounted 挂载后(可访问 DOM)
beforeUpdate onBeforeUpdate 更新前
updated onUpdated 更新后
beforeUnmount onBeforeUnmount 卸载前
unmounted onUnmounted 卸载后
errorCaptured onErrorCaptured 捕获后代组件错误

11.2 使用示例

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

const timer = ref(null)

onMounted(() => {
  console.log('组件已挂载,可以访问 DOM')
  timer.value = setInterval(() => {
    console.log('定时器运行中...')
  }, 1000)
})

onUnmounted(() => {
  console.log('组件已卸载,清理定时器')
  clearInterval(timer.value)
})
</script>
TIP

📌 常见用途:

  • onMounted:发起 API 请求、初始化第三方库、访问 DOM
  • onUnmounted:清理定时器、移除事件监听、销毁实例
  • onUpdated:数据更新后操作 DOM(避免在此修改数据,可能死循环) :::

12 · 模板引用(Template Refs)

12.1 基本用法

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

const inputRef = ref(null)

onMounted(() => {
  inputRef.value.focus()  // 访问 DOM 元素
})
</script>

<template>
  <input ref="inputRef" />
</template>

12.2 引用子组件

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

const childRef = ref(null)

onMounted(() => {
  // 访问子组件通过 defineExpose 暴露的内容
  childRef.value.someMethod()
})
</script>

<template>
  <ChildComponent ref="childRef" />
</template>
<!-- ChildComponent.vue -->
<script setup>
import { defineExpose } from 'vue'

function someMethod() {
  console.log('被子组件调用')
}

// 必须显式暴露,父组件才能访问
defineExpose({ someMethod })
</script>

:::tip <script setup> 组件默认是封闭的,父组件通过 ref 无法直接访问子组件的变量和方法。必须使用 defineExpose 显式暴露。


13 · Teleport 与 Suspense

13.1 Teleport — 将内容传送到 DOM 的其他位置

<template>
  <!-- 将弹窗传送到 body 下,避免父组件的 overflow/z-index 影响 -->
  <Teleport to="body">
    <div v-if="showModal" class="modal">
      <p>这是一个弹窗</p>
      <button @click="showModal = false">关闭</button>
    </div>
  </Teleport>
</template>

13.2 Suspense — 处理异步组件

<template>
  <Suspense>
    <!-- 异步组件 -->
    <template #default>
      <AsyncComponent />
    </template>
    <!-- 加载中 -->
    <template #fallback>
      <p>加载中...</p>
    </template>
  </Suspense>
</template>

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

const AsyncComponent = defineAsyncComponent(() =>
  import('./HeavyComponent.vue')
)
</script>

14 · 自定义指令

14.1 局部指令

<script setup>
// v-focus:自动聚焦
const vFocus = {
  mounted: (el) => el.focus()
}
</script>

<template>
  <input v-focus />
</template>

14.2 全局指令

// main.js
import { createApp } from 'vue'
import App from './App.vue'

const app = createApp(App)

// 注册全局指令
app.directive('focus', {
  mounted: (el) => el.focus()
})

// 常用:v-permission 权限控制
app.directive('permission', {
  mounted: (el, binding) => {
    if (!checkPermission(binding.value)) {
      el.parentNode.removeChild(el)
    }
  }
})

app.mount('#app')

14.3 指令钩子

钩子 说明
created 元素属性和事件监听器设置前
beforeMount 挂载前
mounted 挂载后
beforeUpdate 更新前
updated 更新后
beforeUnmount 卸载前
unmounted 卸载后

15 · Vue Router

15.1 安装与基本配置

npm install vue-router@4
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'

const routes = [
  { path: '/', name: 'home', component: () => import('../views/Home.vue') },
  { path: '/about', name: 'about', component: () => import('../views/About.vue') },
  { path: '/user/:id', name: 'user', component: () => import('../views/User.vue'), props: true },
  { path: '/:pathMatch(.*)*', name: 'not-found', component: () => import('../views/NotFound.vue') }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

export default router
// main.js
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'

createApp(App).use(router).mount('#app')

15.2 路由使用

<template>
  <!-- 路由链接 -->
  <router-link to="/">首页</router-link>
  <router-link :to="{ name: 'user', params: { id: 123 } }">用户</router-link>

  <!-- 路由出口 -->
  <router-view />
</template>

<script setup>
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()  // 编程式导航
const route = useRoute()    // 当前路由信息

// 跳转
router.push('/about')
router.push({ name: 'user', params: { id: 123 } })
router.replace('/login')
router.back()

// 获取路由参数
console.log(route.params.id)
console.log(route.query.q)
</script>

15.3 导航守卫

// 全局前置守卫
router.beforeEach((to, from, next) => {
  const isAuthenticated = localStorage.getItem('token')
  if (to.meta.requiresAuth && !isAuthenticated) {
    next('/login')
  } else {
    next()
  }
})

// 路由独享守卫
{
  path: '/admin',
  component: Admin,
  beforeEnter: (to, from, next) => {
    if (isAdmin()) next()
    else next('/403')
  }
}

16 · Pinia 状态管理

16.1 安装与基本使用

npm install pinia
// 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')

16.2 定义 Store

// stores/counter.js
import { defineStore } from 'pinia'

// 方式 1:Options Store
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0,
    history: []
  }),
  getters: {
    doubleCount: (state) => state.count * 2,
    // 访问其他 getter
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  actions: {
    increment() {
      this.count++
      this.history.push('increment')
    },
    async fetchData() {
      const res = await fetch('/api/data')
      this.count = await res.json()
    }
  }
})
// 方式 2:Setup Store(推荐,更灵活)
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const history = ref([])

  const doubleCount = computed(() => count.value * 2)

  function increment() {
    count.value++
    history.value.push('increment')
  }

  return { count, history, doubleCount, increment }
})

16.3 在组件中使用

<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'

const counterStore = useCounterStore()

// 直接使用
console.log(counterStore.count)
counterStore.increment()

// 解构需要 storeToRefs 保持响应性
const { count, doubleCount } = storeToRefs(counterStore)
// actions 可以直接解构
const { increment } = counterStore
</script>

:::tip 📌 Pinia vs Vuex:Pinia 是 Vue 官方推荐的新一代状态管理库,取代了 Vuex。

  • 更简洁的 API(无 mutations)
  • 完美的 TypeScript 支持
  • 支持组合式 API 风格
  • 体积更小 :::

17 · VueUse 工具库

17.1 什么是 VueUse

VueUse 是一个 Vue 组合式 API 工具集合,提供 200+ 个实用的 Composable 函数。

npm install @vueuse/core

17.2 常用工具

import { useMouse, useLocalStorage, useDebounceFn, useEventListener, useDark, useToggle } from '@vueuse/core'

// 鼠标位置
const { x, y } = useMouse()

// 本地存储(自动序列化/反序列化,响应式)
const settings = useLocalStorage('settings', { theme: 'dark', lang: 'zh' })

// 防抖函数
const debouncedSearch = useDebounceFn((keyword) => {
  console.log('搜索:', keyword)
}, 500)

// 事件监听(自动清理)
useEventListener(window, 'scroll', () => {
  console.log('滚动位置:', window.scrollY)
})

// 暗黑模式
const isDark = useDark()
const toggleDark = useToggle(isDark)

18 · Vite 构建工具

18.1 Vite 配置

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { fileURLToPath, URL } from 'node:url'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': fileURLToPath(new URL('./src', import.meta.url))
    }
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  build: {
    outDir: 'dist',
    chunkSizeWarningLimit: 1000,
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['vue', 'vue-router', 'pinia']
        }
      }
    }
  }
})

18.2 环境变量

# .env
VITE_API_URL=http://localhost:8080/api

# .env.production
VITE_API_URL=https://api.example.com
// 使用环境变量
const apiUrl = import.meta.env.VITE_API_URL

19 · 项目架构最佳实践

19.1 推荐目录结构

src/ ├── api/ # API 请求封装 │ ├── request.js # axios 封装 │ ├── user.js # 用户相关 API │ └── product.js # 商品相关 API ├── assets/ # 静态资源 ├── components/ # 公共组件 │ ├── common/ # 基础组件 │ └── business/ # 业务组件 ├── composables/ # 组合式函数 ├── layouts/ # 布局组件 ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── styles/ # 全局样式 ├── utils/ # 工具函数 ├── views/ # 页面组件 ├── App.vue └── main.js

19.2 API 请求封装

// api/request.js
import axios from 'axios'
import { useUserStore } from '@/stores/user'

const request = axios.create({
  baseURL: import.meta.env.VITE_API_URL,
  timeout: 10000
})

// 请求拦截器
request.interceptors.request.use((config) => {
  const userStore = useUserStore()
  if (userStore.token) {
    config.headers.Authorization = `Bearer ${userStore.token}`
  }
  return config
})

// 响应拦截器
request.interceptors.response.use(
  (response) => response.data,
  (error) => {
    if (error.response?.status === 401) {
      // 跳转登录
    }
    return Promise.reject(error)
  }
)

export default request

20 · 性能优化

20.1 路由懒加载

// 所有路由组件使用动态 import,按需加载
const routes = [
  { path: '/', component: () => import('@/views/Home.vue') },
  { path: '/about', component: () => import('@/views/About.vue') }
]

20.2 组件异步加载

import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent({
  loader: () => import('@/components/HeavyChart.vue'),
  loadingComponent: LoadingSpinner,
  errorComponent: ErrorDisplay,
  delay: 200,
  timeout: 10000
})

20.3 v-once 与 v-memo

<!-- v-once:只渲染一次,适合静态内容 -->
<div v-once>{{ staticContent }}</div>

<!-- v-memo:按依赖条件缓存,依赖不变时跳过更新 -->
<div v-memo="[item.id, item.selected]">
  <!-- 复杂内容 -->
</div>

20.4 列表虚拟滚动

<!-- 使用 @vueuse/core 的 useVirtualList -->
<script setup>
import { useVirtualList } from '@vueuse/core'

const allItems = Array.from({ length: 10000 }, (_, i) => ({ id: i, name: `Item ${i}` }))

const { list, containerProps, wrapperProps } = useVirtualList(
  allItems,
  { itemHeight: 40 }
)
</script>

<template>
  <div v-bind="containerProps" style="height: 400px; overflow-y: auto;">
    <div v-bind="wrapperProps">
      <div v-for="item in list" :key="item.data.id" style="height: 40px;">
        {{ item.data.name }}
      </div>
    </div>
  </div>
</template>

20.5 性能优化清单

优化项 方法 收益
路由懒加载 () => import() 减小首屏体积
代码分割 manualChunks 利用浏览器缓存
图片懒加载 v-lazy / IntersectionObserver 减少初始请求
虚拟滚动 useVirtualList 大列表性能
v-memo 缓存子树 减少不必要的 diff
shallowRef / shallowReactive 浅层响应式 减少代理开销
markRaw 跳过响应式 第三方实例无需代理
Tree-shaking 按需引入 减小打包体积
本页目录