前端基础 — 小白讲解 + 面试题精解

第九阶段:前端基础(全栈协作)— 讲解与面试题

本文档定位: 对应《简历技术栈学习指南》第九阶段。你简历中写的是“技术栈:Spring Boot + LangChain4j + Redis + PostgreSQL(pgvector) + Vue3 + DeepSeek API”,技能清单写“了解 Vue.js/Element UI 前端技术,了解 Node.js/TypeScript”。你是后端工程师,面试官不会深问前端,但会确认两件事:① 你能看懂/改动前端代码;② 你能把前后端对接的细节(尤其是 SSE 流式、跨域、Token)讲清楚。

使用方法: 每个知识点按 小白讲解 → 名词详解(举例) → 结合简历案例 → 代码示例 → 面试题 组织。作为后端,重点掌握前四章 + SSE 章。


目录


第一章: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 installmvn 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