news 2026/7/19 9:52:10

从BFF到SSE:我在Vue项目里藏了个“AI翻译官”

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
从BFF到SSE:我在Vue项目里藏了个“AI翻译官”

当大模型流式输出遇上BFF架构,前端终于可以“躺平”了

前言:一个前端开发的“非分之想”

“咱们要加一个AI聊天功能,而且要流式输出,就像ChatGPT那样打字机效果。”产品经理轻描淡写地扔过来一句话,我却盯着屏幕上DeepSeek的API文档陷入了沉思。

直接在前端调接口?API Key赤裸裸地暴露在浏览器里,等于把保险柜密码贴在门上。fetch硬接流式数据?那意味着我要在前端手动处理ReadableStream、解码二进制、解析data:前缀、拼接碎片化文本……光是想想这些,血压就上来了。

更麻烦的是,产品经理要求的不只是一个“能跑的Demo”,而是一个可维护、可扩展的企业级功能。如果将来要换模型、要加鉴权、要做日志监控,难道每次都要改前端代码、重新打包发布吗?

这不合理。

我理想中的方案是:前端只用关心UI和用户交互,所有跟“流”相关的脏活累活,全部交给一个中间层去搞定。这个中间层能藏住密钥、能转发流式数据、还能随时扩展新能力——说白了,就是给前端配一个“御用翻译官”。

于是,BFF(Backend For Frontend)走进了我的视野。当BFF碰上SSE(Server-Sent Events),所有问题都迎刃而解。

核心思路:BFF负责“翻译”LLM的流式方言,前端只需听懂“标准普通话”。

一、BFF:大前端的“御用翻译官”

1.1 什么是BFF?

BFF全称Backend For Frontend,翻译过来就是“为前端服务的后端”。

常规架构里,前端直接调后端的Java/Go接口,但有时候后端接口设计是为通用业务服务的,不太“体贴”前端的特定场景。比如:

  • 后端返回的数据结构字段太多,前端用不上
  • 需要聚合多个接口的数据
  • 需要处理一些特殊协议(比如WebSocket、SSE)

这时候BFF层就登场了:

前端(Vue/React) → Node(BFF) → 后端(Java/Go/LLM)

BFF层由前端团队维护,前端需要什么数据格式,BFF就给什么格式。这就像前端在Java大后端面前配了个“自己人”,好说话。

1.2 为什么BFF适合做流式输出中转?

流式输出(SSE/Streaming)对前端来说有几个痛点:

  • 二进制流对象需要解码
  • 数据格式需要解析(比如SSE的data:前缀)
  • 错误处理需要兼容多种异常情况
  • 连接状态需要管理

把这些复杂逻辑塞到前端,既增加了打包体积,又让代码难以维护。

把脏活累活交给BFF,前端只需要一个fetch,拿到处理好的数据就行。这就是BFF的“翻译官”价值。

BFF不是多此一举,而是把复杂性留在服务端,把简单留给前端。

二、实战:从0搭一个BFF流式服务

我们先用Vite创建一个Vue项目,然后在项目根目录下建一个server.mjs,这就是我们的BFF服务器。

2.1 初始化BFF服务

// server.mjs import * as dotenv from 'dotenv'; import express from 'express'; // 加载环境变量(API Key放这里) dotenv.config({ path: ['.env.local', '.env'] }); const app = express(); const port = 3000; // 健康检查路由 app.get('/', (req, res) => { res.send('BFF Server is running 🚀'); }); app.listen(port, () => { console.log(`BFF 服务已启动: http://localhost:${port}`); });

这段代码在做什么?

  • dotenv.config():加载.env.env.local中的环境变量,我们会在里面存放VITE_DEEPSEEK_API_KEY,这样API Key就只存在于服务端,前端无法窥探。
  • express:最轻量的Node框架,用来快速搭建HTTP服务。
  • app.get('/'):定义一个健康检查路由,方便验证服务是否正常。
  • app.listen(3000):BFF独立监听3000端口,与前端Vite服务(默认5173)完全隔离。

注意server.mjs和前端项目在同一个代码仓库,但运行时是两个独立进程。前端工程化工具(Vite)负责编译和热更新,BFF负责业务逻辑转发。两者各司其职,互不干扰。

启动方式:

# 终端1:启动BFF node server.mjs # 终端2:启动前端 npm run dev

2.2 核心:流式转发路由

接下来是重头戏——/stream路由。它要做三件事:

  1. 接收前端的prompt参数
  2. 带着API Key去请求DeepSeek的流式接口
  3. 把流式数据实时透传给前端

我们按照用户给定的代码来搭建:

// server.mjs 继续添加 app.get('/stream', async (req, res) => { // prompt req 解析 // fetch deepseek stream: true // llm // console.log(req.query.prompt); // res.json({ // prompt: req.query.prompt, // }) const { prompt } = req.query; const endpoint = 'https://api.deepseek.com/v1/chat/completions'; try { const response = await fetch(endpoint, { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.VITE_DEEPSEEK_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'deepseek-v4-flash', messages: [ { role: 'user', content: prompt, } ], stream: true, }), }) console.log(response.body); // ReadableStream } catch (err) { console.error('fetch 失败:', err.message); } })

