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 | 较弱 | 更好 |
| 性能 | 较好 | 更好 |
| 生命周期命名 | mounted、destroyed |
onMounted、onUnmounted |
| 双向绑定 | .sync、model |
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-if 和 v-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.set 或 this.$set。
5. Vue3 为什么可以检测对象新增属性?
Vue3 使用 Proxy 代理整个对象,可以拦截属性读取、设置、删除等操作。
6. ref 和 reactive 区别?
ref 适合基本类型,也可以包装对象;reactive 适合对象。ref 在 JS 中访问需要 .value,reactive 不需要。
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 有 created、mounted、updated、destroyed 等;Vue3 Composition API 对应 onMounted、onUpdated、onUnmounted 等。
14. created 和 mounted 区别?
created 时数据已初始化但 DOM 未挂载;mounted 时 DOM 已挂载,可以操作 DOM 或初始化图表。
15. Vue3 中 setup 执行时机?
setup 在组件创建阶段执行,早于 beforeCreate 和 created 的 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 默认使用 modelValue 和 update:modelValue。
25. Vue3 支持多个 v-model 吗?
支持。例如 v-model:title、v-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. 路由守卫有哪些?
全局守卫、路由独享守卫、组件内守卫。常见有 beforeEach、afterEach、beforeEnter、beforeRouteLeave。
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 注册,指令有 mounted、updated、unmounted 等钩子。
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 官方与常用资源链接
总结
2026 年学习 Vue,不能只停留在会写 v-if、v-for、ref、reactive。真正项目中必须掌握:
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 相关的项目开发和面试都会更有底气。