《2026 Vue2 + Vue3 完整学习指南:基础语法、路由缓存、登录拦截、项目实战与面试题》

2026 Vue2 + Vue3 超详细讲解:基础、项目实战、路由缓存、登录鉴权、性能优化与面试题

适合人群:Vue 初学者、前端开发工程师、准备面试的人、正在做后台管理系统 / 官网 / H5 / 数据大屏项目的人。

建议技术栈:Vue 3 + TypeScript + Vite + Vue Router + Pinia + Axios + Element Plus / Naive UI

说明:Vue2 已经在 2023-12-31 结束官方维护,但很多老项目仍在使用,所以本文会同时讲 Vue2 和 Vue3,并重点说明二者区别、迁移思路和项目中常见写法。


目录

  • [一、Vue 是什么](#一、Vue 是什么)
  • [二、2026 年为什么还要学 Vue2](#二、2026 年为什么还要学 Vue2)
  • [三、Vue2 和 Vue3 核心区别](#三、Vue2 和 Vue3 核心区别)
  • [四、Vue2 基础知识详细讲解](#四、Vue2 基础知识详细讲解)
  • [五、Vue3 基础知识详细讲解](#五、Vue3 基础知识详细讲解)
  • [六、Vue3 Composition API 详细讲解](#六、Vue3 Composition API 详细讲解)
  • [七、Vue Router 路由详细讲解](#七、Vue Router 路由详细讲解)
  • [八、页面缓存 KeepAlive 与路由缓存](#八、页面缓存 KeepAlive 与路由缓存)
  • 九、登录注册、Token、路由拦截、权限控制完整流程
  • [十、Axios 请求封装与接口管理](#十、Axios 请求封装与接口管理)
  • [十一、Pinia 与 Vuex 状态管理](#十一、Pinia 与 Vuex 状态管理)
  • 十二、后台管理系统项目架构
  • 十三、组件封装思路
  • [十四、Vue 性能优化](#十四、Vue 性能优化)
  • [十五、Vue 项目部署上线](#十五、Vue 项目部署上线)
  • [十六、Vue 面试题 80 道](#十六、Vue 面试题 80 道)
  • [十七、Vue 学习路线](#十七、Vue 学习路线)
  • [十八、Vue 官方与常用资源链接](#十八、Vue 官方与常用资源链接)

一、Vue 是什么

Vue 是一个用于构建用户界面的渐进式 JavaScript 框架。它的核心思想是:

txt 复制代码
数据驱动视图
组件化开发
响应式更新
声明式渲染

传统开发中,页面变化通常需要手动操作 DOM:

js 复制代码
document.querySelector('#title').innerText = '新标题'

Vue 中通常只需要修改数据:

js 复制代码
title.value = '新标题'

页面会自动更新。

Vue 的优势

优势 说明
上手快 模板语法接近 HTML,初学者容易理解
组件化 页面可以拆成组件,方便复用和维护
响应式 数据变化后视图自动更新
生态完善 Router、Pinia、Vite、Nuxt、Element Plus 等生态成熟
适合中后台 国内后台管理系统大量使用 Vue
渐进式 可以局部使用,也可以开发大型 SPA

Vue 常见应用场景

场景 说明
后台管理系统 用户管理、权限管理、订单管理、内容管理
企业官网 展示型页面、产品介绍、品牌官网
H5 移动端 活动页、营销页、移动端管理页面
数据大屏 ECharts + Vue 实现可视化大屏
电商系统 商品、购物车、订单、支付流程
CMS 系统 文章、分类、标签、权限
低代码平台 动态表单、动态页面、组件拖拽

二、2026 年为什么还要学 Vue2

Vue2 已在 2023-12-31 结束官方维护,不再获得新功能、更新或修复。新项目建议使用 Vue3。

但是仍然需要了解 Vue2,原因有:

txt 复制代码
1. 很多公司老项目仍然是 Vue2
2. 面试经常问 Vue2 和 Vue3 区别
3. 维护老项目需要看懂 Options API、Vuex、Vue Router 3
4. Vue3 的很多概念是从 Vue2 演进而来
5. 迁移 Vue2 到 Vue3 需要理解二者差异

建议学习策略:

情况 建议
新项目 Vue3
老项目维护 Vue2 + Vuex + Vue Router 3
面试准备 Vue2、Vue3 都要掌握
企业后台 Vue3 + TS + Pinia
迁移项目 先理解 Vue2 Options API,再学 Vue3 Composition API

三、Vue2 和 Vue3 核心区别

对比项 Vue2 Vue3
发布时间 2016 年左右成熟流行 2020 年发布,当前主流
维护状态 已 EOL,不再官方维护 当前主流版本
响应式原理 Object.defineProperty Proxy
API 风格 Options API Composition API + Options API
入口文件 new Vue() createApp()
状态管理 Vuex Pinia 推荐
路由版本 Vue Router 3 Vue Router 4
构建工具 Vue CLI / Webpack Vite
TypeScript 支持一般 更友好
多根节点 不支持,组件必须单根 支持 Fragment 多根节点
Teleport 不支持 支持
Suspense 不支持 支持
Tree-shaking 较弱 更好
性能 较好 更好
生命周期命名 mounteddestroyed onMountedonUnmounted
双向绑定 .syncmodel v-model 多参数更强

Vue2 响应式缺陷

Vue2 使用 Object.defineProperty 劫持对象属性,所以有一些限制:

js 复制代码
data() {
  return {
    user: {
      name: '张三'
    },
    list: [1, 2, 3]
  }
}

以下操作在 Vue2 中可能无法被正确监听:

js 复制代码
this.user.age = 18
this.list[0] = 100
this.list.length = 0

Vue2 中需要这样处理:

js 复制代码
this.$set(this.user, 'age', 18)
this.$set(this.list, 0, 100)

Vue3 使用 Proxy 后,对对象新增属性、删除属性、数组下标变化等支持更好。


四、Vue2 基础知识详细讲解

4.1 Vue2 项目创建

老项目常见创建方式:

bash 复制代码
npm install -g @vue/cli
vue create my-vue2-app

如果创建 Vue2 项目,需要在 Vue CLI 中选择 Vue2。

Vue2 常见技术栈:

txt 复制代码
Vue2 + Vue Router 3 + Vuex + Axios + Element UI + Webpack

4.2 Vue2 基本写法

vue 复制代码
<template>
  <div class="page">
    <h1>{{ title }}</h1>
    <p>数量:{{ count }}</p>
    <button @click="add">增加</button>
  </div>
</template>

<script>
export default {
  name: 'HomePage',

  data() {
    return {
      title: 'Vue2 基础',
      count: 0
    }
  },

  methods: {
    add() {
      this.count++
    }
  }
}
</script>

<style scoped>
.page {
  padding: 20px;
}
</style>

4.3 data 为什么必须是函数

组件中的 data 必须写成函数:

js 复制代码
data() {
  return {
    count: 0
  }
}

原因:组件可能被复用多次,如果 data 是对象,多个组件实例会共用同一份数据,造成数据污染。写成函数后,每个组件实例都会返回一份独立数据。

4.4 methods 方法

js 复制代码
methods: {
  submit() {
    console.log('提交表单')
  },
  reset() {
    this.form = {
      username: '',
      password: ''
    }
  }
}

methods 适合放事件处理、业务操作、接口请求等函数。

4.5 computed 计算属性

js 复制代码
computed: {
  totalPrice() {
    return this.goodsList.reduce((sum, item) => {
      return sum + item.price * item.count
    }, 0)
  }
}

特点:

txt 复制代码
1. 有缓存
2. 依赖数据变化才会重新计算
3. 适合总价、过滤、统计、格式化

4.6 watch 监听器

js 复制代码
watch: {
  keyword(newVal, oldVal) {
    console.log('搜索词变化:', newVal)
  },

  user: {
    handler(newVal) {
      console.log('用户信息变化:', newVal)
    },
    deep: true,
    immediate: true
  }
}

常见用途:

txt 复制代码
监听搜索词变化后请求接口
监听路由参数变化
监听表单变化
监听弹窗显示隐藏

4.7 Vue2 生命周期

生命周期 说明
beforeCreate 实例初始化前,data 和 methods 还不能用
created data、methods 可用,DOM 未挂载
beforeMount 挂载前
mounted DOM 挂载完成,常用于请求数据、初始化图表
beforeUpdate 数据变化,DOM 更新前
updated DOM 更新后
beforeDestroy 组件销毁前
destroyed 组件销毁后
activated 被 keep-alive 缓存的组件激活
deactivated 被 keep-alive 缓存的组件停用

示例:

js 复制代码
export default {
  created() {
    console.log('可以请求接口,但不能直接操作 DOM')
  },

  mounted() {
    console.log('DOM 已挂载,可以初始化 ECharts')
  },

  beforeDestroy() {
    console.log('清除定时器、解绑事件')
  }
}

4.8 Vue2 组件通信

父传子 props

父组件:

vue 复制代码
<UserCard :user="userInfo" />

子组件:

js 复制代码
export default {
  props: {
    user: {
      type: Object,
      required: true
    }
  }
}
子传父 $emit

子组件:

js 复制代码
this.$emit('change', '子组件数据')

父组件:

vue 复制代码
<Child @change="handleChange" />
兄弟组件通信

常见方式:

txt 复制代码
1. 共同父组件中转
2. EventBus
3. Vuex
4. provide / inject

Vue2 EventBus 示例:

js 复制代码
// eventBus.js
import Vue from 'vue'
export default new Vue()

发送:

js 复制代码
eventBus.$emit('refreshList')

接收:

js 复制代码
eventBus.$on('refreshList', () => {
  this.getList()
})

注意:EventBus 在大型项目中容易混乱,建议优先使用 Vuex / Pinia。

4.9 Vue2 Vuex 状态管理

js 复制代码
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    token: '',
    userInfo: null
  },

  mutations: {
    SET_TOKEN(state, token) {
      state.token = token
    },
    SET_USER_INFO(state, userInfo) {
      state.userInfo = userInfo
    }
  },

  actions: {
    login({ commit }, token) {
      commit('SET_TOKEN', token)
    }
  },

  getters: {
    isLogin(state) {
      return !!state.token
    }
  }
})

组件中使用:

js 复制代码
this.$store.commit('SET_TOKEN', 'abc123')
this.$store.dispatch('login', 'abc123')
console.log(this.$store.state.token)

五、Vue3 基础知识详细讲解

5.1 Vue3 项目创建

推荐:

bash 复制代码
npm create vue@latest

常见选择:

txt 复制代码
TypeScript:Yes
Vue Router:Yes
Pinia:Yes
ESLint:Yes
Prettier:Yes
Vitest:根据需要选择

启动项目:

bash 复制代码
cd my-vue-app
npm install
npm run dev

打包:

bash 复制代码
npm run build

5.2 Vue3 项目结构

推荐结构:

txt 复制代码
src
├── api                 # 接口
├── assets              # 静态资源
├── components          # 通用组件
├── composables         # 组合式函数
├── constants           # 常量
├── directives          # 自定义指令
├── hooks               # 可复用逻辑,也可叫 composables
├── layouts             # 布局组件
├── router              # 路由
├── stores              # Pinia
├── styles              # 全局样式
├── types               # TS 类型
├── utils               # 工具函数
├── views               # 页面
├── App.vue
└── main.ts

5.3 main.ts 入口文件

ts 复制代码
import { createApp } from 'vue'
import { createPinia } from 'pinia'

import App from './App.vue'
import router from './router'

import './styles/index.scss'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')

5.4 Vue3 单文件组件

vue 复制代码
<template>
  <div class="page">
    <h1>{{ title }}</h1>
    <button @click="changeTitle">修改标题</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const title = ref('Vue3 基础知识')

const changeTitle = () => {
  title.value = 'Vue3 + TypeScript 项目开发'
}
</script>

<style scoped lang="scss">
.page {
  padding: 20px;

  h1 {
    color: #333;
  }
}
</style>

5.5 模板语法

插值表达式
vue 复制代码
<p>{{ message }}</p>
属性绑定
vue 复制代码
<img :src="avatar" :alt="username" />
事件绑定
vue 复制代码
<button @click="submit">提交</button>
条件渲染
vue 复制代码
<div v-if="status === 1">启用</div>
<div v-else-if="status === 2">禁用</div>
<div v-else>未知</div>
显示隐藏
vue 复制代码
<div v-show="visible">弹窗内容</div>

v-ifv-show 区别:

指令 原理 适合场景
v-if 创建 / 销毁 DOM 条件很少变化
v-show display: none 频繁切换
列表渲染
vue 复制代码
<ul>
  <li v-for="item in userList" :key="item.id">
    {{ item.name }}
  </li>
</ul>

不要随便用 index 作为 key,尤其是列表会增删改排序时。

5.6 class 与 style 绑定

vue 复制代码
<div :class="{ active: isActive, disabled: isDisabled }"></div>
vue 复制代码
<div :class="['card', status === 1 ? 'success' : 'error']"></div>
vue 复制代码
<div :style="{ color: textColor, fontSize: fontSize + 'px' }"></div>

5.7 表单绑定 v-model

vue 复制代码
<input v-model="form.username" placeholder="请输入用户名" />
<input v-model="form.password" type="password" placeholder="请输入密码" />
ts 复制代码
import { reactive } from 'vue'

const form = reactive({
  username: '',
  password: ''
})

常见修饰符:

vue 复制代码
<input v-model.trim="username" />
<input v-model.number="age" />
<input v-model.lazy="keyword" />
修饰符 说明
.trim 去除首尾空格
.number 转为数字
.lazy change 时更新,不是 input 时实时更新

六、Vue3 Composition API 详细讲解

Vue3 最核心的是 Composition API,尤其是 <script setup> 写法。

6.1 ref

ref 用于定义响应式数据。

ts 复制代码
import { ref } from 'vue'

const count = ref(0)

const add = () => {
  count.value++
}

注意:

txt 复制代码
在 script 中使用 ref,要通过 .value 访问
在 template 中使用 ref,不需要 .value

模板:

vue 复制代码
<p>{{ count }}</p>

6.2 reactive

reactive 用于定义响应式对象。

ts 复制代码
import { reactive } from 'vue'

const form = reactive({
  username: '',
  password: ''
})

const reset = () => {
  form.username = ''
  form.password = ''
}

6.3 ref 和 reactive 选择

场景 推荐
数字、字符串、布尔值 ref
数组 ref 更常见
接口返回列表 ref<T[]>([])
表单对象 reactive
需要整体替换对象 ref
局部字段频繁修改 reactive

推荐写法:

ts 复制代码
const loading = ref(false)
const list = ref<User[]>([])
const detail = ref<UserDetail | null>(null)

const form = reactive<LoginForm>({
  username: '',
  password: ''
})

6.4 computed

ts 复制代码
import { computed, ref } from 'vue'

const price = ref(100)
const count = ref(2)

const total = computed(() => {
  return price.value * count.value
})

可写计算属性:

ts 复制代码
const firstName = ref('Zhang')
const lastName = ref('San')

const fullName = computed({
  get() {
    return firstName.value + ' ' + lastName.value
  },
  set(value: string) {
    const names = value.split(' ')
    firstName.value = names[0]
    lastName.value = names[1]
  }
})

6.5 watch

监听单个 ref:

ts 复制代码
watch(keyword, (newVal, oldVal) => {
  console.log(newVal, oldVal)
})

监听多个数据:

ts 复制代码
watch([startDate, endDate], ([newStart, newEnd]) => {
  getList()
})

监听对象属性:

ts 复制代码
watch(
  () => form.username,
  (val) => {
    console.log('用户名变化:', val)
  }
)

立即执行:

ts 复制代码
watch(
  keyword,
  () => {
    getList()
  },
  {
    immediate: true
  }
)

深度监听:

ts 复制代码
watch(
  form,
  () => {
    console.log('表单变化')
  },
  {
    deep: true
  }
)

6.6 watchEffect

watchEffect 会自动收集依赖。

ts 复制代码
watchEffect(() => {
  console.log('当前关键词:', keyword.value)
})

区别:

对比 watch watchEffect
是否需要指定监听源 需要 不需要
是否立即执行 默认不立即 默认立即
是否能拿到旧值 可以 不适合
适合场景 精确监听 自动收集依赖

6.7 生命周期

Vue2 Vue3 Composition API
beforeCreate setup
created setup
beforeMount onBeforeMount
mounted onMounted
beforeUpdate onBeforeUpdate
updated onUpdated
beforeDestroy onBeforeUnmount
destroyed onUnmounted
activated onActivated
deactivated onDeactivated

示例:

ts 复制代码
import { onMounted, onUnmounted } from 'vue'

let timer: number | null = null

onMounted(() => {
  timer = window.setInterval(() => {
    console.log('定时任务')
  }, 1000)
})

onUnmounted(() => {
  if (timer) {
    clearInterval(timer)
  }
})

6.8 props

子组件:

vue 复制代码
<script setup lang="ts">
interface Props {
  title: string
  count?: number
}

const props = withDefaults(defineProps<Props>(), {
  count: 0
})
</script>

<template>
  <div>{{ title }} - {{ count }}</div>
</template>

父组件:

vue 复制代码
<Child title="用户列表" :count="10" />

6.9 emits

子组件:

vue 复制代码
<script setup lang="ts">
const emit = defineEmits<{
  submit: [value: string]
  cancel: []
}>()

const handleSubmit = () => {
  emit('submit', '提交数据')
}
</script>

父组件:

vue 复制代码
<Child @submit="handleSubmit" @cancel="handleCancel" />

6.10 defineExpose

<script setup> 中组件内部变量默认不会暴露给父组件。如果父组件要通过 ref 调用子组件方法,需要使用 defineExpose

子组件:

vue 复制代码
<script setup lang="ts">
const open = () => {
  console.log('打开弹窗')
}

defineExpose({
  open
})
</script>

父组件:

vue 复制代码
<script setup lang="ts">
import { ref } from 'vue'
import Dialog from './Dialog.vue'

const dialogRef = ref<InstanceType<typeof Dialog>>()

const openDialog = () => {
  dialogRef.value?.open()
}
</script>

<template>
  <Dialog ref="dialogRef" />
  <button @click="openDialog">打开</button>
</template>

6.11 slot 插槽

默认插槽:

vue 复制代码
<Card>
  <p>这是内容</p>
</Card>

具名插槽:

vue 复制代码
<Card>
  <template #header>
    <h3>标题</h3>
  </template>

  <template #default>
    <p>内容</p>
  </template>

  <template #footer>
    <button>确定</button>
  </template>
</Card>

子组件:

vue 复制代码
<template>
  <div class="card">
    <div class="card-header">
      <slot name="header"></slot>
    </div>

    <div class="card-body">
      <slot></slot>
    </div>

    <div class="card-footer">
      <slot name="footer"></slot>
    </div>
  </div>
</template>

6.12 provide / inject

适合跨层级传值,例如主题、语言、表单上下文。

父级:

ts 复制代码
import { provide, ref } from 'vue'

const theme = ref('dark')

provide('theme', theme)

后代组件:

ts 复制代码
import { inject } from 'vue'

const theme = inject('theme')

6.13 composables 组合式函数

封装请求逻辑:

ts 复制代码
// composables/useRequest.ts
import { ref } from 'vue'

export function useRequest<T>(apiFn: () => Promise<T>) {
  const data = ref<T | null>(null)
  const loading = ref(false)
  const error = ref<unknown>(null)

  const run = async () => {
    try {
      loading.value = true
      error.value = null
      data.value = await apiFn()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  return {
    data,
    loading,
    error,
    run
  }
}

页面使用:

ts 复制代码
const { data, loading, run } = useRequest(getUserListApi)

onMounted(() => {
  run()
})

常见 composables:

txt 复制代码
useRequest
useTable
usePagination
useForm
useDialog
usePermission
useDebounce
useThrottle
useLocalStorage
useTheme

七、Vue Router 路由详细讲解

Vue Router 是 Vue 官方路由库,用于 SPA 单页面应用中的页面切换。

7.1 安装

Vue3 使用 Vue Router 4:

bash 复制代码
npm install vue-router

Vue2 使用 Vue Router 3:

bash 复制代码
npm install vue-router@3

7.2 Vue3 路由基本配置

ts 复制代码
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    redirect: '/home'
  },
  {
    path: '/home',
    name: 'Home',
    component: () => import('@/views/home/index.vue'),
    meta: {
      title: '首页',
      requiresAuth: true
    }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/login/index.vue'),
    meta: {
      title: '登录',
      requiresAuth: false
    }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes
})

export default router

注册路由:

ts 复制代码
// main.ts
import router from './router'

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

7.3 hash 模式和 history 模式

hash 模式
ts 复制代码
import { createWebHashHistory } from 'vue-router'

const router = createRouter({
  history: createWebHashHistory(),
  routes
})

URL 示例:

txt 复制代码
https://example.com/#/home

优点:部署简单,刷新不会 404。

缺点:URL 中有 #,不够美观。

history 模式
ts 复制代码
import { createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes
})

URL 示例:

txt 复制代码
https://example.com/home

优点:URL 美观。

缺点:服务器需要配置回退到 index.html,否则刷新页面可能 404。

Nginx 配置:

nginx 复制代码
location / {
  try_files $uri $uri/ /index.html;
}

7.4 路由跳转

声明式跳转:

vue 复制代码
<RouterLink to="/home">首页</RouterLink>
<RouterLink :to="{ name: 'UserDetail', params: { id: 1 } }">用户详情</RouterLink>

编程式跳转:

ts 复制代码
import { useRouter } from 'vue-router'

const router = useRouter()

router.push('/home')
router.push({ name: 'UserDetail', params: { id: 1 } })
router.replace('/login')
router.back()
router.forward()

7.5 获取路由信息

ts 复制代码
import { useRoute } from 'vue-router'

const route = useRoute()

console.log(route.path)
console.log(route.name)
console.log(route.params)
console.log(route.query)
console.log(route.meta)

7.6 动态路由参数

路由配置:

ts 复制代码
{
  path: '/user/:id',
  name: 'UserDetail',
  component: () => import('@/views/user/detail.vue')
}

跳转:

ts 复制代码
router.push({
  name: 'UserDetail',
  params: {
    id: 1001
  }
})

获取:

ts 复制代码
const route = useRoute()
const id = route.params.id

URL:

txt 复制代码
/user/1001

7.7 query 参数

跳转:

ts 复制代码
router.push({
  path: '/search',
  query: {
    keyword: 'vue',
    page: 1
  }
})

获取:

ts 复制代码
const keyword = route.query.keyword

URL:

txt 复制代码
/search?keyword=vue&page=1

7.8 params 和 query 区别

对比 params query
URL 形式 /user/1 /user?id=1
配置要求 需要在 path 中声明 不需要
页面刷新 name + params 某些场景要注意 一般不会丢
适合场景 详情页 ID 搜索条件、分页参数

7.9 嵌套路由

ts 复制代码
{
  path: '/system',
  component: () => import('@/layouts/AdminLayout.vue'),
  children: [
    {
      path: 'user',
      name: 'SystemUser',
      component: () => import('@/views/system/user.vue'),
      meta: {
        title: '用户管理'
      }
    },
    {
      path: 'role',
      name: 'SystemRole',
      component: () => import('@/views/system/role.vue'),
      meta: {
        title: '角色管理'
      }
    }
  ]
}

布局组件:

vue 复制代码
<template>
  <div class="layout">
    <AsideMenu />
    <main>
      <RouterView />
    </main>
  </div>
</template>

7.10 404 页面

ts 复制代码
{
  path: '/:pathMatch(.*)*',
  name: 'NotFound',
  component: () => import('@/views/error/404.vue')
}

7.11 路由 meta

meta 常用于:

txt 复制代码
页面标题
是否需要登录
是否缓存页面
权限编码
菜单图标
是否隐藏菜单
面包屑

示例:

ts 复制代码
{
  path: '/user',
  name: 'User',
  component: () => import('@/views/user/index.vue'),
  meta: {
    title: '用户管理',
    requiresAuth: true,
    keepAlive: true,
    permission: 'user:list',
    icon: 'User'
  }
}

7.12 全局前置守卫

ts 复制代码
router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token')

  if (to.meta.requiresAuth && !token) {
    next({
      path: '/login',
      query: {
        redirect: to.fullPath
      }
    })
  } else {
    next()
  }
})

Vue Router 4 中也可以直接返回:

ts 复制代码
router.beforeEach((to) => {
  const token = localStorage.getItem('token')

  if (to.meta.requiresAuth && !token) {
    return {
      path: '/login',
      query: {
        redirect: to.fullPath
      }
    }
  }
})

7.13 全局后置守卫

ts 复制代码
router.afterEach((to) => {
  document.title = to.meta.title
    ? `${to.meta.title} - 后台管理系统`
    : '后台管理系统'
})

7.14 路由独享守卫

ts 复制代码
{
  path: '/admin',
  component: () => import('@/views/admin/index.vue'),
  beforeEnter: (to, from) => {
    const role = localStorage.getItem('role')
    if (role !== 'admin') {
      return '/403'
    }
  }
}

7.15 组件内守卫

Options API:

js 复制代码
export default {
  beforeRouteEnter(to, from, next) {
    next()
  },

  beforeRouteUpdate(to, from, next) {
    next()
  },

  beforeRouteLeave(to, from, next) {
    const answer = window.confirm('确定离开当前页面吗?')
    if (answer) {
      next()
    } else {
      next(false)
    }
  }
}

Composition API:

ts 复制代码
import { onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'

onBeforeRouteLeave(() => {
  const answer = window.confirm('确定离开吗?')
  if (!answer) return false
})

onBeforeRouteUpdate((to) => {
  console.log('路由参数变化:', to.params.id)
})

八、页面缓存 KeepAlive 与路由缓存

后台系统中常见需求:

txt 复制代码
1. 从列表页进入详情页
2. 返回列表页时保留搜索条件、分页页码、滚动位置
3. 切换 tab 时页面不重新请求
4. 某些页面需要缓存,某些页面不需要缓存

这就需要 KeepAlive

8.1 基本用法

vue 复制代码
<template>
  <KeepAlive>
    <UserList />
  </KeepAlive>
</template>

被缓存的组件不会被销毁,而是进入停用状态。

8.2 路由页面缓存写法

vue 复制代码
<!-- App.vue 或 Layout.vue -->
<template>
  <RouterView v-slot="{ Component, route }">
    <KeepAlive>
      <component
        :is="Component"
        v-if="route.meta.keepAlive"
        :key="route.name"
      />
    </KeepAlive>

    <component
      :is="Component"
      v-if="!route.meta.keepAlive"
      :key="route.fullPath"
    />
  </RouterView>
</template>

路由配置:

ts 复制代码
{
  path: '/user',
  name: 'UserList',
  component: () => import('@/views/user/index.vue'),
  meta: {
    title: '用户列表',
    keepAlive: true
  }
}

8.3 include 缓存指定组件

vue 复制代码
<KeepAlive :include="cacheNames">
  <component :is="Component" />
</KeepAlive>
ts 复制代码
const cacheNames = ['UserList', 'RoleList']

注意:include 匹配的是组件的 name

<script setup> 中推荐使用:

vue 复制代码
<script setup lang="ts">
defineOptions({
  name: 'UserList'
})
</script>

8.4 activated 和 deactivated

缓存组件不会频繁执行 mounted,而是执行:

ts 复制代码
import { onActivated, onDeactivated } from 'vue'

onActivated(() => {
  console.log('页面重新激活')
})

onDeactivated(() => {
  console.log('页面进入缓存')
})

典型场景:

txt 复制代码
onMounted:第一次进入页面,请求列表
onActivated:返回页面时判断是否刷新
onDeactivated:暂停轮询、保存滚动位置

8.5 列表页缓存完整示例

vue 复制代码
<script setup lang="ts">
import { ref, onMounted, onActivated, onDeactivated, nextTick } from 'vue'

defineOptions({
  name: 'UserList'
})

const keyword = ref('')
const page = ref(1)
const list = ref([])
const scrollTop = ref(0)
const tableWrapperRef = ref<HTMLElement>()

const getList = async () => {
  console.log('请求列表', keyword.value, page.value)
}

onMounted(() => {
  getList()
})

onActivated(() => {
  nextTick(() => {
    if (tableWrapperRef.value) {
      tableWrapperRef.value.scrollTop = scrollTop.value
    }
  })
})

onDeactivated(() => {
  if (tableWrapperRef.value) {
    scrollTop.value = tableWrapperRef.value.scrollTop
  }
})
</script>

<template>
  <div ref="tableWrapperRef" class="page-list">
    <input v-model="keyword" placeholder="搜索用户名" />
    <button @click="getList">搜索</button>

    <div v-for="item in list" :key="item.id">
      {{ item.name }}
    </div>
  </div>
</template>

8.6 常见缓存问题

问题一:keepAlive 不生效

检查:

txt 复制代码
1. 路由 meta.keepAlive 是否设置
2. 组件是否有 name
3. include 是否和组件 name 匹配
4. RouterView 写法是否正确
5. 组件 key 是否导致强制重建
问题二:缓存页面返回后不刷新

解决:在 onActivated 中根据条件刷新。

ts 复制代码
onActivated(() => {
  if (needRefresh.value) {
    getList()
  }
})
问题三:同一个组件不同参数页面缓存混乱

例如:

txt 复制代码
/user/detail/1
/user/detail/2

可以使用:

vue 复制代码
<component :is="Component" :key="route.fullPath" />

但注意:key 不同会创建不同实例,缓存数量可能增加。


九、登录注册、Token、路由拦截、权限控制完整流程

9.1 登录注册整体流程

txt 复制代码
用户注册
  ↓
填写用户名、密码、手机号等
  ↓
调用注册接口
  ↓
注册成功后跳转登录页
  ↓
用户登录
  ↓
调用登录接口
  ↓
后端返回 token / refreshToken / 用户信息
  ↓
前端保存 token
  ↓
请求用户信息、权限、菜单
  ↓
生成动态路由
  ↓
进入后台首页

9.2 登录页面示例

vue 复制代码
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { useUserStore } from '@/stores/user'
import { loginApi } from '@/api/auth'

const router = useRouter()
const route = useRoute()
const userStore = useUserStore()

const loading = ref(false)

const form = reactive({
  username: '',
  password: ''
})

const handleLogin = async () => {
  if (!form.username || !form.password) {
    window.alert('请输入账号和密码')
    return
  }

  try {
    loading.value = true

    const res = await loginApi(form)

    userStore.setToken(res.token)
    await userStore.getUserInfo()

    const redirect = route.query.redirect as string
    router.replace(redirect || '/')
  } finally {
    loading.value = false
  }
}
</script>

<template>
  <div class="login-page">
    <input v-model.trim="form.username" placeholder="请输入账号" />
    <input v-model.trim="form.password" type="password" placeholder="请输入密码" />
    <button :disabled="loading" @click="handleLogin">
      {{ loading ? '登录中...' : '登录' }}
    </button>
  </div>
</template>

9.3 注册页面示例

vue 复制代码
<script setup lang="ts">
import { reactive, ref } from 'vue'
import { useRouter } from 'vue-router'
import { registerApi } from '@/api/auth'

const router = useRouter()
const loading = ref(false)

const form = reactive({
  username: '',
  password: '',
  confirmPassword: '',
  phone: ''
})

const handleRegister = async () => {
  if (!form.username || !form.password) {
    window.alert('请填写完整信息')
    return
  }

  if (form.password !== form.confirmPassword) {
    window.alert('两次密码不一致')
    return
  }

  try {
    loading.value = true
    await registerApi(form)
    window.alert('注册成功,请登录')
    router.push('/login')
  } finally {
    loading.value = false
  }
}
</script>

9.4 Token 存储方式

常见方案:

方式 优点 缺点
localStorage 简单,刷新不丢 有 XSS 风险
sessionStorage 关闭标签页失效 用户体验一般
Cookie 可配合后端 HttpOnly 配置稍复杂
Pinia 内存 安全性略好 刷新会丢

常见实际项目:

txt 复制代码
token 存 localStorage / Cookie
用户信息存 Pinia
刷新页面时根据 token 重新获取用户信息

封装 token 工具:

ts 复制代码
const TOKEN_KEY = 'admin_token'

export const getToken = () => localStorage.getItem(TOKEN_KEY)

export const setToken = (token: string) => {
  localStorage.setItem(TOKEN_KEY, token)
}

export const removeToken = () => {
  localStorage.removeItem(TOKEN_KEY)
}

9.5 Pinia 用户状态

ts 复制代码
// stores/user.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { getToken, setToken, removeToken } from '@/utils/auth'
import { getUserInfoApi } from '@/api/auth'

interface UserInfo {
  id: number
  username: string
  avatar: string
  roles: string[]
  permissions: string[]
}

export const useUserStore = defineStore('user', () => {
  const token = ref(getToken() || '')
  const userInfo = ref<UserInfo | null>(null)
  const permissions = ref<string[]>([])
  const roles = ref<string[]>([])

  const setUserToken = (value: string) => {
    token.value = value
    setToken(value)
  }

  const getUserInfo = async () => {
    const res = await getUserInfoApi()
    userInfo.value = res.userInfo
    permissions.value = res.permissions || []
    roles.value = res.roles || []
  }

  const logout = () => {
    token.value = ''
    userInfo.value = null
    permissions.value = []
    roles.value = []
    removeToken()
  }

  return {
    token,
    userInfo,
    permissions,
    roles,
    setToken: setUserToken,
    getUserInfo,
    logout
  }
})

9.6 路由白名单

ts 复制代码
const whiteList = ['/login', '/register', '/404', '/403']

9.7 登录拦截完整写法

ts 复制代码
// router/permission.ts
import router from './index'
import { useUserStore } from '@/stores/user'
import { getToken } from '@/utils/auth'

const whiteList = ['/login', '/register', '/404', '/403']

router.beforeEach(async (to) => {
  const userStore = useUserStore()
  const token = getToken()

  document.title = to.meta.title
    ? `${to.meta.title} - 管理系统`
    : '管理系统'

  if (token) {
    // 已登录时访问登录页,直接跳首页
    if (to.path === '/login') {
      return '/'
    }

    // 刷新页面后 Pinia 可能为空,需要重新拉用户信息
    if (!userStore.userInfo) {
      try {
        await userStore.getUserInfo()
      } catch (error) {
        userStore.logout()
        return {
          path: '/login',
          query: {
            redirect: to.fullPath
          }
        }
      }
    }

    // 权限判断
    const permission = to.meta.permission as string | undefined
    if (permission && !userStore.permissions.includes(permission)) {
      return '/403'
    }

    return true
  }

  // 未登录,白名单放行
  if (whiteList.includes(to.path)) {
    return true
  }

  // 未登录,跳转登录页
  return {
    path: '/login',
    query: {
      redirect: to.fullPath
    }
  }
})

main.ts 中引入:

ts 复制代码
import './router/permission'

9.8 动态权限路由

后端返回菜单:

json 复制代码
[
  {
    "path": "/system/user",
    "name": "SystemUser",
    "component": "system/user/index",
    "title": "用户管理",
    "permission": "system:user:list"
  }
]

前端映射组件:

ts 复制代码
const modules = import.meta.glob('@/views/**/*.vue')

function loadView(component: string) {
  return modules[`/src/views/${component}.vue`]
}

生成路由:

ts 复制代码
function transformMenuToRoutes(menuList: any[]) {
  return menuList.map(item => ({
    path: item.path,
    name: item.name,
    component: loadView(item.component),
    meta: {
      title: item.title,
      permission: item.permission,
      keepAlive: item.keepAlive
    }
  }))
}

添加路由:

ts 复制代码
const asyncRoutes = transformMenuToRoutes(menuList)

asyncRoutes.forEach(route => {
  router.addRoute('Layout', route)
})

9.9 按钮权限

指令方式:

ts 复制代码
// directives/permission.ts
import type { App, Directive } from 'vue'
import { useUserStore } from '@/stores/user'

const permission: Directive = {
  mounted(el, binding) {
    const userStore = useUserStore()
    const value = binding.value

    if (value && !userStore.permissions.includes(value)) {
      el.parentNode?.removeChild(el)
    }
  }
}

export function setupPermissionDirective(app: App) {
  app.directive('permission', permission)
}

注册:

ts 复制代码
setupPermissionDirective(app)

使用:

vue 复制代码
<button v-permission="'user:add'">新增用户</button>
<button v-permission="'user:delete'">删除用户</button>

函数方式:

ts 复制代码
export function hasPermission(code: string) {
  const userStore = useUserStore()
  return userStore.permissions.includes(code)
}
vue 复制代码
<button v-if="hasPermission('user:add')">新增</button>

9.10 退出登录

ts 复制代码
const handleLogout = () => {
  userStore.logout()
  router.replace('/login')
}

同时建议后端提供退出接口:

ts 复制代码
await logoutApi()
userStore.logout()
router.replace('/login')

9.11 401 token 过期处理

在响应拦截器中统一处理:

ts 复制代码
if (error.response?.status === 401) {
  userStore.logout()
  router.replace({
    path: '/login',
    query: {
      redirect: router.currentRoute.value.fullPath
    }
  })
}

十、Axios 请求封装与接口管理

10.1 安装

bash 复制代码
npm install axios

10.2 request 封装

ts 复制代码
// utils/request.ts
import axios from 'axios'
import type { AxiosError, InternalAxiosRequestConfig } from 'axios'
import router from '@/router'
import { getToken, removeToken } from '@/utils/auth'

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

request.interceptors.request.use(
  (config: InternalAxiosRequestConfig) => {
    const token = getToken()

    if (token) {
      config.headers.Authorization = `Bearer ${token}`
    }

    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

request.interceptors.response.use(
  (response) => {
    const res = response.data

    if (res.code !== 200) {
      return Promise.reject(res)
    }

    return res.data
  },
  (error: AxiosError) => {
    if (error.response?.status === 401) {
      removeToken()
      router.replace('/login')
    }

    return Promise.reject(error)
  }
)

export default request

10.3 接口模块化

ts 复制代码
// api/auth.ts
import request from '@/utils/request'

export interface LoginParams {
  username: string
  password: string
}

export function loginApi(data: LoginParams) {
  return request.post('/auth/login', data)
}

export function registerApi(data: any) {
  return request.post('/auth/register', data)
}

export function getUserInfoApi() {
  return request.get('/auth/user-info')
}

export function logoutApi() {
  return request.post('/auth/logout')
}

10.4 环境变量

txt 复制代码
.env.development
.env.production
env 复制代码
VITE_API_BASE_URL=/api
env 复制代码
VITE_API_BASE_URL=https://api.example.com

使用:

ts 复制代码
import.meta.env.VITE_API_BASE_URL

十一、Pinia 与 Vuex 状态管理

11.1 什么是状态管理

状态管理用于存储多个组件都需要使用的数据:

txt 复制代码
token
用户信息
权限
菜单
主题
语言
购物车
系统配置

11.2 Vue2 常用 Vuex

Vuex 核心概念:

概念 说明
state 状态数据
getters 派生状态
mutations 同步修改 state
actions 异步逻辑
modules 模块化

11.3 Vue3 推荐 Pinia

Pinia 优势:

txt 复制代码
1. API 更简单
2. TypeScript 类型推导更好
3. 不需要 mutations
4. 支持组合式写法
5. 模块化更自然

11.4 Pinia Option Store

ts 复制代码
export const useCounterStore = defineStore('counter', {
  state: () => ({
    count: 0
  }),

  getters: {
    doubleCount: (state) => state.count * 2
  },

  actions: {
    increment() {
      this.count++
    }
  }
})

11.5 Pinia Setup Store

ts 复制代码
export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)

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

  const increment = () => {
    count.value++
  }

  return {
    count,
    doubleCount,
    increment
  }
})

11.6 Pinia 持久化

可使用 pinia-plugin-persistedstate

bash 复制代码
npm install pinia-plugin-persistedstate
ts 复制代码
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)
ts 复制代码
export const useUserStore = defineStore(
  'user',
  () => {
    const token = ref('')
    return { token }
  },
  {
    persist: true
  }
)

十二、后台管理系统项目架构

12.1 推荐目录

txt 复制代码
src
├── api
│   ├── auth.ts
│   ├── user.ts
│   └── role.ts
├── assets
├── components
│   ├── BaseTable
│   ├── BaseForm
│   └── SvgIcon
├── composables
│   ├── useTable.ts
│   ├── useDialog.ts
│   └── usePermission.ts
├── directives
│   └── permission.ts
├── layouts
│   ├── AdminLayout.vue
│   ├── components
│   │   ├── Sidebar.vue
│   │   ├── Header.vue
│   │   └── TagsView.vue
├── router
│   ├── index.ts
│   ├── routes.ts
│   └── permission.ts
├── stores
│   ├── user.ts
│   ├── app.ts
│   └── permission.ts
├── styles
│   ├── index.scss
│   ├── variables.scss
│   └── reset.scss
├── types
│   ├── user.ts
│   └── common.ts
├── utils
│   ├── auth.ts
│   ├── request.ts
│   ├── format.ts
│   └── validate.ts
├── views
│   ├── login
│   ├── dashboard
│   ├── system
│   └── error
├── App.vue
└── main.ts

12.2 后台系统核心模块

模块 内容
登录注册 登录、注册、验证码、找回密码
权限系统 菜单权限、按钮权限、接口权限
用户管理 新增、编辑、删除、查询
角色管理 分配菜单、分配权限
菜单管理 动态路由、菜单树
表格搜索 分页、排序、筛选
表单弹窗 新增编辑共用弹窗
文件上传 图片上传、Excel 导入
数据图表 ECharts、统计卡片
系统设置 主题、布局、字典配置

十三、组件封装思路

13.1 为什么要封装组件

txt 复制代码
减少重复代码
统一 UI 风格
提升维护性
提升开发效率
降低页面复杂度

13.2 表格组件封装思路

vue 复制代码
<BaseTable
  :columns="columns"
  :data="tableData"
  :loading="loading"
  :pagination="pagination"
  @page-change="handlePageChange"
/>

columns:

ts 复制代码
const columns = [
  {
    label: '用户名',
    prop: 'username'
  },
  {
    label: '邮箱',
    prop: 'email'
  },
  {
    label: '状态',
    prop: 'status'
  }
]

13.3 弹窗表单封装思路

vue 复制代码
<UserFormDialog
  v-model="dialogVisible"
  :detail="currentRow"
  @success="getList"
/>

组件内部:

ts 复制代码
const props = defineProps<{
  modelValue: boolean
  detail?: User
}>()

const emit = defineEmits<{
  'update:modelValue': [value: boolean]
  success: []
}>()

13.4 useTable 封装

ts 复制代码
import { ref } from 'vue'

export function useTable<T>(apiFn: (params: any) => Promise<any>) {
  const loading = ref(false)
  const list = ref<T[]>([])
  const page = ref(1)
  const pageSize = ref(10)
  const total = ref(0)
  const query = ref<Record<string, any>>({})

  const getList = async () => {
    try {
      loading.value = true

      const res = await apiFn({
        page: page.value,
        pageSize: pageSize.value,
        ...query.value
      })

      list.value = res.list
      total.value = res.total
    } finally {
      loading.value = false
    }
  }

  const search = () => {
    page.value = 1
    getList()
  }

  const reset = () => {
    query.value = {}
    page.value = 1
    getList()
  }

  return {
    loading,
    list,
    page,
    pageSize,
    total,
    query,
    getList,
    search,
    reset
  }
}

十四、Vue 性能优化

14.1 路由懒加载

ts 复制代码
component: () => import('@/views/user/index.vue')

14.2 组件懒加载

ts 复制代码
const Dialog = defineAsyncComponent(() => import('./Dialog.vue'))

14.3 合理使用 computed

不推荐:

vue 复制代码
<div>{{ list.filter(item => item.status === 1).length }}</div>

推荐:

ts 复制代码
const enabledCount = computed(() => {
  return list.value.filter(item => item.status === 1).length
})

14.4 v-if 和 v-show 合理选择

txt 复制代码
频繁切换:v-show
很少切换:v-if

14.5 大列表优化

txt 复制代码
分页
虚拟列表
懒加载
服务端搜索
服务端排序
减少 DOM 数量

14.6 keep-alive 缓存

适合缓存列表页、tab 页面、复杂表单页。

14.7 图片优化

txt 复制代码
压缩图片
使用 WebP / AVIF
使用 CDN
懒加载
按需加载

14.8 打包优化

ts 复制代码
// vite.config.ts
export default defineConfig({
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          echarts: ['echarts']
        }
      }
    }
  }
})

14.9 防抖和节流

搜索框防抖:

ts 复制代码
export function debounce<T extends (...args: any[]) => any>(fn: T, delay = 300) {
  let timer: number | null = null

  return (...args: Parameters<T>) => {
    if (timer) clearTimeout(timer)

    timer = window.setTimeout(() => {
      fn(...args)
    }, delay)
  }
}

滚动节流:

ts 复制代码
export function throttle<T extends (...args: any[]) => any>(fn: T, delay = 300) {
  let last = 0

  return (...args: Parameters<T>) => {
    const now = Date.now()

    if (now - last > delay) {
      last = now
      fn(...args)
    }
  }
}

十五、Vue 项目部署上线

15.1 打包

bash 复制代码
npm run build

生成:

txt 复制代码
dist

15.2 Nginx 部署

nginx 复制代码
server {
  listen 80;
  server_name example.com;

  root /www/wwwroot/vue-admin/dist;
  index index.html;

  location / {
    try_files $uri $uri/ /index.html;
  }

  location /api/ {
    proxy_pass http://127.0.0.1:8080/;
  }
}

15.3 宝塔部署步骤

txt 复制代码
1. Vue 项目本地执行 npm run build
2. 将 dist 压缩上传到宝塔网站目录
3. 解压 dist 文件
4. 网站根目录指向 dist
5. 配置 Nginx 伪静态,解决 history 刷新 404
6. 配置反向代理 /api 到后端服务
7. 配置 SSL 证书
8. 访问域名测试

伪静态:

nginx 复制代码
location / {
  try_files $uri $uri/ /index.html;
}

十六、Vue 面试题 80 道

1. Vue 是什么?

Vue 是用于构建用户界面的渐进式 JavaScript 框架,核心特点是响应式、组件化、声明式渲染和渐进式集成。

2. Vue 的核心思想是什么?

数据驱动视图和组件化开发。开发者关注数据和组件逻辑,Vue 负责把数据变化同步到视图。

3. Vue2 和 Vue3 最大区别是什么?

Vue2 主要使用 Options API,响应式基于 Object.defineProperty;Vue3 推荐 Composition API,响应式基于 Proxy,性能、Tree-shaking、TypeScript 支持更好。

4. Vue2 为什么检测不到对象新增属性?

因为 Vue2 初始化时通过 Object.defineProperty 劫持已有属性,后续新增属性没有被劫持,所以需要 Vue.setthis.$set

5. Vue3 为什么可以检测对象新增属性?

Vue3 使用 Proxy 代理整个对象,可以拦截属性读取、设置、删除等操作。

6. ref 和 reactive 区别?

ref 适合基本类型,也可以包装对象;reactive 适合对象。ref 在 JS 中访问需要 .valuereactive 不需要。

7. computed 和 watch 区别?

computed 用于根据已有数据计算新值,有缓存;watch 用于监听数据变化并执行副作用,例如请求接口、写缓存。

8. watch 和 watchEffect 区别?

watch 需要明确监听源,可以获取新旧值;watchEffect 自动收集依赖,默认立即执行,不适合精确比较旧值。

9. v-if 和 v-show 区别?

v-if 是创建和销毁 DOM,适合条件不频繁变化;v-show 是切换 CSS display,适合频繁显示隐藏。

10. v-for 为什么要加 key?

key 用于标识节点身份,帮助 Vue 在 diff 时更准确复用和移动节点,避免状态错乱。

11. 为什么不建议用 index 作为 key?

当列表发生增删改排序时,index 会变化,可能导致组件状态复用错误。

12. Vue 组件 data 为什么是函数?

组件可能复用多次,data 写成函数可以为每个组件实例返回独立数据,避免多个实例共享同一对象。

13. Vue 生命周期有哪些?

Vue2 有 createdmountedupdateddestroyed 等;Vue3 Composition API 对应 onMountedonUpdatedonUnmounted 等。

14. created 和 mounted 区别?

created 时数据已初始化但 DOM 未挂载;mounted 时 DOM 已挂载,可以操作 DOM 或初始化图表。

15. Vue3 中 setup 执行时机?

setup 在组件创建阶段执行,早于 beforeCreatecreated 的 Options API 逻辑。

16. script setup 有什么优势?

代码更简洁,不需要 return;顶层变量可直接在模板使用;配合 TypeScript 类型推导更好。

17. defineProps 和 defineEmits 需要导入吗?

不需要。它们是 <script setup> 中的编译宏,只能在 <script setup> 中使用。

18. 父子组件如何通信?

父传子使用 props,子传父使用 emit,也可以通过 v-model、slot、provide/inject、状态管理实现。

19. 兄弟组件如何通信?

可以通过共同父组件中转、EventBus、Pinia/Vuex、provide/inject 等方式。

20. provide/inject 适合什么场景?

适合跨层级传值,如主题、语言、表单上下文、布局配置等。

21. slot 是什么?

slot 是插槽,用于让父组件向子组件传入一段模板内容。

22. 具名插槽是什么?

具名插槽通过 name 区分不同插槽区域,例如 header、default、footer。

23. 作用域插槽是什么?

子组件通过 slot 向父组件暴露数据,父组件根据数据自定义渲染内容。

24. v-model 原理是什么?

本质是 props + emit。Vue3 默认使用 modelValueupdate:modelValue

25. Vue3 支持多个 v-model 吗?

支持。例如 v-model:titlev-model:visible

26. Vue Router 是什么?

Vue Router 是 Vue 官方路由库,用于 SPA 单页面应用中的页面跳转和路由管理。

27. hash 和 history 模式区别?

hash URL 中带 #,部署简单;history URL 更美观,但服务器需要配置回退到 index.html

28. history 模式刷新 404 怎么解决?

Nginx 配置 try_files $uri $uri/ /index.html;,让非静态资源请求回退到入口 HTML。

29. params 和 query 区别?

params 通常用于路径参数,如 /user/1;query 用于查询参数,如 /user?id=1

30. 路由守卫有哪些?

全局守卫、路由独享守卫、组件内守卫。常见有 beforeEachafterEachbeforeEnterbeforeRouteLeave

31. 登录拦截怎么实现?

router.beforeEach 中判断 token。没有 token 且访问非白名单页面时跳转登录页,并携带 redirect 参数。

32. 登录后如何跳回原页面?

未登录跳转登录页时带上 redirect=to.fullPath,登录成功后 router.replace(redirect || '/')

33. 刷新页面后 Pinia 数据丢失怎么办?

可以使用持久化插件,或通过 token 重新请求用户信息和权限。

34. 什么是动态路由?

根据用户权限或后端菜单动态生成并添加的路由,常用于后台管理系统。

35. router.addRoute 有什么用?

用于运行时动态添加路由,常用于权限路由和菜单路由。

36. route meta 有什么用?

用于存储路由额外信息,如标题、权限码、是否需要登录、是否缓存、菜单图标等。

37. KeepAlive 是什么?

KeepAlive 是 Vue 内置组件,用于缓存组件实例,避免组件切换时重复创建和销毁。

38. KeepAlive 的 include 根据什么匹配?

通常根据组件的 name 进行匹配。

39. activated 和 deactivated 有什么用?

缓存组件被重新显示时执行 activated,进入缓存状态时执行 deactivated

40. 列表页返回后如何保持搜索条件?

可以使用 KeepAlive 缓存页面组件,或把搜索条件存到路由 query / Pinia / sessionStorage 中。

41. Axios 请求拦截器做什么?

统一添加 token、设置请求头、处理 loading、处理请求参数。

42. Axios 响应拦截器做什么?

统一处理响应数据、错误提示、401 过期、接口异常。

43. 401 token 过期怎么处理?

清除本地 token 和用户状态,跳转登录页,必要时携带 redirect 参数。

44. Vuex 和 Pinia 区别?

Pinia API 更简单,不需要 mutations,TypeScript 支持更好,Vue3 项目更推荐 Pinia。

45. Pinia 中 state 为什么通常写成函数?

和 Vuex 类似,返回初始状态,便于 SSR 和状态隔离。

46. Pinia actions 可以写异步吗?

可以。Pinia 没有 mutations,异步请求通常直接写在 actions 中。

47. 什么是组件封装?

把重复 UI 和逻辑抽成可复用组件,例如表格、弹窗、表单、上传组件等。

48. 什么是 composables?

组合式函数,用于复用状态逻辑,例如 useTable、useRequest、useDialog。

49. Vue 中如何封装表格逻辑?

可以封装 useTable,统一管理 loading、list、page、pageSize、total、query、getList、search、reset。

50. Vue 如何做按钮权限?

可以通过权限函数 hasPermission 或自定义指令 v-permission 判断权限码后显示或移除按钮。

51. Vue 如何做菜单权限?

后端返回菜单和权限,前端根据菜单生成路由和侧边栏。

52. 前端权限安全吗?

前端权限主要控制 UI 展示,真正安全必须由后端接口权限控制。

53. Vue 如何优化首屏加载?

路由懒加载、组件懒加载、代码分包、CDN、图片压缩、减少首屏请求、开启 gzip/brotli。

54. Vue 如何优化大列表?

分页、虚拟列表、服务端搜索、服务端排序、减少 DOM 数量。

55. Vue 中 key 改变会发生什么?

key 改变会让 Vue 认为是新节点,从而销毁旧组件并创建新组件。

56. nextTick 是什么?

nextTick 用于等待 DOM 更新完成后再执行回调,常用于数据变化后操作 DOM。

57. 为什么修改数据后马上获取 DOM 可能不是最新的?

Vue DOM 更新是异步批处理的,数据变化后 DOM 不一定立即更新,需要 nextTick

58. shallowRef 和 ref 区别?

shallowRef 只追踪 .value 的变化,不深度追踪内部对象属性变化,适合大型对象或第三方实例。

59. markRaw 是什么?

markRaw 标记对象不变成响应式,适合 ECharts 实例、地图实例等第三方对象。

60. toRefs 有什么用?

把 reactive 对象的属性转换成 ref,解构后仍保持响应式。

61. reactive 解构后为什么可能失去响应式?

直接解构会脱离原代理对象,推荐使用 toRefs

62. Vue 中如何自定义指令?

Vue3 使用 app.directive 注册,指令有 mountedupdatedunmounted 等钩子。

63. Vue 中如何处理表单校验?

可以手写校验,也可以使用 Element Plus Form、VeeValidate 等方案。

64. Vue 如何处理跨域?

开发环境用 Vite proxy,生产环境用 Nginx 反向代理或后端配置 CORS。

65. Vite proxy 只在什么环境生效?

只在开发环境生效,生产环境需要 Nginx 或后端处理跨域。

66. Vue 项目如何配置路径别名?

在 Vite 中配置 resolve.alias,TypeScript 中同步配置 tsconfig.json paths。

67. Vue 如何做主题切换?

可以使用 CSS 变量、class 切换、Pinia 存主题状态、本地持久化。

68. Vue 如何做国际化?

常用 vue-i18n,通过语言包和 $t / t 函数实现多语言。

69. Vue 如何做错误边界?

Vue3 可使用 onErrorCaptured 捕获子组件错误,也可以全局配置 app.config.errorHandler

70. Vue 如何做埋点?

在路由后置守卫记录页面访问,在按钮点击事件或指令中记录用户行为。

71. Vue 如何做页面标题?

在路由 meta 中配置 title,在 router.afterEach 中设置 document.title

72. Vue 项目如何做代码规范?

使用 ESLint、Prettier、Stylelint、Husky、lint-staged、Commitlint。

73. Vue 项目如何做环境区分?

使用 .env.development.env.production,变量需要以 VITE_ 开头。

74. Vue 如何做文件上传?

使用 FormData,设置 Content-Type: multipart/form-data,结合上传组件显示进度。

75. Vue 如何实现下载文件?

通过接口获取 Blob,创建 objectURL,动态创建 a 标签触发下载。

76. Vue 如何做 Excel 导入导出?

导入使用上传文件接口;导出通常后端返回文件流,前端按 Blob 下载。

77. Vue 如何初始化 ECharts?

onMounted 中初始化,在窗口 resize 时调用 resize,在 onUnmounted 中销毁实例。

78. ECharts 放在 keep-alive 页面中要注意什么?

onActivated 中调用 resize,在 onDeactivated 中暂停不必要任务。

79. Vue2 项目如何迁移到 Vue3?

先升级依赖,处理破坏性变化,将 Vuex 逐步替换为 Pinia,将 Options API 逐步改成 Composition API,修改路由、UI 组件库和构建工具。

80. 2026 年 Vue 学习重点是什么?

重点掌握 Vue3、TypeScript、Vite、Vue Router、Pinia、组件封装、权限系统、性能优化、项目部署和工程化规范,同时能看懂 Vue2 老项目。


十七、Vue 学习路线

第一阶段:前端基础

txt 复制代码
HTML
CSS
JavaScript
ES6+
DOM
BOM
npm
模块化
TypeScript 基础

第二阶段:Vue2 基础

txt 复制代码
模板语法
data
methods
computed
watch
生命周期
组件通信
Vuex
Vue Router 3
Element UI

第三阶段:Vue3 基础

txt 复制代码
createApp
script setup
ref
reactive
computed
watch
watchEffect
生命周期
props
emits
slot
provide/inject

第四阶段:Vue3 工程化

txt 复制代码
Vite
TypeScript
Vue Router 4
Pinia
Axios
环境变量
路径别名
ESLint
Prettier
Husky

第五阶段:项目实战

txt 复制代码
登录注册
路由拦截
权限管理
动态菜单
表格搜索
表单弹窗
文件上传
图表统计
页面缓存
后台管理系统

第六阶段:高级能力

txt 复制代码
组件库封装
性能优化
自动化部署
前端监控
单元测试
源码原理
SSR
Nuxt
微前端
低代码

十八、Vue 官方与常用资源链接

分类 名称 链接
Vue3 官方 Vue 官方文档 https://vuejs.org/
Vue3 快速开始 Quick Start https://vuejs.org/guide/quick-start
Vue3 API API Reference https://vuejs.org/api/
Vue3 script setup script setup 文档 https://vuejs.org/api/sfc-script-setup
Vue3 TypeScript TypeScript with Composition API https://vuejs.org/guide/typescript/composition-api
Vue2 官方 Vue2 Guide https://v2.vuejs.org/v2/guide/
Vue2 EOL Vue2 End of Life https://v2.vuejs.org/eol/
Vue Router Vue Router 官方文档 https://router.vuejs.org/
路由守卫 Navigation Guards https://router.vuejs.org/guide/advanced/navigation-guards.html
路由 Meta Route Meta Fields https://router.vuejs.org/guide/advanced/meta.html
KeepAlive KeepAlive 官方文档 https://vuejs.org/guide/built-ins/keep-alive
Pinia Pinia 官方文档 https://pinia.vuejs.org/
Vuex Vuex 官方文档 https://vuex.vuejs.org/
Vite Vite 官方文档 https://vite.dev/
Element Plus Element Plus https://element-plus.org/
Naive UI Naive UI https://www.naiveui.com/
Ant Design Vue Ant Design Vue https://antdv.com/
Axios Axios 文档 https://axios-http.com/
Vitest Vitest 文档 https://vitest.dev/
Vue Test Utils Vue Test Utils https://test-utils.vuejs.org/
Nuxt Nuxt 官方文档 https://nuxt.com/
ECharts Apache ECharts https://echarts.apache.org/

总结

2026 年学习 Vue,不能只停留在会写 v-ifv-forrefreactive。真正项目中必须掌握:

txt 复制代码
Vue2 老项目维护能力
Vue3 新项目开发能力
Vue Router 路由和权限控制
KeepAlive 页面缓存
登录注册和 token 鉴权
Pinia / Vuex 状态管理
Axios 请求封装
后台管理系统架构
组件封装
性能优化
部署上线
面试表达能力

推荐主线:

txt 复制代码
Vue2 看得懂
Vue3 用得熟
TypeScript 写得稳
路由权限做得清
组件封装有思路
项目部署能独立完成

如果你是准备找前端工作,建议至少独立完成一个:

txt 复制代码
Vue3 + TypeScript + Vite + Pinia + Vue Router + Axios + Element Plus 后台管理系统

项目中必须包含:

txt 复制代码
登录注册
路由拦截
动态菜单
按钮权限
表格分页
表单弹窗
文件上传
页面缓存
数据图表
部署上线

掌握这些内容后,Vue 相关的项目开发和面试都会更有底气。

相关推荐
蜡台10 小时前
VUE 侧边按钮组,可自定义位置
前端·javascript·css
AI科技星10 小时前
维度原本——基于超复数谱系的全域维度统一理论
c语言·前端·javascript·网络·electron
kyriewen10 小时前
14MB VS 15KB:前React核心成员用AI写了个排版库,让Safari快了一千倍
前端·javascript·react.js
幸运小圣11 小时前
动态表格在 Vue 3 中的实现指南【前端】
前端·javascript·vue.js
SwJieJie11 小时前
Day 3|表格表单分页范式与 vue-request 最佳实践:从配置驱动到业务落地
前端·javascript·vue.js
ZengLiangYi11 小时前
任务队列设计:p-queue 限速 + 重试策略
前端·javascript·后端
sugar__salt11 小时前
从零吃透 ES6 核心:变量声明、作用域、变量提升与坑点
前端·javascript·ecmascript·es6
罗超驿11 小时前
1.HTML基础入门:标签、属性与路径详解(VSCode开发环境)
前端·vscode·html
Dante丶11 小时前
Codex Desktop 不断 Reconnecting 的代理环境变量处理
前端·后端·代码规范