逐段解析:

  1. 接收参数
    const { prompt } = req.query从URL查询字符串中获取用户输入。前端请求/stream?prompt=你好,这里就能拿到prompt='你好'

  2. 构造请求DeepSeek

    • endpoint:DeepSeek的聊天补全API地址。
    • headers:通过Authorization传递API Key,从环境变量读取,前端永远拿不到这个Key,保证了安全性。
    • body:设置modeldeepseek-v4-flashmessages为用户消息,最关键的是stream: true——这告诉DeepSeek以流式方式返回数据。
  3. 发起fetch请求
    await fetch(endpoint, {...})会向DeepSeek发送HTTP请求。由于stream: true,服务器不会一次性返回完整JSON,而是分块发送SSE格式的数据流。

  4. 拿到响应流
    response.body是一个ReadableStream对象,它代表了从DeepSeek接收到的原始字节流。这里我们只是用console.log打印了它,实际上我们还需要读取这个流、解析SSE格式、并将数据逐块写回给前端。目前这段代码只是“打通了”BFF到LLM的连接,但还没有把数据转发给前端。

  5. 错误捕获
    如果网络超时、API Key无效或DeepSeek服务异常,fetch会抛出错误,我们捕获后打印日志。但这里也没有把错误信息返回给前端,前端会一直等待。

流式输出的本质是“边生产边消费”,BFF就是那个传送带上的分拣员。

三、跨域?Vite代理一把梭

3.1 跨域是怎么产生的?

浏览器有一个同源策略:协议、域名、端口三个完全相同才算“同源”,否则任何fetchXMLHttpRequest请求都会触发跨域限制。

我们的场景:

  • 前端服务:http://localhost:5173(Vite默认)
  • BFF服务:http://localhost:3000(Express)

端口不同(5173 vs 3000),所以前端直接请求http://localhost:3000/stream会被浏览器拦截,报CORS错误。

为什么浏览器要这么“多管闲事”?为了防止恶意网站窃取用户数据。如果任意网站都能随意请求其他域名的接口,那你的银行账户可能就危险了。

3.2 解决跨域的常见方式

方式一:CORS(跨域资源共享)
在BFF服务端设置响应头:

res.setHeader('Access-Control-Allow-Origin', '*');

简单粗暴,但生产环境如果BFF暴露在外网,允许所有来源存在风险。如果允许携带凭证(Cookie),还需要设置Access-Control-Allow-Credentials: true,且不能使用*,必须指定具体域名,同时浏览器会发送预检请求(OPTIONS),增加复杂度。

方式二:JSONP
只支持GET请求,且存在安全风险,现在基本弃用。

方式三:反向代理
在开发环境用Vite代理,生产环境用Nginx。这是最推荐的方式,因为它对前端完全透明,且不需要后端额外配置CORS。

3.3 Vite代理的原理

