2026 前端面试题 - Vue3 & Nuxt 篇

6 阅读11分钟

2026 前端面试题 - Vue3 & Nuxt 篇

📅 最后更新: 2026年2月
🎯 涵盖 Vue 3.x 和 Nuxt 3.x/4.x 核心知识点


目录

  1. Vue3 基础面试题
  2. Vue3 响应式原理
  3. Composition API 面试题
  4. Vue Router 4 面试题
  5. Pinia 状态管理面试题
  6. Nuxt 3/4 基础面试题
  7. Nuxt 渲染模式与SSR
  8. Nuxt 服务端开发
  9. Vue3 性能优化
  10. Vue3 生态与工具
  11. 手写题与代码题

Vue3 基础面试题

1. Vue3 相比 Vue2 有哪些主要改进?

核心改进点:

  1. 响应式系统重构:使用 Proxy 替代 Object.defineProperty

    • 支持 Map、Set、WeakMap、WeakSet
    • 支持数组索引和 length 监听
    • 新增/删除属性自动触发更新
  2. Composition API:更灵活的代码组织方式

    • 逻辑复用更方便(替代 mixins)
    • 更好的 TypeScript 支持
    • <script setup> 语法糖
  3. 性能提升

    • 编译优化(静态提升、PatchFlag)
    • 更小的打包体积(~10KB gzip)
    • 更快的渲染速度
  4. 新特性

    • Teleport:组件渲染到 DOM 其他位置
    • Suspense:异步组件加载状态
    • Fragment:多根节点组件
    • 全局 API 修改(createApp)

2. ref 和 reactive 的区别是什么?

特性refreactive
适用类型任意类型仅对象类型
访问方式.value直接访问
替换对象可以不可以(会失去响应式)
解构保持响应式(使用 toRefs)会失去响应式
使用场景基本类型、需要替换的对象复杂对象、固定结构数据

最佳实践:

  • 优先使用 ref,Vue 官方推荐
  • reactive 适合复杂表单对象或状态管理
  • 避免解构 reactive 对象

3. computed 和 watch 的区别?

特性computedwatchwatchEffect
用途派生状态副作用操作自动副作用
缓存有(依赖不变不重新计算)
返回值
立即执行否(惰性)可选(immediate)
旧值
使用场景模板中展示的计算值特定数据变化时执行操作初始化+数据变化时执行

4. Vue3 生命周期有哪些变化?

对照表:

Options APIComposition API说明
beforeCreate不需要setup 替代
created不需要setup 替代
beforeMountonBeforeMount-
mountedonMounted-
beforeUpdateonBeforeUpdate-
updatedonUpdated-
beforeDestroyonBeforeUnmount改名
destroyedonUnmounted改名
activatedonActivatedkeep-alive
deactivatedonDeactivatedkeep-alive
errorCapturedonErrorCaptured错误处理

5. 什么是 Teleport?

Teleport 可以将组件的模板渲染到 DOM 的其他位置,而不受组件层级限制。

使用场景:

  1. 模态框/对话框:避免被父组件的 overflow: hiddenz-index 影响
  2. 通知/Toast:需要在页面顶层显示
  3. 全屏加载动画:覆盖整个视口
  4. 悬浮按钮/侧边栏:固定在页面特定位置

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 的优势:

  1. 支持更多数据类型(Map、Set 等)
  2. 监听更完整(新增/删除属性)
  3. 数组处理更原生
  4. 性能更好(懒加载代理)

8. ref 为什么需要 .value?

因为 JavaScript 无法直接监听基本类型的变化,需要通过对象的 getter/setter 来拦截。


9. 响应式丢失问题及解决方案

问题1:reactive 解构丢失响应式

  • 解决:使用 toRefs()

问题2:reactive 替换对象丢失响应式

  • 解决:修改属性而非替换对象,或使用 ref

问题3:数组/集合中的 ref

  • 解决:展开到 reactive 对象中

Composition API 面试题

10. Composition API 的优势?

  1. 逻辑复用:替代 mixins,避免命名冲突
  2. 代码组织:按功能组织代码,而非选项类型
  3. TypeScript 支持:更好的类型推断
  4. Tree Shaking:按需导入,减少打包体积
  5. 灵活灵活:代码可以更自由地组织

11. 自定义 Composable 的最佳实践

命名约定:

  • use 开头
  • 如:useCounteruseMouseuseFetch

