2026 前端面试题 - Vue3 & Nuxt 篇
📅 最后更新: 2026年2月
🎯 涵盖 Vue 3.x 和 Nuxt 3.x/4.x 核心知识点
目录
- Vue3 基础面试题
- Vue3 响应式原理
- Composition API 面试题
- Vue Router 4 面试题
- Pinia 状态管理面试题
- Nuxt 3/4 基础面试题
- Nuxt 渲染模式与SSR
- Nuxt 服务端开发
- Vue3 性能优化
- Vue3 生态与工具
- 手写题与代码题
Vue3 基础面试题
1. Vue3 相比 Vue2 有哪些主要改进?
核心改进点:
-
响应式系统重构:使用 Proxy 替代 Object.defineProperty
- 支持 Map、Set、WeakMap、WeakSet
- 支持数组索引和 length 监听
- 新增/删除属性自动触发更新
-
Composition API:更灵活的代码组织方式
- 逻辑复用更方便(替代 mixins)
- 更好的 TypeScript 支持
<script setup>语法糖
-
性能提升
- 编译优化(静态提升、PatchFlag)
- 更小的打包体积(~10KB gzip)
- 更快的渲染速度
-
新特性
- Teleport:组件渲染到 DOM 其他位置
- Suspense:异步组件加载状态
- Fragment:多根节点组件
- 全局 API 修改(createApp)
2. ref 和 reactive 的区别是什么?
| 特性 | ref | reactive |
|---|---|---|
| 适用类型 | 任意类型 | 仅对象类型 |
| 访问方式 | .value | 直接访问 |
| 替换对象 | 可以 | 不可以(会失去响应式) |
| 解构 | 保持响应式(使用 toRefs) | 会失去响应式 |
| 使用场景 | 基本类型、需要替换的对象 | 复杂对象、固定结构数据 |
最佳实践:
- 优先使用
ref,Vue 官方推荐 reactive适合复杂表单对象或状态管理- 避免解构
reactive对象
3. computed 和 watch 的区别?
| 特性 | computed | watch | watchEffect |
|---|---|---|---|
| 用途 | 派生状态 | 副作用操作 | 自动副作用 |
| 缓存 | 有(依赖不变不重新计算) | 无 | 无 |
| 返回值 | 有 | 无 | 无 |
| 立即执行 | 否(惰性) | 可选(immediate) | 是 |
| 旧值 | 无 | 有 | 无 |
| 使用场景 | 模板中展示的计算值 | 特定数据变化时执行操作 | 初始化+数据变化时执行 |
4. Vue3 生命周期有哪些变化?
对照表:
| Options API | Composition API | 说明 |
|---|---|---|
| beforeCreate | 不需要 | setup 替代 |
| created | 不需要 | setup 替代 |
| beforeMount | onBeforeMount | - |
| mounted | onMounted | - |
| beforeUpdate | onBeforeUpdate | - |
| updated | onUpdated | - |
| beforeDestroy | onBeforeUnmount | 改名 |
| destroyed | onUnmounted | 改名 |
| activated | onActivated | keep-alive |
| deactivated | onDeactivated | keep-alive |
| errorCaptured | onErrorCaptured | 错误处理 |
5. 什么是 Teleport?
Teleport 可以将组件的模板渲染到 DOM 的其他位置,而不受组件层级限制。
使用场景:
- 模态框/对话框:避免被父组件的
overflow: hidden或z-index影响 - 通知/Toast:需要在页面顶层显示
- 全屏加载动画:覆盖整个视口
- 悬浮按钮/侧边栏:固定在页面特定位置
6. 什么是 Suspense?
Suspense 用于在等待异步组件加载时显示备用内容。
<template>
<Suspense>
<template #default>
<AsyncUserList />
</template>
<template #fallback>
<div>加载中...</div>
</template>
</Suspense>
</template>
Vue3 响应式原理
7. Vue3 响应式原理是什么?
Vue3 使用 ES6 Proxy 实现响应式系统。
核心机制:
track(): 依赖收集(getter 时)trigger(): 触发更新(setter 时)- 使用 WeakMap 存储依赖关系
相比 Vue2 的优势:
- 支持更多数据类型(Map、Set 等)
- 监听更完整(新增/删除属性)
- 数组处理更原生
- 性能更好(懒加载代理)
8. ref 为什么需要 .value?
因为 JavaScript 无法直接监听基本类型的变化,需要通过对象的 getter/setter 来拦截。
9. 响应式丢失问题及解决方案
问题1:reactive 解构丢失响应式
- 解决:使用
toRefs()
问题2:reactive 替换对象丢失响应式
- 解决:修改属性而非替换对象,或使用 ref
问题3:数组/集合中的 ref
- 解决:展开到 reactive 对象中
Composition API 面试题
10. Composition API 的优势?
- 逻辑复用:替代 mixins,避免命名冲突
- 代码组织:按功能组织代码,而非选项类型
- TypeScript 支持:更好的类型推断
- Tree Shaking:按需导入,减少打包体积
- 灵活灵活:代码可以更自由地组织
11. 自定义 Composable 的最佳实践
命名约定:
- 以
use开头 - 如:
useCounter、useMouse、useFetch
关键要点:
- 参数处理:使用
toValue处理 ref 或普通值 - 副作用清理:在
onUnmounted中清理 - 约束条件:使用
getCurrentInstance()确保在 setup 中调用
12. provide/inject 的使用
用于跨层级组件通信,避免 props 层层传递。
// 祖先组件
const user = ref({ name: 'Tom' })
provide('user', readonly(user))
// 后代组件
const user = inject('user')
Vue Router 4 面试题
13. Vue Router 4 的新特性
- 新的创建方式:
createRouter替代new VueRouter - History 模式改进:
createWebHistory()替代mode: 'history' - 组合式 API:
useRouter()、useRoute() - 导航守卫改进:next 是可选的
- 动态路由添加/删除:
addRoute()、removeRoute()
14. 路由守卫执行顺序
- 全局
beforeEach - 复用组件的
beforeRouteUpdate - 路由配置的
beforeEnter - 解析异步路由组件
- 激活组件的
beforeRouteEnter - 全局
beforeResolve - 全局
afterEach
15. 如何优雅地处理路由权限?
方案1:路由元信息 + 全局守卫
router.beforeEach((to) => {
if (to.meta.requiresAuth && !isAuthenticated) {
return { path: '/login', query: { redirect: to.fullPath }}
}
})
方案2:动态路由生成
- 根据后端返回的权限动态生成路由
Pinia 状态管理面试题
16. 为什么推荐使用 Pinia 替代 Vuex?
| 特性 | Vuex 4 | Pinia |
|---|---|---|
| Vue3 支持 | 支持 | 原生支持,更优 |
| TypeScript | 复杂 | 完美支持 |
| API | Options 风格 | Composition API 风格 |
| 体积 | 较大 | 更小(~1KB) |
| 模块化 | namespaced | 自动模块化 |
17. Pinia 的 State、Getters、Actions
Setup Store(推荐):
export const useStore = defineStore('main', () => {
// State
const count = ref(0)
// Getters
const double = computed(() => count.value * 2)
// Actions
function increment() {
count.value++
}
return { count, double, increment }
})
Nuxt 3/4 基础面试题
18. Nuxt 3/4 相比 Nuxt 2 的变化
| 特性 | Nuxt 2 | Nuxt 3/4 |
|---|---|---|
| Vue 版本 | Vue 2 | Vue 3 |
| 引擎 | 基于 Node.js | Nitro 引擎(支持 Edge) |
| TypeScript | 需要配置 | 原生支持 |
| 状态管理 | Vuex | Pinia(内置) |
| 构建工具 | Webpack | Vite(默认) |
19. Nuxt 的文件路由系统
基础路由:
pages/
├── index.vue # /
├── about.vue # /about
└── contact.vue # /contact
动态路由:
pages/
├── user/
│ └── [id].vue # /user/:id
└── [...slug].vue # /:slug(.*)* (捕获所有)
嵌套路由:
pages/
├── parent.vue # 父路由
└── parent/
├── index.vue # /parent
└── child.vue # /parent/child
20. Nuxt 的自动导入机制
自动导入 Vue、Nuxt 和已注册模块的 API:
- Vue API:
ref、computed、watch等 - Nuxt API:
useRoute()、useRouter()、useFetch()等 - Composables:
composables/目录下的函数
Nuxt 渲染模式与 SSR
21. Nuxt 支持的渲染模式
- SSR(默认):
ssr: true - SPA:
ssr: false - 静态生成(SSG):
nuxt generate - 混合渲染:
routeRules配置 - Edge 渲染:Nitro preset
22. Nuxt 数据获取方式
useFetch(推荐):
<script setup>
const { data, pending, error, refresh } = await useFetch('/api/users')
</script>
useAsyncData(更灵活):
<script setup>
const { data } = await useAsyncData('users', async () => {
return await $fetch('/api/users')
})
</script>
23. Nuxt 如何处理 SEO 和 Meta?
useHead:
<script setup>
useHead({
title: '页面标题',
meta: [
{ name: 'description', content: '页面描述' }
]
})
</script>
useSeoMeta(Nuxt 3.3+):
<script setup>
useSeoMeta({
title: '页面标题',
ogTitle: '分享标题',
description: '页面描述'
})
</script>
Nuxt 服务端开发
24. Nuxt 中创建 API 路由
基础 API 路由:
// server/api/users.get.ts
export default defineEventHandler(async (event) => {
const query = getQuery(event)
return { users: [] }
})
RESTful API:
server/api/users/
├── index.get.ts # GET /api/users
├── index.post.ts # POST /api/users
└── [id].get.ts # GET /api/users/:id
25. Nuxt 中间件类型
- 路由中间件(客户端):
middleware/目录 - 全局中间件:
middleware/*.global.ts - 服务端中间件:
server/middleware/
Vue3 性能优化
26. Vue3 性能优化手段
- 编译优化:静态提升、PatchFlag
- 组件优化:
shallowRef、markRaw、虚拟列表 - 渲染优化:
v-once、v-memo、computed 缓存 - 异步组件:代码分割、懒加载
- 减少响应式开销:
readonly、shallowReactive - 事件优化:事件委托、passive 事件、防抖/节流
27. Nuxt 项目性能优化
- 渲染策略:
routeRules配置 ISR/SWR - 图片优化:
NuxtImg组件 - 代码优化:分析构建、代码分割
- 数据获取:合理使用缓存策略
- 插件优化:
.client/.server后缀
Vue3 生态与工具
28. Vue3 生态系统重要工具
核心库:
- Vue Router 4、Pinia、VueUse、Vite
UI 框架:
- Element Plus、Ant Design Vue、Naive UI、Vuetify 3
开发工具:
- Volar、Vue DevTools、Vitest、Playwright
29. Vue3 + TypeScript 使用
Props 类型:
<script setup lang="ts">
interface Props {
title: string
count?: number
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
</script>
Emits 类型:
<script setup lang="ts">
interface Emits {
(e: 'update', value: string): void
}
const emit = defineEmits<Emits>()
</script>
手写题与代码题
30. 手写简化版 ref
function ref<T>(value: T) {
const dep = new Set<() => void>()
return {
get value() {
if (activeEffect) dep.add(activeEffect)
return value
},
set value(newValue: T) {
if (value !== newValue) {
value = newValue
dep.forEach(effect => effect())
}
}
}
}
31. 手写简化版 reactive
const targetMap = new WeakMap()
function reactive<T extends object>(target: T): T {
return new Proxy(target, {
get(obj, key) {
track(obj, key)
const value = Reflect.get(obj, key)
return isObject(value) ? reactive(value) : value
},
set(obj, key, value) {
const result = Reflect.set(obj, key, value)
trigger(obj, key)
return result
}
})
}
32. 手写 useLocalStorage Composable
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, defaultValue: T) {
const storedValue = ref<T>(JSON.parse(localStorage.getItem(key) || 'null') ?? defaultValue)
watch(storedValue, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return storedValue
}
33. 手写防抖/节流 Composable
// useDebounce.ts
export function useDebounce<T>(value: Ref<T>, delay = 300) {
const debouncedValue = ref(value.value)
let timeout: ReturnType<typeof setTimeout>
watch(value, (newValue) => {
clearTimeout(timeout)
timeout = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
})
return debouncedValue
}
34. 手写自定义指令
// vFocus.ts
export const vFocus = {
mounted(el: HTMLElement) {
el.focus()
}
}
// vClickOutside.ts
export const vClickOutside = {
mounted(el: HTMLElement, binding) {
el._clickOutside = (event: Event) => {
if (!(el === event.target || el.contains(event.target as Node))) {
binding.value(event)
}
}
document.addEventListener('click', el._clickOutside)
},
unmounted(el: HTMLElement) {
document.removeEventListener('click', el._clickOutside)
}
}
Vue3 进阶面试题
35. Vue3 编译优化原理是什么?
答:
Vue3 在编译阶段进行了多项优化,提升运行时性能:
1. 静态提升(Static Hoisting):
- 将静态节点提升到渲染函数外,只创建一次
- 更新时直接复用,不参与 Diff
<template>
<div>
<h1>静态标题</h1> <!-- 编译为 _hoisted_1 -->
<p>{{ dynamic }}</p> <!-- 动态内容 -->
</div>
</template>
2. PatchFlag(补丁标记):
- 标记动态节点类型,精确更新
- 避免全量 Diff
// 编译结果
export function render(_ctx, _cache) {
return (_openBlock(), _createElementBlock("div", null, [
_hoisted_1, // 静态节点,直接复用
_createElementVNode("p", null, _toDisplayString(_ctx.dynamic), 1 /* TEXT */)
// 1 表示只有文本是动态的
]))
}
3. 缓存事件处理函数:
<template>
<button @click="onClick">Click</button>
</template>
编译为:
_createElementVNode("button", {
onClick: _cache[0] || (_cache[0] = (...args) => _ctx.onClick(...args))
}, "Click")
4. Block Tree(块树):
- 标记动态子节点集合
- 更新时只遍历动态节点
36. 什么是 Effect Scope?使用场景?
答:
Effect Scope 用于批量收集和销毁副作用。
问题场景:
<script setup>
import { ref, watch, onUnmounted } from 'vue'
const count = ref(0)
const name = ref('Vue')
// 多个副作用需要手动清理
const stop1 = watch(count, () => console.log('count changed'))
const stop2 = watch(name, () => console.log('name changed'))
onUnmounted(() => {
stop1()
stop2()
})
</script>
使用 Effect Scope:
<script setup>
import { ref, watch, effectScope } from 'vue'
const scope = effectScope()
scope.run(() => {
const count = ref(0)
const name = ref('Vue')
watch(count, () => console.log('count changed'))
watch(name, () => console.log('name changed'))
})
// 组件卸载时一次性清理
onUnmounted(() => {
scope.stop()
})
</script>
高级 Composable 中的使用:
export function useFeature() {
const scope = effectScope()
const state = scope.run(() => {
const count = ref(0)
const double = computed(() => count.value * 2)
watch(count, (newVal) => {
console.log('Count:', newVal)
})
return { count, double }
})
// 提供清理方法
const dispose = () => scope.stop()
return { ...state, dispose }
}
37. Vue3 的渲染机制详解
答:
渲染流程:
-
编译阶段(Compile)
- 模板 → AST(抽象语法树)
- AST 优化(静态提升、标记动态节点)
- AST → 渲染函数(render function)
-
运行时阶段(Runtime)
- 执行渲染函数 → Virtual DOM
- Virtual DOM Diff → 更新 DOM
VNode(虚拟节点)结构:
interface VNode {
__v_isVNode: true
type: string | Component | typeof Text | typeof Fragment
props: Record<string, any>
children: string | VNode[] | null
key: string | number | symbol | null
el: Element | null // 真实 DOM 引用
component: ComponentInternalInstance | null
// ... 其他属性
}
Diff 算法(双端比较):
// 简化版 Diff 逻辑
function patchChildren(n1, n2, container) {
const c1 = n1.children
const c2 = n2.children
// 1. 旧子节点是数组,新子节点是文本
if (typeof c2 === 'string') {
if (Array.isArray(c1)) {
unmountChildren(c1)
}
if (c1 !== c2) {
container.textContent = c2
}
} else if (Array.isArray(c2)) {
// 2. 新旧都是数组 - 核心 Diff
if (Array.isArray(c1)) {
patchKeyedChildren(c1, c2, container)
} else {
container.textContent = ''
mountChildren(c2, container)
}
}
}
// 带 key 的 Diff
function patchKeyedChildren(c1, c2, container) {
let i = 0
let e1 = c1.length - 1
let e2 = c2.length - 1
// 1. 从前向后对比
while (i <= e1 && i <= e2) {
if (sameVNode(c1[i], c2[i])) {
patch(c1[i], c2[i], container)
i++
} else {
break
}
}
// 2. 从后向前对比
while (i <= e1 && i <= e2) {
if (sameVNode(c1[e1], c2[e2])) {
patch(c1[e1], c2[e2], container)
e1--
e2--
} else {
break
}
}
// 3. 处理新增或删除
if (i > e1) {
// 新增
while (i <= e2) {
patch(null, c2[i], container)
i++
}
} else if (i > e2) {
// 删除
while (i <= e1) {
unmount(c1[i])
i++
}
} else {
// 4. 处理中间乱序部分(最长递增子序列优化)
// ...
}
}
38. 如何实现自定义渲染器?
答:
Vue3 通过 @vue/runtime-core 提供创建自定义渲染器的能力。
使用场景:
- 渲染到 Canvas、WebGL
- 渲染到移动端原生组件
- 渲染到终端命令行
实现示例(Canvas 渲染器):
import { createRenderer } from '@vue/runtime-core'
// 自定义渲染器选项
const rendererOptions = {
// 创建元素
createElement(type) {
return { type, children: [] }
},
// 设置文本
setText(node, text) {
node.text = text
drawToCanvas(node) // 自定义绘制逻辑
},
// 设置元素属性
patchProp(el, key, prevValue, nextValue) {
el.props = el.props || {}
el.props[key] = nextValue
drawToCanvas(el)
},
// 插入元素
insert(child, parent, anchor) {
child.parent = parent
if (anchor) {
const index = parent.children.indexOf(anchor)
parent.children.splice(index, 0, child)
} else {
parent.children.push(child)
}
drawToCanvas(parent)
},
// 移除元素
remove(child) {
const parent = child.parent
const index = parent.children.indexOf(child)
parent.children.splice(index, 1)
drawToCanvas(parent)
},
// 创建文本节点
createText(text) {
return { type: 'text', text }
},
// 设置元素文本内容
setElementText(el, text) {
el.text = text
drawToCanvas(el)
},
// 获取父节点
parentNode(node) {
return node.parent
},
// 获取下一个兄弟节点
nextSibling(node) {
const parent = node.parent
const index = parent.children.indexOf(node)
return parent.children[index + 1]
}
}
// 创建渲染器
const renderer = createRenderer(rendererOptions)
// 使用
import { h, ref } from 'vue'
const App = {
setup() {
const count = ref(0)
return { count }
},
render() {
return h('rect', {
x: 10,
y: 10,
width: 100,
height: this.count * 10
}, this.count)
}
}
// 挂载到 canvas
const canvas = document.getElementById('canvas')
renderer.createApp(App).mount(canvas)
Vue3 高阶面试题
39. 如何设计一个大型 Vue3 项目架构?
答:
项目结构:
project/
├── apps/ # 应用目录(Monorepo)
│ ├── web/ # Web 应用
│ ├── admin/ # 管理后台
│ └── mobile/ # 移动端
│
├── packages/ # 共享包
│ ├── ui/ # 组件库
│ ├── utils/ # 工具函数
│ ├── api/ # API 接口
│ └── types/ # 类型定义
│
├── shared/ # 共享配置
│ ├── eslint-config/
│ ├── tsconfig/
│ └── tailwind-config/
│
└── turbo.json # Turborepo 配置
核心设计原则:
1. 分层架构:
Presentation Layer (UI 组件)
↓
Business Logic Layer (Composables/Store)
↓
Data Access Layer (API/Repository)
↓
External Services (后端 API)
2. 模块化设计:
// features/user/index.ts
export * from './api'
export * from './components'
export * from './composables'
export * from './stores'
// 使用
import { UserCard, useUserStore } from '@/features/user'
3. API 层封装:
// api/request.ts
import axios from 'axios'
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE
})
// 请求拦截器
request.interceptors.request.use((config) => {
const token = useAuthStore().token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
useAuthStore().logout()
}
return Promise.reject(error)
}
)
export default request
4. 状态管理分层:
// stores/auth.ts - 全局状态
export const useAuthStore = defineStore('auth', () => {
const token = ref('')
const user = ref(null)
const login = async (credentials) => {
const data = await authApi.login(credentials)
token.value = data.token
user.value = data.user
}
return { token, user, login }
})
// features/cart/stores/cart.ts - 特性状态
export const useCartStore = defineStore('cart', () => {
const items = ref([])
const totalPrice = computed(() =>
items.value.reduce((sum, item) => sum + item.price * item.quantity, 0)
)
const addItem = (product) => {
items.value.push(product)
}
return { items, totalPrice, addItem }
})
40. 如何实现 Vue3 组件库?
答:
项目结构:
ui-lib/
├── packages/
│ ├── components/ # 组件
│ │ ├── Button/
│ │ ├── Input/
│ │ └── Modal/
│ ├── theme/ # 主题
│ └── utils/ # 工具
├── docs/ # 文档
├── playground/ # 测试环境
└── package.json
Button 组件实现:
<!-- packages/components/Button/src/Button.vue -->
<template>
<button
:class="buttonClasses"
:disabled="disabled || loading"
@click="handleClick"
>
<LoadingIcon v-if="loading" />
<span v-if="$slots.default">
<slot />
</span>
</button>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { buttonProps, buttonEmits } from './button'
import type { ButtonProps } from './button'
defineOptions({
name: 'ElButton'
})
const props = defineProps(buttonProps)
const emit = defineEmits(buttonEmits)
const buttonClasses = computed(() => [
'el-button',
`el-button--${props.type}`,
`el-button--${props.size}`,
{
'is-plain': props.plain,
'is-round': props.round,
'is-circle': props.circle,
'is-disabled': props.disabled,
'is-loading': props.loading
}
])
const handleClick = (evt: MouseEvent) => {
emit('click', evt)
}
</script>
<!-- packages/components/Button/src/button.ts -->
import type { ExtractPropTypes } from 'vue'
export const buttonTypes = ['default', 'primary', 'success', 'warning', 'danger', 'info'] as const
export const buttonSizes = ['large', 'default', 'small'] as const
export const buttonProps = {
type: {
type: String,
values: buttonTypes,
default: 'default'
},
size: {
type: String,
values: buttonSizes,
default: 'default'
},
plain: Boolean,
round: Boolean,
circle: Boolean,
loading: Boolean,
disabled: Boolean,
icon: String,
nativeType: {
type: String,
default: 'button'
}
} as const
export const buttonEmits = {
click: (evt: MouseEvent) => evt instanceof MouseEvent
}
export type ButtonProps = ExtractPropTypes<typeof buttonProps>
构建配置:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import dts from 'vite-plugin-dts'
export default defineConfig({
plugins: [
vue(),
dts({
include: 'packages/**/*'
})
],
build: {
lib: {
entry: 'packages/index.ts',
name: 'MyUI',
formats: ['es', 'umd'],
fileName: (format) => `my-ui.${format}.js`
},
rollupOptions: {
external: ['vue'],
output: {
globals: {
vue: 'Vue'
}
}
}
}
})
主题系统:
// packages/theme/button.scss
.el-button {
display: inline-flex;
align-items: center;
justify-content: center;
padding: 8px 16px;
font-size: 14px;
border-radius: 4px;
border: 1px solid transparent;
cursor: pointer;
transition: all 0.3s;
&--primary {
background-color: var(--el-color-primary);
color: white;
&:hover {
background-color: var(--el-color-primary-light-3);
}
&.is-plain {
background-color: transparent;
border-color: var(--el-color-primary);
color: var(--el-color-primary);
}
}
&.is-disabled {
opacity: 0.5;
cursor: not-allowed;
}
}
41. 如何实现 Vue3 的微前端架构?
答:
方案1:基于 qiankun:
// main.ts (基座应用)
import { createApp } from 'vue'
import { registerMicroApps, start } from 'qiankun'
import App from './App.vue'
const app = createApp(App)
app.mount('#app')
// 注册微应用
registerMicroApps([
{
name: 'vue3-app1',
entry: '//localhost:8081',
container: '#micro-app-container',
activeRule: '/app1'
},
{
name: 'vue3-app2',
entry: '//localhost:8082',
container: '#micro-app-container',
activeRule: '/app2'
}
])
start()
微应用配置:
// micro-app/src/main.ts
import { createApp } from 'vue'
import App from './App.vue'
import routes from './routes'
import { createRouter, createWebHistory } from 'vue-router'
let instance: any = null
let router: any = null
function render(props: any = {}) {
const { container } = props
router = createRouter({
history: createWebHistory(window.__POWERED_BY_QIANKUN__ ? '/app1' : '/'),
routes
})
instance = createApp(App)
instance.use(router)
instance.mount(container ? container.querySelector('#app') : '#app')
}
// 独立运行
if (!window.__POWERED_BY_QIANKUN__) {
render()
}
// qiankun 生命周期
export async function bootstrap() {
console.log('vue3 app bootstraped')
}
export async function mount(props: any) {
console.log('vue3 app mount', props)
render(props)
}
export async function unmount() {
console.log('vue3 app unmount')
instance.unmount()
instance._container.innerHTML = ''
instance = null
router = null
}
方案2:基于 Module Federation(Webpack 5):
// webpack.config.js (远程应用)
const { ModuleFederationPlugin } = require('webpack').container
module.exports = {
plugins: [
new ModuleFederationPlugin({
name: 'remote_app',
filename: 'remoteEntry.js',
exposes: {
'./Button': './src/components/Button.vue',
'./utils': './src/utils/index.ts'
},
shared: {
vue: {
singleton: true,
requiredVersion: '^3.0.0'
}
}
})
]
}
<!-- 宿主应用使用远程组件 -->
<template>
<div>
<RemoteButton :text="'Click me'" />
</div>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const RemoteButton = defineAsyncComponent(() =>
import('remote_app/Button')
)
</script>
状态共享方案:
// shared/store.ts
import { reactive } from 'vue'
// 使用 RxJS 或 EventEmitter 实现跨应用通信
import { Subject } from 'rxjs'
const globalState = reactive({
user: null,
permissions: []
})
const eventBus = new Subject()
export function setGlobalState(state: any) {
Object.assign(globalState, state)
eventBus.next({ type: 'stateChange', payload: state })
}
export function getGlobalState() {
return globalState
}
export function onStateChange(callback: Function) {
return eventBus.subscribe((event: any) => {
if (event.type === 'stateChange') {
callback(event.payload)
}
})
}
42. Vue3 SSR 水合(Hydration)失败如何处理?
答:
水合失败原因:
- 服务端和客户端渲染结果不一致
- 客户端数据获取时机不对
- 使用了浏览器特有 API
解决方案:
1. 确保数据一致性:
<script setup>
// 正确:服务端和客户端使用相同数据
const { data } = await useFetch('/api/user') // 服务端获取,客户端复用
// 错误:仅客户端获取
const data = ref(null)
onMounted(async () => {
data.value = await fetch('/api/user') // 导致水合失败
})
</script>
2. 客户端特有逻辑处理:
<script setup>
const isClient = ref(false)
onMounted(() => {
isClient.value = true
// 客户端特有逻辑
localStorage.setItem('visited', 'true')
})
</script>
<template>
<div>
<ServerData /> <!-- 服务端渲染 -->
<ClientOnlyData v-if="isClient" /> <!-- 仅客户端渲染 -->
</div>
</template>
3. 使用 ClientOnly 组件:
<template>
<ClientOnly>
<Chart /> <!-- 仅在客户端渲染 -->
<template #fallback>
<ChartPlaceholder /> <!-- 服务端占位 -->
</template>
</ClientOnly>
</template>
4. 调试水合问题:
// nuxt.config.ts
export default defineNuxtConfig({
vue: {
config: {
performance: true,
devtools: true
}
},
// 开启详细的水合错误日志
debug: true
})
43. Vue3 性能监控方案?
答:
1. 内置性能指标:
// composables/usePerformance.ts
import { onMounted, onUnmounted } from 'vue'
export function usePerformanceMonitor() {
onMounted(() => {
// 使用 Performance API
const observer = new PerformanceObserver((list) => {
for (const entry of list.getEntries()) {
console.log('Performance entry:', entry)
// 发送到监控系统
sendToAnalytics({
name: entry.name,
duration: entry.duration,
entryType: entry.entryType
})
}
})
observer.observe({ entryTypes: ['measure', 'navigation', 'paint'] })
// FCP
new PerformanceObserver((list) => {
const entries = list.getEntries()
const fcp = entries.find(e => e.name === 'first-contentful-paint')
if (fcp) {
console.log('FCP:', fcp.startTime)
}
}).observe({ entryTypes: ['paint'] })
// LCP
new PerformanceObserver((list) => {
const entries = list.getEntries()
const lastEntry = entries[entries.length - 1]
console.log('LCP:', lastEntry.startTime)
}).observe({ entryTypes: ['largest-contentful-paint'] })
return () => observer.disconnect()
})
}
// 组件渲染性能
export function useComponentPerformance(componentName: string) {
const startTime = performance.now()
onMounted(() => {
const endTime = performance.now()
console.log(`${componentName} mounted in ${endTime - startTime}ms`)
})
}
2. Vue DevTools 集成:
// plugins/performance.client.ts
export default defineNuxtPlugin(() => {
if (process.dev) return
// 生产环境性能监控
window.addEventListener('load', () => {
setTimeout(() => {
const perfData = performance.getEntriesByType('navigation')[0] as PerformanceNavigationTiming
const metrics = {
dns: perfData.domainLookupEnd - perfData.domainLookupStart,
tcp: perfData.connectEnd - perfData.connectStart,
ttfb: perfData.responseStart - perfData.requestStart,
domInteractive: perfData.domInteractive,
domComplete: perfData.domComplete,
loadComplete: perfData.loadEventEnd - perfData.loadEventStart
}
// 发送到监控平台
fetch('/api/metrics', {
method: 'POST',
body: JSON.stringify(metrics)
})
}, 0)
})
})
3. 错误监控:
// plugins/error-handler.ts
export default defineNuxtPlugin((nuxtApp) => {
// Vue 错误处理
nuxtApp.vueApp.config.errorHandler = (err, instance, info) => {
console.error('Vue Error:', err)
console.error('Component:', instance)
console.error('Info:', info)
// 上报错误
reportError({
type: 'vue',
error: err instanceof Error ? err.message : String(err),
stack: err instanceof Error ? err.stack : undefined,
component: instance?.$options?.name,
info
})
}
// 全局错误处理
window.onerror = (msg, url, line, col, error) => {
reportError({
type: 'global',
message: msg,
url,
line,
col,
stack: error?.stack
})
}
// Promise 错误
window.addEventListener('unhandledrejection', (event) => {
reportError({
type: 'promise',
reason: event.reason
})
})
})
function reportError(data: any) {
// 发送到错误监控平台(Sentry、Fundebug 等)
fetch('/api/errors', {
method: 'POST',
body: JSON.stringify({
...data,
timestamp: Date.now(),
userAgent: navigator.userAgent,
url: window.location.href
})
})
}
44. Vue3 + Vite 工程化配置最佳实践?
答:
完整配置:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
import { visualizer } from 'rollup-plugin-visualizer'
import compression from 'vite-plugin-compression'
export default defineConfig(({ mode }) => ({
// 路径别名
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
'@components': resolve(__dirname, 'src/components'),
'@composables': resolve(__dirname, 'src/composables'),
'@stores': resolve(__dirname, 'src/stores'),
'@utils': resolve(__dirname, 'src/utils')
}
},
// 插件
plugins: [
vue(),
// 自动导入
AutoImport({
imports: ['vue', 'vue-router', 'pinia', '@vueuse/core'],
dts: 'src/auto-imports.d.ts',
resolvers: [ElementPlusResolver()]
}),
// 组件自动导入
Components({
dirs: ['src/components'],
dts: 'src/components.d.ts',
resolvers: [ElementPlusResolver()]
}),
// Gzip 压缩
compression({
algorithm: 'gzip',
ext: '.gz'
}),
// 分析包体积(生产环境)
mode === 'production' && visualizer({
open: true,
gzipSize: true
})
],
// 开发服务器
server: {
port: 3000,
open: true,
cors: true,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
},
// 构建配置
build: {
target: 'esnext',
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
},
rollupOptions: {
output: {
// 代码分割
manualChunks: {
'vendor': ['vue', 'vue-router', 'pinia'],
'ui': ['element-plus'],
'utils': ['lodash-es', 'dayjs']
},
// 静态资源处理
chunkFileNames: 'js/[name]-[hash].js',
entryFileNames: 'js/[name]-[hash].js',
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
const ext = info[info.length - 1]
if (/\.(png|jpe?g|gif|svg|webp|ico)$/i.test(assetInfo.name)) {
return 'images/[name]-[hash][extname]'
}
if (/\.(woff2?|eot|ttf|otf)$/i.test(assetInfo.name)) {
return 'fonts/[name]-[hash][extname]'
}
return 'assets/[name]-[hash][extname]'
}
}
},
// 图片资源限制
assetsInlineLimit: 4096
},
// CSS 配置
css: {
preprocessorOptions: {
scss: {
additionalData: `@use "@/styles/vars.scss" as *;`
}
}
},
// 优化依赖预构建
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia', 'element-plus'],
exclude: ['vue-demi']
}
}))
ESLint + Prettier 配置:
// .eslintrc.cjs
module.exports = {
root: true,
env: {
browser: true,
es2021: true,
node: true
},
extends: [
'eslint:recommended',
'plugin:vue/vue3-recommended',
'@vue/eslint-config-typescript/recommended',
'@vue/eslint-config-prettier'
],
parserOptions: {
ecmaVersion: 'latest',
parser: '@typescript-eslint/parser',
sourceType: 'module'
},
plugins: ['vue', '@typescript-eslint'],
rules: {
'vue/multi-word-component-names': 'off',
'@typescript-eslint/no-explicit-any': 'warn',
'no-console': process.env.NODE_ENV === 'production' ? 'warn' : 'off',
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off'
}
}
面试场景实战
场景1:如何排查 Vue3 应用内存泄漏?
面试官: 用户反馈应用运行一段时间后变得卡顿,怀疑是内存泄漏,你会如何排查?
答:
排查步骤:
-
Chrome DevTools Memory 面板分析:
- 拍摄堆快照(Heap Snapshot)
- 对比多次快照,查看增长对象
- 查看 Detached DOM Tree
-
常见泄漏场景:
<script setup>
// ❌ 错误:全局事件未清理
onMounted(() => {
window.addEventListener('resize', handleResize) // 忘记移除
})
// ✅ 正确:清理事件监听
onMounted(() => {
window.addEventListener('resize', handleResize)
})
onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
// ❌ 错误:定时器未清理
const timer = setInterval(() => {
console.log('tick')
}, 1000)
// ✅ 正确:清理定时器
onUnmounted(() => {
clearInterval(timer)
})
// ❌ 错误:全局状态引用未清理
const globalStore = useGlobalStore()
const hugeData = ref([])
watch(() => globalStore.data, (newData) => {
hugeData.value = [...hugeData.value, ...newData] // 无限增长
})
</script>
- 使用 VueUse 的 useEventListener:
<script setup>
import { useEventListener, useIntervalFn } from '@vueuse/core'
// 自动清理
useEventListener(window, 'resize', handleResize)
// 自动清理的定时器
const { pause, resume } = useIntervalFn(() => {
console.log('tick')
}, 1000)
</script>
场景2:实现一个虚拟滚动列表组件
面试官: 列表有 10 万条数据,如何实现流畅滚动?
答:
<template>
<div
ref="containerRef"
class="virtual-list"
@scroll="handleScroll"
>
<!-- 撑开容器高度 -->
<div
class="virtual-list__phantom"
:style="{ height: `${totalHeight}px` }"
/>
<!-- 可见区域 -->
<div
class="virtual-list__content"
:style="{ transform: `translateY(${offsetY}px)` }"
>
<div
v-for="item in visibleData"
:key="item.id"
class="virtual-list__item"
:style="{ height: `${itemHeight}px` }"
>
{{ item.name }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'
interface Props {
data: any[]
itemHeight: number
bufferSize?: number // 缓冲区大小
}
const props = withDefaults(defineProps<Props>(), {
bufferSize: 5
})
const containerRef = ref<HTMLElement>()
const scrollTop = ref(0)
const containerHeight = ref(0)
// 总高度
const totalHeight = computed(() =>
props.data.length * props.itemHeight
)
// 可见数量
const visibleCount = computed(() =>
Math.ceil(containerHeight.value / props.itemHeight) + props.bufferSize * 2
)
// 起始索引
const startIndex = computed(() => {
const start = Math.floor(scrollTop.value / props.itemHeight)
return Math.max(0, start - props.bufferSize)
})
// 结束索引
const endIndex = computed(() =>
Math.min(props.data.length, startIndex.value + visibleCount.value)
)
// 可见数据
const visibleData = computed(() =>
props.data.slice(startIndex.value, endIndex.value)
)
// Y轴偏移量
const offsetY = computed(() =>
startIndex.value * props.itemHeight
)
// 滚动处理
const handleScroll = () => {
if (containerRef.value) {
scrollTop.value = containerRef.value.scrollTop
}
}
onMounted(() => {
if (containerRef.value) {
containerHeight.value = containerRef.value.clientHeight
}
})
</script>
<style scoped>
.virtual-list {
height: 400px;
overflow-y: auto;
position: relative;
}
.virtual-list__phantom {
position: absolute;
left: 0;
top: 0;
right: 0;
}
.virtual-list__content {
position: absolute;
left: 0;
right: 0;
top: 0;
}
.virtual-list__item {
display: flex;
align-items: center;
padding: 0 16px;
border-bottom: 1px solid #eee;
}
</style>
场景3:如何设计权限管理系统?
面试官: 需要实现一个 RBAC 权限管理系统,包含菜单权限、按钮权限、数据权限,如何设计?
答:
// types/permission.ts
export interface Permission {
id: string
name: string
code: string // 唯一标识
type: 'menu' | 'button' | 'api' | 'data'
parentId?: string
path?: string // 菜单路径
children?: Permission[]
}
export interface Role {
id: string
name: string
permissions: string[] // permission codes
}
export interface User {
id: string
name: string
roles: string[]
permissions: string[] // 直接权限
}
权限 Store:
// stores/permission.ts
export const usePermissionStore = defineStore('permission', () => {
const userStore = useUserStore()
// 所有权限点
const permissions = ref<Permission[]>([])
// 当前用户权限码集合
const permissionCodes = computed(() => {
const codes = new Set<string>(userStore.user?.permissions || [])
// 从角色获取权限
userStore.user?.roles.forEach(roleId => {
const role = userStore.roles.find(r => r.id === roleId)
role?.permissions.forEach(code => codes.add(code))
})
return codes
})
// 菜单权限
const menuPermissions = computed(() => {
return permissions.value.filter(p =>
p.type === 'menu' && permissionCodes.value.has(p.code)
)
})
// 判断是否有权限
const hasPermission = (code: string | string[]) => {
if (Array.isArray(code)) {
return code.some(c => permissionCodes.value.has(c))
}
return permissionCodes.value.has(code)
}
// 判断是否有所有权限
const hasEveryPermission = (codes: string[]) => {
return codes.every(c => permissionCodes.value.has(c))
}
return {
permissions,
permissionCodes,
menuPermissions,
hasPermission,
hasEveryPermission
}
})
权限指令:
// directives/permission.ts
import type { Directive } from 'vue'
export const vPermission: Directive = {
mounted(el, binding) {
const { value } = binding
const permissionStore = usePermissionStore()
const hasPermission = permissionStore.hasPermission(value)
if (!hasPermission) {
el.parentNode?.removeChild(el)
}
}
}
// 使用
// <button v-permission="'user:create'">创建用户</button>
// <button v-permission="['user:edit', 'user:admin']">编辑</button>
动态路由生成:
// router/guard.ts
router.beforeEach(async (to) => {
const permissionStore = usePermissionStore()
// 如果还没有加载权限,先加载
if (permissionStore.permissions.length === 0) {
await permissionStore.fetchPermissions()
// 动态添加路由
const asyncRoutes = generateRoutes(permissionStore.menuPermissions)
asyncRoutes.forEach(route => router.addRoute(route))
// 重新导航
return to.fullPath
}
// 检查路由权限
if (to.meta.permission && !permissionStore.hasPermission(to.meta.permission)) {
return '/403'
}
})
面试技巧与建议
准备建议
- 理解原理:不只是会用,要理解 Vue3 的响应式原理、编译优化等
- 对比差异:Vue2 vs Vue3、Vuex vs Pinia、Options API vs Composition API
- 项目经验:准备 2-3 个实际案例,体现解决问题的能力
- 代码规范:展示良好的 TypeScript 使用和代码组织能力
- 性能意识:了解常见的性能优化手段
常见问题追问
- "你如何组织大型 Vue 项目的代码结构?"
- "遇到过哪些 Vue 的坑,如何解决的?"
- "如何做组件库的设计?"
- "服务端渲染的优缺点是什么?"
- "如何处理复杂的状态管理场景?"
参考资源
祝你面试顺利!