Vite开发服务器内置了一个HTTP代理功能,它的工作方式类似于Nginx的反向代理:

  1. 前端请求一个同源的地址,比如/api/stream(相对路径,默认同源5173)。
  2. Vite开发服务器接收到这个请求后,发现/api前缀匹配了代理规则。
  3. Vite作为“中间人”,代替前端向真正的目标服务器(http://localhost:3000)发起请求。
  4. 拿到响应后,Vite再原样返回给前端。

对浏览器来说,它始终在和localhost:5173通信,不存在跨域。跨域被“消化”在了服务器之间,而服务器之间的请求不受同源策略限制。

3.4 代理配置详解

// vite.config.js import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ plugins: [vue()], server: { proxy: { '/api': { target: 'http://localhost:3000', // BFF服务地址 changeOrigin: true, rewrite: (path) => path.replace(/^/api/, ''), // 去掉/api前缀 } } } });

每个配置项的含义:

  • '/api':匹配所有以/api开头的请求路径,例如/api/stream/api/chat
  • target:代理转发的目标地址,这里指向BFF的3000端口。
  • changeOrigin: true:修改请求头中的Origin字段为目标地址的Origin。有些后端服务会校验Origin,开启这个选项可以避免被拒绝。如果不开启,Origin仍然是localhost:5173,BFF可能因为不信任该来源而拒绝。
  • rewrite:路径重写函数。我们想把/api/stream转发到BFF的/stream(因为BFF没有/api前缀),所以用正则去掉/api

工作流程示例:

前端请求: /api/stream?prompt=hello ↓ (Vite拦截) 代理转发: http://localhost:3000/stream?prompt=hello ↓ (BFF响应) BFF返回: 流式数据 ↓ (Vite透传) 前端接收: 同源数据 ✅

3.5 为什么不直接在BFF配置CORS?

你可能会想:我在BFF里加一行res.setHeader('Access-Control-Allow-Origin', '*')不就解决了?

当然可以,但有以下弊端:

  • 生产环境风险:生产环境中,BFF可能部署在独立域名下,如果直接允许所有来源(*),存在安全隐患,容易遭受CSRF攻击。
  • 需要额外配置:如果BFF要支持携带Cookie等凭证,CORS配置会更复杂(需要指定具体域名、处理预检请求等),代码臃肿。
  • 多环境不一致:开发环境加CORS,生产环境可能又需要去掉,容易出错。

相比之下,Vite代理只在开发环境生效,生产环境我们通常会使用Nginx或云服务商的网关来做反向代理,策略统一。开发环境用Vite代理,生产环境用Nginx,两种环境保持一致的转发逻辑,减少心智负担。

金句:Vite代理就像给前端配了个“通关文牒”,跨域?不存在的。

3.6 生产环境怎么办?

生产环境部署时,你的前端静态文件(打包后的dist)通常由Nginx托管,BFF服务可能运行在另一个端口或另一个容器。

此时在Nginx中配置反向代理:

location /api/ { proxy_pass http://localhost:3000/; # 注意末尾的/会去掉前缀 proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; # 针对SSE流式,需要禁用缓冲 proxy_buffering off; proxy_cache off; }

关键点:

  • proxy_pass末尾的/会去掉匹配的前缀/api/,所以/api/stream会被转发到/stream
  • proxy_buffering off非常重要:对于流式输出,Nginx默认会缓冲响应,等到完整接收后再发给客户端,这会破坏SSE的实时性。关闭缓冲后,数据会立即透传给前端。

这样,开发和生产环境的请求路径都是/api/stream,前端代码无需任何修改。

最后:BFF不是银弹,但它是把好刀

BFF不是万能的,它增加了部署成本和维护复杂度。但在以下场景,它物超所值

  • ✅ 需要对接多个第三方API
  • ✅ 需要处理特殊协议(SSE/WebSocket)
  • ✅ 需要隐藏敏感信息(API Key)
  • ✅ 前端需要定制化数据格式

如果你的项目符合以上任意一条,BFF值得拥有

回到开头的故事:用了BFF之后,前端代码从200行精简到30行,产品经理满意,后端同事也不用为了我的小需求改接口。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/7/19 9:51:55

免冠照片拍摄全攻略:规范要求、实拍技巧、修图方法全覆盖

2026年各类证件办理、考试报名、求职入职、学籍登记等场景,均需要合规的免冠证件照。很多人拍摄的照片频繁审核失败,核心原因是不了解官方统一的拍摄规范,在服装、妆容、背景、仪容细节上出现疏漏。免冠照片拍摄有明确的标准化要求&#xff0…

作者头像 李华
网站建设 2026/7/19 9:51:13

2026年施工动画制作公司排名

一、行业背景:施工动画市场突破12亿,专业能力成核心竞争力 2026年,施工动画已成为工程投标、施工交底和安全培训的核心工具。行业数据显示,国内建筑工程可视化动画市场规模突破12亿元,同比增长23.7%,其中施…

作者头像 李华
网站建设 2026/7/19 9:51:04

鲸剪 WhaleClip怎么样?5款剪气口工具对比横评

口播视频停顿太多,完播率上不去怎么办做口播、知识分享、课程拆条的创作者,大概率都遇到过同一个问题:录完一段 10 分钟的素材,实际能用的干货只有 6 分钟,剩下的全是「嗯、啊、那个」和换气停顿。手动把这些气口一帧帧…

作者头像 李华
网站建设 2026/7/19 9:48:05

向打工搬砖人致敬!

向打工搬砖人致敬 996是打工人的归宿。 <!DOCTYPE html> <html><head><meta charset"utf-8" /><meta name"viewport" content"widthdevice-width, initial-scale1" /><title>每日搬砖倒计时</title>&…

作者头像 李华
网站建设 2026/7/19 9:45:36

XUnity.AutoTranslator:3分钟让Unity游戏跨越语言障碍的终极指南

XUnity.AutoTranslator&#xff1a;3分钟让Unity游戏跨越语言障碍的终极指南 【免费下载链接】XUnity.AutoTranslator 项目地址: https://gitcode.com/gh_mirrors/xu/XUnity.AutoTranslator 你是否曾因为语言不通而错过心仪的游戏大作&#xff1f;当游戏中的对话、菜单…

作者头像 李华
网站建设 2026/7/19 9:44:46

Avalonia UI:跨平台.NET框架核心技术与实战解析

1. Avalonia UI&#xff1a;跨平台.NET UI框架深度解析Avalonia作为当前.NET生态中最受关注的跨平台UI框架&#xff0c;正在彻底改变C#开发者构建桌面、移动和Web应用的方式。如果你还在为WPF应用的平台局限性而苦恼&#xff0c;或正在评估下一代.NET UI技术栈&#xff0c;这份…

作者头像 李华