前端基础 — 小白讲解 + 面试题精解
第九阶段:前端基础(全栈协作)— 讲解与面试题
本文档定位: 对应《简历技术栈学习指南》第九阶段。你简历中写的是“技术栈:Spring Boot + LangChain4j + Redis + PostgreSQL(pgvector) + Vue3 + DeepSeek API”,技能清单写“了解 Vue.js/Element UI 前端技术,了解 Node.js/TypeScript”。你是后端工程师,面试官不会深问前端,但会确认两件事:① 你能看懂/改动前端代码;② 你能把前后端对接的细节(尤其是 SSE 流式、跨域、Token)讲清楚。
使用方法: 每个知识点按
小白讲解 → 名词详解(举例) → 结合简历案例 → 代码示例 → 面试题组织。作为后端,重点掌握前四章 + SSE 章。
目录
- 第一章:Vue3 核心概念(能看懂前端代码)
- 第二章:组件化与通信
- 第三章:前后端协作(后端视角重点)
- 第四章:SSE 流式对话(你 RAG 项目的核心对接点)
- 第五章:Element Plus 与工程化名词
- 第六章:面试速查表
第一章:Vue3 核心概念(能看懂前端代码)
1.1 Vue 是什么?和 jQuery 的思维差别
小白讲解
- jQuery 思维(命令式):“拿到数据了 → 我手动找到那个 DOM 节点 → 把文本塞进去”(
$('#name').text('张三')) - Vue 思维(声明式):“页面长什么样由数据决定,我只改数据,界面自动变”
<!-- 声明式:count 变了,界面上的数字自动变,不需要操作 DOM -->
<button @click="count++">点了 {{ count }} 次</button>
一句话:数据驱动视图。改数据(状态),不改页面(DOM)。
1.2 名词详解(看到这些词不能懵)
| 名词 | 解释 | 举例 |
|---|---|---|
| MVVM | Model(数据)-View(页面)-ViewModel(Vue 实例,双向绑定桥梁) | 你改 input 框,question 变量自动变;question 变了,页面也自动变 |
| 响应式(Reactivity) | 数据被修改时,依赖它的页面部分自动更新的机制 | ref(0) 的 count 一变,模板里 {{ count }} 立即重渲染 |
| v-model | 双向绑定语法糖 | 聊天输入框 <el-input v-model="question">:输入即改变量、变量改即改输入框 |
| 指令(Directive) | 模板里带 v- 前缀的特殊属性 |
v-if(条件渲染)/ v-for(循环)/ v-on/@(事件)/ v-bind/:(绑定属性) |
| 组件(Component) | 可复用的页面积木,自带模板+逻辑+样式 | ChatMessage.vue(单条气泡)、DocUpload.vue(上传组件) |
| SFC(Single File Component) | .vue 单文件组件:template + script + style 三段式 |
你的每个页面/组件都是 SFC |
| 生命周期钩子 | 组件从创建到销毁的关键时间点回调 | onMounted(页面加载完 → 调接口拉历史对话) |
| 组合式 API(Composition API) | Vue3 主推的 setup() 写法(对比 Vue2 的 options API) |
ref/reactive/onMounted/watch/computed |
| 虚拟 DOM(Virtual DOM) | 用 JS 对象描述页面结构,diff 出最小变更再更新真实 DOM | 数据变了 → 新旧虚拟 DOM 对比 → 只改差异部分(性能好) |
| SPA(Single Page Application) | 单页应用:只有一个 html,路由切换由 JS 完成,不整页刷新 | RAG 助手页面:点“知识库管理”不刷页面,局部切换视图 |
响应式原理(Vue3 vs Vue2,高频)
Vue2:Object.defineProperty —— 劫持对象属性的 getter/setter。
缺陷(举例): ① 新增属性不响应(要 $set);② 数组下标赋值不响应;③ 性能问题(初始化递归全部属性)。
Vue3:Proxy —— 代理整个对象,任何读写操作(包括新增/删除/数组下标)都能拦截。
// Proxy 直观理解:给对象装一个"代理人",所有操作先经过它
const data = { question: '' };
const proxy = new Proxy(data, {
get(target, key) {
console.log(`读取了 ${key}`); // 依赖收集:谁在用这个属性
return target[key];
},
set(target, key, value) {
console.log(`修改了 ${key} = ${value}`); // 派发更新:通知用到的页面部分刷新
target[key] = value;
return true;
}
});
proxy.question = '什么是头寸?'; // 触发 set → 自动更新聊天界面
一句话面试答案: “Vue2 用 defineProperty 逐属性劫持,新增属性检测不到;Vue3 换成 Proxy 代理整个对象,任意操作都能拦截,还支持 Map/Set,性能上也只在访问时才递归(懒代理)。”
1.3 ref vs reactive(Vue3 最容易混的两个 API)
import { ref, reactive } from 'vue';
// ref:包装任意值(基本类型/对象),取值/赋值要 .value
const question = ref(''); // 聊天输入框内容
question.value = '什么是头寸?'; // script 里必须 .value
// 但模板里自动解包:<div>{{ question }}</div>(不用 .value)
// reactive:只包装对象/数组,直接点属性,不能整体替换
const chatState = reactive({
messages: [], // 消息列表
loading: false, // 是否正在生成回答
currentAnswer: '' // 正在流式输出的回答
});
chatState.loading = true; // 直接改,不用 .value
| 对比 | ref | reactive |
|---|---|---|
| 支持类型 | 任意(基本类型必须用它) | 仅对象/数组 |
| 访问方式 | .value(模板自动解包) |
直接点属性 |
| 重新赋值 | xxx.value = 新值 ✅ |
整体替换会丢失响应式 ❌ |
你的 RAG 前端举例: 聊天记录数组用 ref([]),因为 SSE 每来一段就 messages.value.push(...) 或整体重新赋值都安全。
1.4 computed 与 watch
// computed:依赖变化自动重算(有缓存,依赖不变不重算)
const wordCount = computed(() => question.value.length);
// 输入框字数实时统计:question 一变 wordCount 自动变
// watch:监听数据变化执行副作用(典型:调后端接口)
watch(() => route.params.docId, (newId) => {
loadDocument(newId); // 切换文档时重新加载
});
区别一句话: computed 是“由 A 算出 B”(用返回值),watch 是“A 变了去做某件事”(用副作用,如调接口)。
第二章:组件化与通信
2.1 父子组件通信(props / emit)
小白讲解 + 举例
组件之间像公司层级:父组件(页面)给子组件(积木)派活用 props;子组件干完活汇报用 emit。
<!-- 父组件 ChatPage.vue -->
<template>
<!-- ① 派活:把消息列表传给子组件 -->
<ChatMessageList
:messages="messages"
@regenerate="handleRegenerate" <!-- ② 监听子组件的汇报事件 -->
/>
</template>
<!-- 子组件 ChatMessageList.vue -->
<script setup>
const props = defineProps({
messages: { type: Array, default: () => [] } // 声明接收什么
});
const emit = defineEmits(['regenerate']);
function onRetry() {
emit('regenerate', props.messages.length); // 汇报:带上参数
}
</script>
规则(面试常考): props 单向数据流——子组件不能直接改 props(改了父组件不知道,数据流向乱)。要改就 emit 事件让父组件自己改。
2.2 组件间通信方式总表(按场景选)
| 方式 | 场景 | 你项目的例子 |
|---|---|---|
| props / emit | 父子组件 | 页面 → 消息列表组件传 messages |
| v-model | 表单双向绑定(本质是 props+emit 的语法糖) | 聊天输入框 |
| provide / inject | 跨多层级注入(祖先→后代) | 全局主题/当前用户信息注入深层组件 |
| 状态管理 Pinia | 全局共享状态 | 登录 token、用户信息、全局配置 |
| 事件总线 mitt | 极少数解耦场景(Vue3 移除了 $on) | 一般不用,用 Pinia 替代 |
2.3 生命周期(后端重点记 3 个)
import { onMounted, onUnmounted } from 'vue';
onMounted(() => {
// ① DOM 已就绪 → 适合调接口初始化
loadHistory(); // 拉取历史对话记录
});
onUnmounted(() => {
// ② 组件销毁 → 必须清理,否则内存泄漏
eventSource.close(); // 关闭 SSE 连接(超重要!见第四章)
clearInterval(timer);
});
// ③ watch 的自动清理:onScopeDispose,一般了解即可
面试话术(结合项目): “我们踩过一个坑:用户在对话页切换到文档管理页后,SSE 连接没关,后端还在推 token,浏览器控制台报错且连接数累积。后来在 onUnmounted 里统一 eventSource.close() 解决——组件卸载时清理副作用(定时器、WebSocket/SSE、事件监听)是前端资源管理的基本功。”
第三章:前后端协作(后端视角重点)
3.1 前后端分离架构名词
| 名词 | 解释 | 举例 |
|---|---|---|
| 前后端分离 | 前端独立工程(Vue CLI/Vite 起的静态站点),只通过 HTTP API 和后端交互 | Vue3 工程打包成静态文件,由 Nginx 托管,调 Spring Boot 接口 |
| 跨域(CORS) | 浏览器同源策略:协议+域名+端口任一不同就是跨域,请求会被浏览器拦截 | 前端 localhost:5173 调后端 localhost:8080 就是跨域 |
| CORS 预检(Preflight) | 浏览器对“非简单请求”先发 OPTIONS 试探,问服务端允不允许 | 带了 Authorization 头 → 浏览器先发 OPTIONS |
| Token/JWT | 无状态认证:登录后后端发 token,前端每次请求带上 | 前端存 token,Axios 拦截器统一加 Authorization: Bearer xxx |
| 环境代理 | 开发时用 Vite devServer 代理解决跨域 | /api 请求代理到 localhost:8080 |
| Nginx 反向代理 | 生产时前端和 API 同域名,Nginx 把 /api 转发到后端 |
彻底消除跨域(同源) |
跨域后端怎么解决(后端必须会答)
// 方式一:Spring 全局 CORS 配置(推荐)
@Configuration
public class CorsConfig {
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.setAllowedOriginPatterns(List.of("http://localhost:5173", "https://rag.xxx.cn"));
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "OPTIONS"));
config.setAllowedHeaders(List.of("*"));
config.setAllowCredentials(true); // 带 cookie 时必须
config.setMaxAge(3600L); // 预检结果缓存1小时,减少 OPTIONS
UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
source.registerCorsConfiguration("/**", config);
return new CorsFilter(source);
}
}
面试话术: “跨域是浏览器的安全策略(同源策略),不是后端拒绝请求——所以 Postman 不跨域、浏览器才跨域。解决上开发期用 Vite 代理,生产用 Nginx 同域部署 + 反向代理,后端同时开 CORS 白名单兜底。”
3.2 Axios 拦截器(前端统一处理 token 和错误)
// request.js — 你的 RAG 前端真实会用到
import axios from 'axios';
const request = axios.create({ baseURL: '/api', timeout: 30000 });
// 请求拦截器:每次请求自动带 token
request.interceptors.request.use(config => {
const token = localStorage.getItem('token');
if (token) config.headers.Authorization = `Bearer ${token}`;
return config;
});
// 响应拦截器:统一处理错误
request.interceptors.response.use(
res => res.data,
err => {
if (err.response?.status === 401) {
localStorage.removeItem('token');
location.href = '/login'; // token 失效统一跳登录
} else if (err.response?.status === 429) {
ElMessage.warning('提问太频繁,请稍后再试'); // 呼应后端限流
}
return Promise.reject(err);
}
);
后端配合点(你能在面试讲的): 后端定义统一返回体 Result<T>{code, message, data},前端拦截器只认 code!=200 就弹 message——前后端契约统一,谁都不用各处判断。
3.3 RESTful 接口约定(协作语言)
| 操作 | 方法 + URL | 你 RAG 项目的例子 |
|---|---|---|
| 查列表 | GET /api/documents | 文档列表页 |
| 查单个 | GET /api/documents/{id} | 文档详情 |
| 新增 | POST /api/documents(multipart 上传) | 上传 PDF/Word |
| 修改 | PUT /api/documents/{id} | 修改文档标签 |
| 删除 | DELETE /api/documents/{id} | 删除文档 |
| 动作类 | POST /api/chat/stream | 发起对话(SSE 流式) |
加分点: “资源用名词、操作用 HTTP 方法表达;对话这种’动作’不好资源化时我们单独开了 /chat 端点,语义优先于教条。”
第四章:SSE 流式对话(你 RAG 项目的核心对接点)
这一章是你作为后端最该讲的亮点——LLM 一个 token 一个 token 往外吐,用户体验上“打字机效果”就是前后端用 SSE 配合实现的。
4.1 名词详解:SSE 是什么?
SSE(Server-Sent Events,服务器推送事件):HTTP 长连接上,服务器单向持续推送文本流给浏览器,浏览器用 EventSource API 接收。
SSE vs WebSocket vs 轮询(高频对比题)
| 对比 | 轮询 | SSE | WebSocket |
|---|---|---|---|
| 通信方向 | 客户端反复问 | 服务器单向推 | 全双工双向 |
| 协议 | 普通 HTTP | 普通 HTTP | 独立 ws:// 协议(HTTP 升级握手) |
| 数据格式 | 任意 | 文本(UTF-8) | 任意(含二进制) |
| 断线重连 | 自己写 | 浏览器自动重连(retry 字段) | 自己实现(心跳+重连) |
| 浏览器支持 | 全部 | EventSource 原生支持 | 全部 |
| 适用场景 | 简单低频 | LLM 流式输出、行情推送、进度通知 | 聊天室、协同编辑、游戏 |
为什么 LLM 对话选 SSE 不选 WebSocket? 对话本质是“一问一答”:请求发出后只有服务器往回推,单向就够。SSE 基于普通 HTTP:不用升级协议、过网关/代理/Nginx 兼容性好、自带断线重连、实现简单。WebSocket 的全双工能力在这里是浪费。
4.2 后端实现(Spring Boot SseEmitter)
@RestController
@RequestMapping("/api/chat")
public class ChatController {
@PostMapping(value = "/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public SseEmitter chat(@RequestBody ChatRequest req) {
// 超时给长一点:LLM 生成长回答可能超过默认 30s
SseEmitter emitter = new SseEmitter(120_000L);
chatService.streamChat(req, new SseChatListener(emitter));
return emitter;
}
}
@Component
public class SseChatListener implements StreamingChatResponseHandler {
private final SseEmitter emitter;
@Override
public void onPartialResponse(String token) {
try {
// 每来一个 token 就推一个 event;data: + \n\n 是 SSE 帧格式
emitter.send(SseEmitter.event().name("message").data(token));
} catch (IOException e) {
// 用户关页面了,通知 LangChain4j 取消上游 LLM 请求,省钱!
emitter.completeWithError(e);
}
}
@Override
public void onCompleteResponse(String fullAnswer) {
emitter.send(SseEmitter.event().name("done").data("[DONE]"));
emitter.complete(); // 正常结束连接
}
@Override
public void onError(Throwable error) {
emitter.send(SseEmitter.event().name("error").data("生成失败,请重试"));
emitter.complete(); // 异常也要关闭,防连接泄漏
}
}
SSE 报文格式(原始 HTTP 视角,能讲出来很加分):
HTTP/1.1 200 OK
Content-Type: text/event-stream;charset=UTF-8
Cache-Control: no-cache ← 禁止代理缓存
Connection: keep-alive
event: message
data: 头寸
event: message
data: 是指
event: message
data: 资产托管中
event: done
data: [DONE]
规则:每个事件以**空行(\n\n)**结尾;data: 是数据字段;event: 是事件类型;retry: 告诉浏览器断线后多少毫秒自动重连。
4.3 前端实现(EventSource + 流式渲染)
// ChatPage.vue — 组合式 API 完整示例
<script setup>
import { ref, onUnmounted } from 'vue';
const messages = ref([]); // 对话记录
const question = ref(''); // 输入框
const generating = ref(false); // 是否生成中(控制按钮禁用+加载动画)
let eventSource = null; // SSE 连接
async function send() {
if (!question.value.trim() || generating.value) return;
generating.value = true;
messages.value.push({ role: 'user', content: question.value });
const aiMsg = { role: 'assistant', content: '' }; // 先占位,流式往里追加
messages.value.push(aiMsg);
const q = question.value;
question.value = '';
// EventSource 只支持 GET,所以 POST 场景用 fetch + ReadableStream 解析
const resp = await fetch('/api/chat/stream', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${token}` },
body: JSON.stringify({ question: q })
});
const reader = resp.body.getReader();
const decoder = new TextDecoder('utf-8');
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
// 按 SSE 帧规则切分:双换行分隔事件
const frames = buffer.split('\n\n');
buffer = frames.pop(); // 最后一段可能不完整,留在缓冲区
for (const frame of frames) {
const data = frame.replace(/^data:\s?/gm, '');
if (data === '[DONE]') continue;
aiMsg.content += data; // 打字机效果:响应式自动刷新界面
}
}
generating.value = false;
}
onUnmounted(() => {
if (eventSource) eventSource.close(); // 组件卸载必须关连接
});
</script>
面试连环问预演:
- Q:为什么不用原生 EventSource? A:EventSource 只支持 GET 且不能自定义 Header(带不了 JWT token)。POST + Authorization 场景用 fetch 的 ReadableStream 手动解析 SSE 帧。
- Q:断线了怎么办? A:原生 SSE 有 retry 自动重连,但对话场景重连会重复生成;我们做成前端捕获流中断后展示“重新生成”按钮,后端对同一会话的上下文有 Redis 缓存(TTL 30 分钟),重试时带上对话 ID 复用上文。
- Q:token 是乱码怎么办? A:
TextDecoder('utf-8', {stream:true})——一个中文字符占 3 字节,一个 chunk 可能切在多字节字符中间,必须用流式解码器拼接,不能直接String.fromCharCode。 - Q:用户中途关闭页面? A:后端
emitter.send抛 IOException,我们在 catch 里取消上游 LLM 请求——否则用户走了 LLM 还在烧钱生成。
4.4 Nginx 对 SSE 的关键配置(生产必踩坑)
location /api/chat/ {
proxy_pass http://rag-backend:8080;
proxy_http_version 1.1;
proxy_set_header Connection '';
proxy_buffering off; # ★ 关键:关闭缓冲,token 立刻推给浏览器
proxy_cache off;
proxy_read_timeout 300s; # 长回答不超时
# 不关 buffering 的症状:token 全部憋到生成结束一次性出现,"流式"失效
}
第五章:Element Plus 与工程化名词
5.1 Element Plus 常用组件(能说出用途即可)
| 组件 | 用途 | 你 RAG 项目里的场景 |
|---|---|---|
el-table + el-pagination |
表格 + 分页 | 文档管理列表(后端 PageHelper/GaussDB 分页配合) |
el-upload |
文件上传(multipart/form-data) | PDF/Word/Excel 上传,前端限制类型和大小 |
el-input + v-model |
表单输入 | 聊天输入框 |
el-dialog |
弹窗 | 文档详情、删除确认 |
el-message / el-message-box |
轻提示/确认框 | “上传成功”、删除前确认 |
el-skeleton |
骨架屏 | 文档列表加载占位 |
上传组件对接点(后端视角):
<el-upload
action="/api/documents/upload"
:headers="{ Authorization: 'Bearer ' + token }"
:before-upload="beforeUpload"
:on-success="onSuccess">
<el-button type="primary">上传文档</el-button>
</el-upload>
<script setup>
function beforeUpload(file) {
const okTypes = ['application/pdf', 'application/msword',
'application/vnd.openxmlformats-officedocument...'];
if (!okTypes.includes(file.type)) { ElMessage.error('仅支持 PDF/Word/Excel'); return false; }
if (file.size > 20 * 1024 * 1024) { ElMessage.error('最大 20MB'); return false; }
return true;
}
</script>
后端对应:@PostMapping("/upload") @RequestParam("file") MultipartFile file → Tika 解析 → 分块 → 向量化入 pgvector。
5.2 前端工程化名词(扫盲,防止被问到懵)
| 名词 | 一句话解释 |
|---|---|
| Vite | Vue3 官方推荐的构建工具(替代 Webpack),启动/热更新基于原生 ES Module,秒级 |
| npm / pnpm | 前端包管理器(类比 Maven);npm install ≈ mvn install |
| node_modules | 前端依赖目录(类比 .m2 本地仓库,但装在项目里) |
| npm run dev / build | 启动开发服务器(热更新)/ 打包成 dist 静态文件 |
| TypeScript | JavaScript 超集,带静态类型;接口返回值可以定义 interface 和后端对齐 |
| ESLint / Prettier | 前端代码规范工具(类比后端 Checkstyle/Spotless) |
| 路由 vue-router | SPA 的页面映射;/chat→对话页,/docs→文档管理页 |
| Pinia | Vue3 官方状态管理(替代 Vuex),存全局共享状态 |
| .env.development / .env.production | 多环境配置(类比 application-{profile}.yml) |
第六章:面试速查表
Vue3 核心
| 考点 | 一句话答案 |
|---|---|
| Vue 核心思想 | 数据驱动视图(声明式),改数据不改 DOM |
| Vue2 vs Vue3 响应式 | defineProperty 逐属性劫持(新增属性不响应)vs Proxy 整对象代理(全能+懒代理) |
| ref vs reactive | ref 包装任意值用 .value;reactive 只包对象直接点属性,整体替换丢响应式 |
| computed vs watch | computed 算新值有缓存;watch 执行副作用(调接口) |
| v-model | 双向绑定,input 事件 + value 属性的语法糖 |
| props 单向数据流 | 子组件不能直接改 props,要 emit 事件让父组件改 |
| 组件通信 | 父子 props/emit、跨级 provide/inject、全局 Pinia |
| 生命周期关键点 | onMounted 初始化调接口;onUnmounted 清理 SSE/定时器/监听 |
| SPA | 单页应用,vue-router 局部切换视图,不整页刷新 |
前后端协作(后端重点)
| 考点 | 一句话答案 |
|---|---|
| 跨域本质 | 浏览器同源策略,不是后端拒绝;Postman 无跨域 |
| 解决方案 | 开发 Vite 代理 / 生产 Nginx 同域反代 / 后端 CORS 白名单 |
| 预检请求 | 非简单请求先发 OPTIONS 试探;Access-Control-Max-Age 缓存预检 |
| Axios 拦截器 | 请求带 token,响应统一处理 401 跳登录/错误提示 |
| RESTful | 资源名词+HTTP 方法;动作类接口(/chat)语义优先 |
SSE 流式(你项目最大亮点)
| 考点 | 一句话答案 |
|---|---|
| SSE 定义 | HTTP 长连接上服务器单向推文本流(text/event-stream) |
| SSE vs WebSocket | 单向文本过网关友好自动重连 vs 全双功独立协议;LLM 一问一答选 SSE |
| 帧格式 | event: 类型 / data: 数据 / 空行(\n\n)分隔事件 / retry 重连间隔 |
| 为什么不用 EventSource | 只支持 GET、带不了 Authorization,用 fetch + ReadableStream |
| 中文乱码 | 多字节字符被 chunk 切断,用 TextDecoder stream:true 解码 |
| Nginx 关键项 | proxy_buffering off(关缓冲立即推送)+ 长 read_timeout |
| 用户关页面 | emitter.send 抛 IOException → 取消上游 LLM 请求(省 token 钱) |
| 后端核心类 | SseEmitter + 流式 handler(onPartialResponse 逐 token 推送) |
本文档对应《简历技术栈学习指南》第九阶段,最后更新:2026-08-20