🔥Vue 面试收官篇:SSR、性能优化落地、30 道高频面试题精讲(附标准答案)
系列最后一篇。把这 30 道题吃透,Vue 面试你可以反向输出。
写在前面
这是 Vue 面试系列的最终篇 ([第 1 篇:核心基础](#第 1 篇:核心基础) | [第 2 篇:Composition API 进阶](#第 2 篇:Composition API 进阶) | [第 3 篇:Router/Pinia/响应式源码](#第 3 篇:Router/Pinia/响应式源码))。
前面三篇是"知识输入",这一篇是**"面试输出"**:
- SSR 到底解决什么问题,Nuxt 3 怎么用
- 测试怎么写(很多候选人只会说"写过"但不会写)
- 30 道高频面试题的完整标准答案------基础题、进阶题、源码题、实战题四个梯队
适合谁看?
- ✅ 面试前一周做最后冲刺的同学(第 4 节的 30 道题就是模拟考卷)
- ✅ 想了解 SSR / 测试但一直没动手实践的人
- ✅ 想收藏一份 Vue 面试题库随时翻的人
阅读建议 :第 4 节建议合上文章自己先答一遍,再对照答案找差距。
📑 目录
- 服务端渲染(SSR)
- 测试
- [Vue 生态工具](#Vue 生态工具)
- 最佳实践
- [高频面试题精选(30 道标准答案)](#高频面试题精选(30 道标准答案))
📌 附:全系列知识点总索引
26. 服务端渲染(SSR)
26.1 先想清楚:为什么需要 SSR
CSR(客户端渲染)的问题:
浏览器 → 下载 HTML(空壳) → 下载 JS → 执行 JS → 渲染页面
↑ 这一段用户看到的是白屏
SSR(服务端渲染):
浏览器 → 下载 HTML(已填充内容) → 立即显示 → 下载 JS → 接管(hydration)
↑ 首屏内容直出
收益 :SEO 友好(爬虫拿到完整内容)、首屏时间大幅缩短、弱网设备体验好。
代价 :服务器算力开销、开发复杂度上升(注意窗口对象、生命周期差异)、mounted 之前的钩子外的环境差异。
判断口诀:内容型站点(官网、博客、电商)需要 SEO → 上 SSR;后台管理系统、登录后的应用 → CSR 足够。
26.2 Nuxt 3 实战速览
typescript
// nuxt.config.ts
export default defineNuxtConfig({
ssr: true, // 开 SSR(false 则变 SPA)
pages: true, // pages/ 目录自动路由
components: true, // 组件自动导入
modules: ['@pinia/nuxt'], // Pinia 官方模块
runtimeConfig: {
apiSecret: '', // 仅服务端可见
public: { apiBase: '/api' } // 客户端可见
}
});
vue
<!-- pages/index.vue ------ 约定式路由 + 服务端取数 -->
<script setup>
// useFetch:SSR 阶段在服务端请求,客户端 hydration 复用结果,不重复请求
const { data, pending, error } = await useFetch('/api/articles');
// SEO
useHead({
title: '首页',
meta: [{ name: 'description', content: '页面描述' }]
});
// 页面级中间件 + 指定布局
definePageMeta({ middleware: 'auth', layout: 'default' });
</script>
typescript
// server/api/articles.ts ------ 服务端 API(Nitro,可部署为 Serverless 函数)
export default defineEventHandler(async (event) => {
const query = getQuery(event);
return await getArticles(query);
});
💬 面试题精选
Q1:SSR 和 CSR 的区别?什么时候用 SSR?
答(决策导向):
| 特性 | SSR | CSR |
|---|---|---|
| 首屏 | 服务端直出 HTML,快 | 下载 JS 后渲染,慢 |
| SEO | ✅ 爬虫拿到完整内容 | ❌ 需要额外预渲染 |
| 服务器成本 | 高 | 低 |
| 开发复杂度 | 高(生命周期、环境判断) | 低 |
| 适用 | 内容站、SEO 刚需 | 后台系统、Web 应用 |
什么时候用:有 SEO 需求/首屏性能要求高(移动端弱网)→ SSR 或 SSG;纯交互型应用(管理后台)→ CSR。
💡 加分话术 :还有第三条路 SSG(静态生成)------构建时把页面渲染成静态 HTML,没有服务器渲染开销又保留 SEO,博客、文档站首选(VitePress/Nuxt SSG)。选型上我会先看"页面是否依赖实时数据 + 是否需要 SEO"这两个维度。
Q2:SSR 时数据是怎么获取的?为什么 hydration 不会重复请求?
答:Nuxt 的 useFetch/useAsyncData 在服务端渲染阶段 就发起请求,结果序列化内联进 HTML(__NUXT_DATA__ 脚本);客户端 hydration 时读取这份内联数据直接复用,不会重新发请求。如果客户端导航到新页面,则走正常的客户端请求。
💡 坑点提醒:SSR 阶段没有 window/document ,访问浏览器 API 要放在
onMounted里或import.meta.client判断,否则服务端报错。
27. 测试
💡 面试被问"写过测试吗",答"写过"和答"写过,我是这么组织的"是两回事。这节给你能讲出来的代码。
27.1 组件测试(Vitest + Vue Test Utils)
typescript
// components/Counter.spec.ts
import { describe, it, expect } from 'vitest';
import { mount } from '@vue/test-utils';
import Counter from './Counter.vue';
describe('Counter.vue', () => {
it('正确渲染初始值', () => {
const wrapper = mount(Counter, { props: { initialCount: 0 } });
expect(wrapper.text()).toContain('0');
});
it('点击按钮递增并发出事件', async () => {
const wrapper = mount(Counter);
await wrapper.find('button.increment').trigger('click');
// 断言 DOM 更新
expect(wrapper.find('.count').text()).toBe('1');
// 断言事件( emitted() 拿到所有触发记录 )
expect(wrapper.emitted('update')![0]).toEqual([1]);
});
it('渲染插槽内容', () => {
const wrapper = mount(Counter, {
slots: { default: '<span>自定义内容</span>' }
});
expect(wrapper.html()).toContain('自定义内容');
});
it('mock 子组件,专注测当前组件', () => {
const wrapper = mount(MyPage, {
global: { stubs: { RouterLink: true, HeavyChart: true } }
});
});
});
💡 三个最常用的断言姿势:
wrapper.find()查 DOM、trigger('click')触发事件(注意要 await)、wrapper.emitted()验证事件。
27.2 Pinia 测试
typescript
// stores/counter.spec.ts
import { describe, it, expect, beforeEach } from 'vitest';
import { setActivePinia, createPinia } from 'pinia';
import { useCounterStore } from './counter';
describe('Counter Store', () => {
beforeEach(() => {
// ⚠️ 每个用例前创建全新 pinia 实例,保证用例间不互相污染
setActivePinia(createPinia());
});
it('increment 生效', () => {
const store = useCounterStore();
store.increment();
expect(store.count).toBe(1);
});
it('getter 计算正确', () => {
const store = useCounterStore();
store.count = 5;
expect(store.doubleCount).toBe(10);
});
});
💬 面试题精选
Q1:Vue 项目的测试策略怎么设计?
答(测试金字塔):
- 单元测试(最多):工具函数、Composables、Store------Vitest,毫秒级
- 组件测试:关键交互组件的渲染/事件/emit------Vue Test Utils
- E2E 测试(最少但关键):登录、下单等核心链路------Cypress / Playwright
💡 加分话术 :我的实践是"Store 和 Composables 必测(它们是业务逻辑核心),展示组件抽几个交互复杂的测,核心链路用 E2E 兜底"。测试的价值在于重构时的安全感,不是覆盖率数字。
Q2:mount 和 shallowMount 的区别?
答:mount 完整挂载组件包括子组件;shallowMount 把子组件替换成占位 stub,只测当前组件本身。现在 VTU 2.x 推荐用 mount + global.stubs 显式声明要 stub 的组件,语义更清晰。
28. Vue 生态工具
💡 这节不用背,知道"有什么轮子"即可,面试时提到是项目经验加分项。
28.1 VueUse:组合式函数工具库(必装)
typescript
import {
useMouse, useLocalStorage, useDark, useDebounceFn,
useIntersectionObserver, useEventListener
} from '@vueuse/core';
// 自动处理监听和卸载的工具,比自己写少踩 80% 的坑
const { x, y } = useMouse();
const theme = useDark(); // 暗黑模式一行搞定
const keyword = ref('');
const debouncedSearch = useDebounceFn(search, 300);
// 元素进入视口(图片懒加载、无限滚动)
useIntersectionObserver(el, ([{ isIntersecting }]) => {
if (isIntersecting) loadMore();
});
// 事件监听自动随组件卸载移除
useEventListener(window, 'resize', handler);
28.2 表单验证 VeeValidate + Zod
vue
<script setup>
import { useForm, useField } from 'vee-validate';
import { z } from 'zod';
import { toTypedSchema } from '@vee-validate/zod';
const schema = toTypedSchema(z.object({
email: z.string().email('请输入有效邮箱'),
password: z.string().min(6, '密码至少 6 位')
}));
const { handleSubmit, errors } = useForm({ validationSchema: schema });
const { value: email } = useField('email');
const { value: password } = useField('password');
const onSubmit = handleSubmit((values) => { /* values 类型安全 */ });
</script>
<template>
<form @submit="onSubmit">
<input v-model="email" />
<span>{{ errors.email }}</span>
<button type="submit">提交</button>
</form>
</template>
28.3 其他生态速览
- UI 库:Element Plus(中后台)、Vant(移动端)、Vuetify(Material)
- 国际化 :vue-i18n(
t('message.hello', { name })) - CSS 方案:UnoCSS / Tailwind(原子化)、SCSS modules
- 文档:VitePress(Vue 官方文档站就是)
💡 面试话术:组件库选型我会看三点------TS 类型支持、按需引入/Tree-shaking 能力、主题定制体系(CSS 变量方案优先,方便换肤)。
29. 最佳实践
29.1 项目结构
src/
├── api/ # API 请求层(按业务模块拆分)
│ └── modules/
├── components/
│ ├── common/ # 通用组件(无业务)
│ └── business/ # 业务组件
├── composables/ # 组合式函数
├── directives/ # 自定义指令
├── layouts/ # 布局组件
├── router/ # 路由
├── stores/ # Pinia
├── styles/ # 全局样式/主题变量
├── types/ # TS 类型
└── utils/ # 纯工具函数
29.2 组件设计原则(被问"如何设计组件"时按这四点答)
vue
<script setup lang="ts">
// ① 单一职责:一个组件只做一件事,做大了就拆
// ② Props:类型明确 + 默认值 + 校验
interface Props {
title: string;
type?: 'primary' | 'secondary';
}
const props = withDefaults(defineProps<Props>(), { type: 'primary' });
// ③ 事件:动词命名,参数语义明确
const emit = defineEmits<{
(e: 'update', value: string): void;
(e: 'delete', id: number): void;
}>();
// ④ 插槽:提供合理的默认内容,使用者不传也能用
</script>
<template>
<div class="card">
<slot name="header"><h3>{{ title }}</h3></slot>
<slot>默认内容</slot>
</div>
</template>
29.3 错误处理体系
typescript
// main.ts:全局兜底(配合 Sentry 等上报)
app.config.errorHandler = (err, instance, info) => {
reportError(err);
};
// 组件级:onErrorCaptured 做局部兜底(如表单区错误不拖垮整页)
onErrorCaptured((err) => {
showToast('该模块出错了');
return false; // 阻止继续向上传播
});
// API 层:统一拦截,401 跳登录、其他弹 toast
request.interceptors.response.use(
res => res.data,
err => {
if (err.response?.status === 401) router.push('/login');
return Promise.reject(err);
}
);
30. 高频面试题精选
🎯 使用方式:合上文自己先答,再对答案。标 ⭐ 的是出现率最高的。
第一梯队:基础题(必对,错一道都危险)
Q1:Vue 的核心特性有哪些?
响应式数据绑定、组件化、虚拟 DOM、指令系统、模板语法、渐进式框架。(展开见第 1 篇 §1)
Q2:⭐ v-if 和 v-show 的区别?
v-if 条件渲染(false 时销毁组件,切换开销大);v-show 仅切 display(初始渲染开销大,切换快)。频繁切换用 v-show,条件稳定用 v-if。
Q3:⭐ Vue 2 和 Vue 3 的主要区别?
响应式(defineProperty→Proxy)、Composition API、编译优化+Tree-shaking、Fragment、原生 TS。(展开见第 1 篇 §3)
Q4:⭐ 为什么 v-for 要加 key?index 做 key 有什么问题?
key 是节点身份标识,帮助 diff 复用/移动节点。index 在顺序变化时导致组件状态错乱(输入框串位、选中态错位)。用唯一 id。(第 1 篇 §6)
第二梯队:进阶题(决定面试评级)
Q5:⭐ Composition API 解决了什么问题?
Options API 中同一逻辑代码分散、Mixins 复用有命名冲突和来源不清问题;Composition API 按逻辑功能组织,Composables 实现干净复用。(第 2 篇 §12)
Q6:⭐ Pinia 和 Vuex 的区别?
去掉 mutations(action 直接改 state)、TS 原生支持、扁平模块化、体积 ~1KB。Pinia 是官方钦定的 Vuex 继任者。(第 3 篇 §21)
Q7:⭐ watch 和 watchEffect 的区别?
watch 显式指定源、可拿旧值、懒执行;watchEffect 自动追踪、立即执行、无旧值。副作用清理都用 onCleanup 防竞态。(第 2 篇 §13)
Q8:组件通信有哪些方式?
props/emit(父子)、provide/inject(跨层)、Pinia(全局)、mitt(任意)。加分项:按场景选型 + 说明 Event Bus 在大型项目难追踪的缺陷。(第 1 篇 §10)
第三梯队:源码题(拉开差距)
Q9:⭐⭐ Vue 3 响应式原理,能手写吗?
Proxy 拦截 + track 依赖收集 + trigger 派发更新,WeakMap 存依赖防泄漏,惰性代理提升初始化性能。建议默写 第 3 篇 §23.2 的迷你实现。
Q10:⭐ Vue 的虚拟 DOM 和 Diff 算法?
数据变化生成新 vnode → 与旧 vnode 比对 → 最小化更新真实 DOM。Vue 3 的优化:Patch Flags 标记动态内容只比对动态部分、Block Tree 跳过静态、最长递增子序列优化移动。意义:用 JS 计算换 DOM 操作(DOM 操作才是性能瓶颈)。
Q11:Vue 3 编译时优化有哪些?
静态提升(hoistStatic)、Patch Flags、Block Tree、静态节点字符串化、事件监听缓存------运行时快的原因大头在编译期。
Q12:为什么 ref 需要 .value?
ref 用包装对象承载任意类型(Proxy 只能代理对象),拦截发生在 .value 这一层。模板中自动解包。(第 1/3 篇)
第四梯队:实战题(体现项目深度)
Q13:⭐ 如何实现前端权限控制?
两种方案:① 路由 meta + 全局守卫做拦截;② 登录后按角色 addRoute 动态注册(路由表里没有,更安全),配合 404 兜底。实际项目两者结合。(第 3 篇 §19)
Q14:⭐ Vue 项目性能优化有哪些?
四层框架:加载层(懒加载/按需/图片优化/CDN)、运行时层(虚拟滚动/防抖节流/shallowRef/markRaw/v-memo)、更新层(合理 key/组件拆细/computed)、体验层(keep-alive/骨架屏/SSR)。先测量再优化。(第 3 篇 §24)
Q15:如何设计一个通用组件/组件库?
单一职责;Props 类型+默认值+校验;事件动词命名;插槽给默认内容;主题用 CSS 变量;Storybook 写文档;单测+快照;构建支持 Tree-shaking 按需引入;语义化版本管理。
Q16:nextTick 的原理和使用场景?
Vue 更新是异步批量的(微任务队列),数据变了 DOM 不会立即更新。await nextTick() 后在回调里拿最新 DOM。场景:改数据后需要测量/操作新 DOM。
Q17:说说 Vue 的 hydration(水合)?
SSR 页面拿到的是静态 HTML,客户端 JS 加载后需要"激活"它------对比服务端 HTML 与虚拟 DOM,挂上事件监听和响应式,让它变成可交互的。hydration 失败常见原因:服务端与客户端渲染结果不一致。
Q18:Vuex/Pinia 的持久化怎么做?
store.$subscribe 订阅变化写 localStorage(注意只存可序列化数据、敏感数据不要存、可配 pinia-plugin-persistedstate)。
Q19:海量数据渲染怎么优化?
虚拟滚动(定高用 useVirtualList,不定高用动态高度缓存);分页/无限滚动加载;shallowRef 存数据避开深层代理;web worker 做数据处理。
Q20:封装 axios 你做了哪些事?
baseURL 环境区分、请求拦截注入 token、响应拦截统一拆 data/错误处理(401 跳登录/刷新 token)、取消重复请求(防抖队列)、loading 与错误 toast 全局开关。(第 4 篇附录有完整代码参考)
Q21-30 :路由守卫完整执行顺序、SSR 与 SSG 选型、keep-alive 原理、Teleport 场景、作用域插槽与无渲染组件、自定义指令场景、跨域与代理配置、v-model 组件封装、错误处理体系、组件库按需引入原理------全部覆盖在前三篇对应章节,此处不重复展开,面试前请按速查表回查。
全系列知识点总索引
| 篇目 | 覆盖内容 |
|---|---|
| [第 1 篇:核心基础](#篇目 覆盖内容 第 1 篇:核心基础 模板语法、响应式、computed/watch、指令、组件通信、生命周期 第 2 篇:进阶 Composition API、侦听器、异步组件/keep-alive/Teleport、自定义指令、插槽、动画、Provide/Inject 第 3 篇:硬核 Router、Vuex、Pinia、Vue3 新特性、响应式源码手写、性能优化、TypeScript 第 4 篇:收官(本文) SSR/Nuxt、测试、生态工具、最佳实践、30 道面试题) | 模板语法、响应式、computed/watch、指令、组件通信、生命周期 |
| [第 2 篇:进阶](#篇目 覆盖内容 第 1 篇:核心基础 模板语法、响应式、computed/watch、指令、组件通信、生命周期 第 2 篇:进阶 Composition API、侦听器、异步组件/keep-alive/Teleport、自定义指令、插槽、动画、Provide/Inject 第 3 篇:硬核 Router、Vuex、Pinia、Vue3 新特性、响应式源码手写、性能优化、TypeScript 第 4 篇:收官(本文) SSR/Nuxt、测试、生态工具、最佳实践、30 道面试题) | Composition API、侦听器、异步组件/keep-alive/Teleport、自定义指令、插槽、动画、Provide/Inject |
| [第 3 篇:硬核](#篇目 覆盖内容 第 1 篇:核心基础 模板语法、响应式、computed/watch、指令、组件通信、生命周期 第 2 篇:进阶 Composition API、侦听器、异步组件/keep-alive/Teleport、自定义指令、插槽、动画、Provide/Inject 第 3 篇:硬核 Router、Vuex、Pinia、Vue3 新特性、响应式源码手写、性能优化、TypeScript 第 4 篇:收官(本文) SSR/Nuxt、测试、生态工具、最佳实践、30 道面试题) | Router、Vuex、Pinia、Vue3 新特性、响应式源码手写、性能优化、TypeScript |
| 第 4 篇:收官(本文) | SSR/Nuxt、测试、生态工具、最佳实践、30 道面试题 |
📌 最后
四篇写完,这套 Vue 知识体系总算完整了。从模板语法到响应式源码,从组件通信到 SSR------收藏这一篇,等于收藏了整个系列。
- 如果这个系列帮到了你,点赞 ❤️ + 收藏 ⭐ 是对创作者最大的鼓励
- 评论区可以留下你面试遇到的新题,我会持续补充到题库中(更新日志见下)
- 接下来想看什么系列?React 八股文 / 前端工程化 / 算法面试------评论区投票,票数高的优先写 🙏
附录:两个可以直接放进项目的工具封装
1. 通用的 loading 组合式函数
typescript
// composables/useLoading.ts
import { ref } from 'vue';
export function useLoading(initial = false) {
const loading = ref(initial);
async function withLoading<T>(fn: () => Promise<T>): Promise<T> {
loading.value = true;
try {
return await fn();
} finally {
loading.value = false; // finally 保证异常时也能复位
}
}
return { loading, withLoading };
}
// 使用:const { loading, withLoading } = useLoading();
// await withLoading(() => api.getList())
2. 封装好的 axios 实例(面试常问的"请求层")
typescript
// utils/request.ts
import axios from 'axios';
const request = axios.create({
baseURL: import.meta.env.VITE_API_BASE,
timeout: 10000
});
request.interceptors.request.use((config) => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
request.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// 清 token 跳登录
window.location.href = '/login';
}
return Promise.reject(error);
}
);
export default request;