关键要点:

  1. 参数处理:使用 toValue 处理 ref 或普通值
  2. 副作用清理:在 onUnmounted 中清理
  3. 约束条件:使用 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 的新特性

  1. 新的创建方式createRouter 替代 new VueRouter
  2. History 模式改进createWebHistory() 替代 mode: 'history'
  3. 组合式 APIuseRouter()useRoute()
  4. 导航守卫改进:next 是可选的
  5. 动态路由添加/删除addRoute()removeRoute()

14. 路由守卫执行顺序

  1. 全局 beforeEach
  2. 复用组件的 beforeRouteUpdate
  3. 路由配置的 beforeEnter
  4. 解析异步路由组件
  5. 激活组件的 beforeRouteEnter
  6. 全局 beforeResolve
  7. 全局 afterEach

15. 如何优雅地处理路由权限?

方案1:路由元信息 + 全局守卫

router.beforeEach((to) => {
  if (to.meta.requiresAuth && !isAuthenticated) {
    return { path: '/login', query: { redirect: to.fullPath }}
  }
})

方案2:动态路由生成

  • 根据后端返回的权限动态生成路由

Pinia 状态管理面试题

16. 为什么推荐使用 Pinia 替代 Vuex?

特性Vuex 4Pinia
Vue3 支持支持原生支持,更优
TypeScript复杂完美支持
APIOptions 风格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 2Nuxt 3/4
Vue 版本Vue 2Vue 3
引擎基于 Node.jsNitro 引擎(支持 Edge)
TypeScript需要配置原生支持
状态管理VuexPinia(内置)
构建工具WebpackVite(默认)

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:refcomputedwatch
  • Nuxt API:useRoute()useRouter()useFetch()
  • Composables:composables/ 目录下的函数

Nuxt 渲染模式与 SSR

21. Nuxt 支持的渲染模式

  1. SSR(默认):ssr: true
  2. SPAssr: false
  3. 静态生成(SSG)nuxt generate
  4. 混合渲染routeRules 配置
  5. 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 中间件类型

  1. 路由中间件(客户端)middleware/ 目录
  2. 全局中间件middleware/*.global.ts
  3. 服务端中间件server/middleware/

Vue3 性能优化

26. Vue3 性能优化手段

  1. 编译优化:静态提升、PatchFlag
  2. 组件优化shallowRefmarkRaw、虚拟列表
  3. 渲染优化v-oncev-memo、computed 缓存
  4. 异步组件:代码分割、懒加载
  5. 减少响应式开销readonlyshallowReactive
  6. 事件优化:事件委托、passive 事件、防抖/节流

27. Nuxt 项目性能优化

  1. 渲染策略routeRules 配置 ISR/SWR
  2. 图片优化NuxtImg 组件
  3. 代码优化:分析构建、代码分割
  4. 数据获取:合理使用缓存策略
  5. 插件优化.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 的渲染机制详解

答:

渲染流程:

  1. 编译阶段(Compile)

    • 模板 → AST(抽象语法树)
    • AST 优化(静态提升、标记动态节点)
    • AST → 渲染函数(render function)
  2. 运行时阶段(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)失败如何处理?

答:

水合失败原因:

  1. 服务端和客户端渲染结果不一致
  2. 客户端数据获取时机不对
  3. 使用了浏览器特有 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 应用内存泄漏?

面试官: 用户反馈应用运行一段时间后变得卡顿,怀疑是内存泄漏,你会如何排查?

答:

排查步骤:

  1. Chrome DevTools Memory 面板分析:

    • 拍摄堆快照(Heap Snapshot)
    • 对比多次快照,查看增长对象
    • 查看 Detached DOM Tree
  2. 常见泄漏场景:

<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>
  1. 使用 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'
  }
})

面试技巧与建议

准备建议

  1. 理解原理:不只是会用,要理解 Vue3 的响应式原理、编译优化等
  2. 对比差异:Vue2 vs Vue3、Vuex vs Pinia、Options API vs Composition API
  3. 项目经验:准备 2-3 个实际案例,体现解决问题的能力
  4. 代码规范:展示良好的 TypeScript 使用和代码组织能力
  5. 性能意识:了解常见的性能优化手段

常见问题追问

  • "你如何组织大型 Vue 项目的代码结构?"
  • "遇到过哪些 Vue 的坑,如何解决的?"
  • "如何做组件库的设计?"
  • "服务端渲染的优缺点是什么?"
  • "如何处理复杂的状态管理场景?"

参考资源


祝你面试顺利!