前言
做Vue AI对话页面,90%新手会踩两个线上致命大坑:
- 前端直接请求DeepSeek接口:API Key打包进前端,抓包/查看源码直接泄露,被盗刷高额账单
- 纯前端手写SSE流式解析:TCP网络分包截断JSON,疯狂报parse错误、文字丢失、乱码
很多教程只给前端极简demo,完全不提安全风险,也没有完整BFF中转层落地代码。 本文带你搭建Vite Vue3 + Express BFF后端完整流式对话架构,读完你能学到:
- BFF层是什么、为什么AI项目必须加中转层
- 完整三层调用链路:Vue前端 → Node BFF → DeepSeek大模型
- Express转发SSE流式响应标准写法,处理二进制流透传
- 优化版前端流式解析代码,极简buffer容错逻辑
- Vite代理跨域配置,解决前后端端口跨域问题
- 全套可复制运行代码 + 高频踩坑清单,直接上线使用
一、先搞懂:BFF中转层解决两大核心痛点
1.1 安全痛点:前端不能存放大模型API密钥
Vite中VITE_开头环境变量会打包进前端产物,任何人打开开发者工具、抓网络请求都能拿到完整Key,存在被盗刷风险。
BFF解决方案:密钥只存在Node服务端.env,前端永远看不到真实鉴权凭证。
1.2 工程化痛点:统一接口、降低前端复杂度
- 没有BFF:前端要处理二进制解码、分片缓存、异常捕获、接口切换逻辑,代码臃肿
- 拥有BFF:统一封装LLM调用逻辑,前端只需要请求本地
/api/stream,后续切换大模型、加限流、过滤敏感词只改后端,前端零改动
1.3 完整三层调用链路
Vue页面(5173端口) → Vite代理转发 → Express BFF(3000端口) → DeepSeek官方API
二、项目环境初始化
2.1 安装依赖
# Vue前端依赖
npm install vue
# BFF后端依赖
npm install express dotenv
2.2 环境变量区分前后端
- 项目根目录
.env.local(BFF后端读取,不会暴露前端)
DEEPSEEK_API_KEY=sk-你的DeepSeek密钥
- 前端无需存放任何LLM密钥,彻底规避泄露风险
2.3 vite.config.js 代理跨域配置(关键)
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
proxy: {
// 匹配前端/api开头请求,转发到3000端口BFF服务
'/api': {
target: 'http://127.0.0.1:3000',
changeOrigin: true,
rewrite: path => path.replace(/^\/api/, '')
}
}
}
})
前端请求/api/stream,Vite自动转发为http://127.0.0.1:3000/stream,消除跨域报错。
三、完整BFF后端代码 server.js(Express流式转发)
import * as dotenv from 'dotenv';
import express from 'express';
dotenv.config({ path: ['.env', '.env.local'] });
const app = express();
const port = 3000;
// 解析url参数
app.use(express.urlencoded({ extended: true }));
// 基础测试路由
app.get('/', (req, res) => {
res.send('BFF服务运行正常,访问 /api/stream 发起AI对话');
});
// SSE流式中转核心接口
app.get('/stream', async (req, res) => {
const { prompt } = req.query;
if (!prompt) return res.status(400).send('prompt参数不能为空');
const endpoint = 'https://api.deepseek.com/chat/completions';
const apiKey = process.env.DEEPSEEK_API_KEY;
const model = 'deepseek-v4-flash';
// SSE标准响应头,必须设置,否则无法持续推送流
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
res.flushHeaders();
try {
// 请求DeepSeek流式接口
const llmRes = await fetch(endpoint, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model,
stream: true,
messages: [{ role: 'user', content: prompt }],
}),
});
// 把大模型返回的二进制流直接透传给前端
llmRes.body.pipe(res);
// 客户端关闭连接时销毁请求,避免资源占用
req.on('close', () => {
llmRes.body.destroy();
res.end();
});
} catch (error) {
console.error('LLM接口请求失败:', error);
res.write(`data: {"error":"服务异常"}\n\n`);
res.write(`data: [DONE]\n\n`);
res.end();
}
});
app.listen(port, () => {
console.log(`BFF中转服务启动成功,端口:${port}`);
});
后端核心逻辑说明
- SSE响应头三件套:告知浏览器保持长连接、禁用缓存,持续接收数据流
pipe(res):二进制流直接透传,无需后端解析JSON,性能更高- 密钥存放服务端环境变量,前端完全无法获取
- 监听客户端断开事件,主动销毁流,防止后台无效请求堆积
四、Vue3前端完整代码 App.vue(优化版流式解析)
<script setup>
import { ref } from 'vue'
// 页面响应式状态
const question = ref('讲一个中国龙的故事')
const stream = ref(true) // 是否开启打字机流式输出
const content = ref('')
// 存储完整回答,用于关闭流式时一次性渲染
let accumulatedText = ''
// 核心请求函数
async function update() {
if (!question.value.trim()) return
content.value = ''
accumulatedText = ''
// 请求本地BFF代理接口,不会跨域、无密钥泄露
const url = `/api/stream?prompt=${encodeURIComponent(question.value)}`
const response = await fetch(url)
const reader = response.body.getReader()
const decoder = new TextDecoder('utf-8', { stream: true })
let buffer = ''
while (true) {
const { done, value } = await reader.read()
if (done) break
// 二进制转文本,stream:true兼容跨分片中文
buffer += decoder.decode(value, { stream: true })
// 按换行分割所有报文
const lines = buffer.split('\n')
// 最后一行大概率是不完整分片,存入buffer下次拼接
buffer = lines.pop() || ''
// 逐行解析有效data报文
for (const line of lines) {
if (!line.startsWith('data: ')) continue
const dataStr = line.slice(6)
// 流结束标识,直接跳过
if (dataStr === '[DONE]') continue
try {
const json = JSON.parse(dataStr)
// 提取增量文字
const deltaText = json.choices?.[0]?.delta?.content || ''
accumulatedText += deltaText
// 流式开启则实时更新页面,打字机效果
if (stream.value) {
content.value = accumulatedText
}
} catch (err) {
// JSON解析失败直接忽略,残缺片段已存入buffer
continue
}
}
}
// 关闭流式:全部接收完成后一次性渲染全文
if (!stream.value) {
content.value = accumulatedText
}
}
</script>
<template>
<div class="container">
<!-- 提问输入区 -->
<div>
<label>输入提问:</label>
<input class="input" v-model="question" placeholder="输入你的问题" />
<button @click="update">提交AI问答</button>
</div>
<!-- 流式开关 + AI回答展示 -->
<div class="output">
<div style="margin-bottom:8px;">
<label>开启Streaming流式打字机:</label>
<input type="checkbox" v-model="stream" />
</div>
<div style="white-space: pre-wrap;">{{ content }}</div>
</div>
</div>
</template>
<style scoped>
.container {
display: flex;
flex-direction: column;
align-items: flex-start;
justify-content: flex-start;
height: 100vh;
font-size: 0.85rem;
padding: 20px;
}
.input {
width: 300px;
padding: 4px 8px;
}
.output {
margin-top: 12px;
min-height: 300px;
width: 100%;
text-align: left;
line-height: 1.7;
}
button {
padding: 4px 12px;
margin-left: 8px;
cursor: pointer;
}
</style>
前端流式优化点(对比纯前端直连方案)
- 请求地址统一代理:只访问本地
/api/stream,不暴露第三方LLM接口 - 极简buffer容错:分割后把最后一行残缺报文放回buffer,逻辑更简洁稳定
TextDecoder({stream:true}):解决跨分片中文乱码问题- 分离
accumulatedText完整缓存,支持一键切换流式/一次性渲染 - 增加输入空值拦截,避免无效请求
五、完整运行步骤
- 新建
.env.local,填入DeepSeek密钥 - 终端启动BFF后端服务
node server.js
- 新开终端启动Vite前端
npm run dev
- 打开页面输入提问,勾选/取消Streaming测试两种输出模式
六、开发高频踩坑清单(必看)
坑1:前端直接请求LLM接口,密钥泄露被盗刷
现象:上线后账单暴涨,开发者工具Network面板可见完整Authorization密钥 解决方案:全部请求走BFF中转,密钥仅存Node服务端环境变量
坑2:BFF未设置SSE响应头,流式一次性返回
现象:页面等待全部生成后才展示文字,无逐字打字效果
修复:必须添加text/event-stream、no-cache、keep-alive三个响应头
坑3:Vite代理502跨域报错
原因:BFF后端未启动、端口不匹配、代理rewrite路径错误 检查:确认BFF运行在3000端口,vite代理配置正确
坑4:TCP分包截断JSON,控制台频繁parse报错
根源:网络分片把一条data: JSON拆成两段
解决:代码中buffer缓存末尾残缺行,下一轮循环拼接完整再解析
坑5:中文跨分片出现乱码问号
修复:new TextDecoder('utf-8', { stream: true })开启流式解码
坑6:切换流式开关文字错乱、重复叠加
优化:每次请求重置accumulatedText完整缓存,分开实时渲染与最终渲染逻辑
七、两种架构方案对比
| 方案 | 前端直连LLM | BFF中转架构(本文方案) |
|---|---|---|
| 密钥安全 | 极低,极易泄露 | 极高,密钥仅存服务端 |
| 接口维护 | 切换模型需修改全部前端代码 | 仅修改后端,前端无感知 |
| 前端复杂度 | 高,需处理鉴权、流、异常 | 低,仅处理本地流式解析 |
| 扩展能力 | 无法加限流、敏感词过滤 | 后端统一加权限、限流、校验 |
| 线上风险 | 盗刷、接口暴露 | 安全可控,生产推荐 |
八、项目扩展优化方向
- 增加请求限流:后端引入
express-rate-limit,防止恶意刷接口 - POST传参替代URL拼接:避免长prompt参数超长,改用body传递提问
- 中断生成:搭配
AbortController,实现停止AI输出功能 - 多轮对话:扩展BFF接口接收完整messages数组,支持连续聊天
- 异常统一处理:后端捕获401/429/500错误,标准化错误报文返回前端
- Markdown渲染:流接收完成后使用marked解析富文本格式
九、总结
- 生产环境绝对禁止前端直连大模型API,BFF中转层是安全底线,杜绝密钥泄露盗刷;
- SSE流式输出核心依靠二进制长连接,
buffer缓存是解决TCP分片截断的必备容错逻辑; - Vite代理转发解决前后端跨域,Express通过
pipe透传LLM二进制流,性能最优; - 整套架构分层清晰:前端负责页面交互、BFF负责安全中转、大模型负责文本生成,可直接落地商用AI对话产品。