作者:互联网 时间: 2026-07-21 08:58:14
最近在做一个AI对话相关的项目,涉及到流式输出的场景。前端需要调用大模型API,但直接在前端调用会遇到两个问题:一是API Key暴露的安全风险,二是跨域问题。这篇文章就记录一下我是如何通过BFF层来解决这些问题的。

BFF(Backend For Frontend),直译过来就是“为前端服务的后端”。这个概念在微服务架构中很常见,但在大前端领域同样适用。
传统模式下,前端直接调用后端Java/Go服务提供的RESTful接口,实现数据的增删改查。但随着前端业务越来越复杂,特别是涉及到流式输出、二进制数据处理等场景时,直接让前端处理这些逻辑会让代码变得臃肿且难以维护。
于是我们可以在前端和后端之间加一层Node服务:
text
前端(Vue/React) → Node(BFF) → 后端(Java/LLM)
这一层Node服务可以帮我们做很多事情:
SSE是一种基于HTTP的服务器推送技术,允许服务器向客户端推送事件流。与WebSocket不同,SSE是单向的,只能由服务器向客户端发送数据。
在AI对话场景中,大模型通常以流式方式返回结果,SSE正是处理这种场景的理想方案。
text
stream-bff/├── src/│ ├── App.vue# 前端主组件│ └── main.js# Vue入口├── server.mjs # BFF后端服务(Express)├── vite.config.js # Vite配置(含袋里)├── .env.local # 环境变量(API Key)├── package.json # 项目依赖└── readme67.md# 笔记文档
bash
pnpm add express dotenv
在.env.local中存放敏感信息:
env
VITE_DEEPSEEK_API_KEY=your_api_key_here
注意:.env.local应该添加到.gitignore中,避免API Key泄露。
跨域问题的本质是浏览器的同源策略:只要域名、端口、协议任意一个不同,就会触发跨域限制。
vite.config.js中配置袋里来解决跨域:
javascript
import { defineConfig } from 'vite'import vue from '@vitejs/plugin-vue'export default defineConfig({plugins: [vue()],server: {proxy: {'/api': {target: 'http://localhost:3000',secure: false,rewrite: (path) => path.replace(/^/api/, '')}}}})
这样,前端请求/api/stream会被Vite袋里转发到http://localhost:3000/stream,完美绕过了跨域问题。
server.mjs是BFF层的核心:
javascript
import * as dotenv from 'dotenv';import express from 'express';dotenv.config({path: ['.env.local', '.env']});const app = express();const port = 3000;app.get('/', (req, res) => {res.send('hello world');});// 流式输出接口app.get('/stream', async (req, res) => {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',stream: true,messages: [{ role: 'user', content: prompt }]})});// response.body 是 ReadableStream// 这里可以进行流式数据的处理和转发console.log(response.body);} catch (err) {console.log(err);}});app.listen(port, () => {console.log(`服务器在${port}端口启动了`);});
text
前端:5173/api/stream → Vite袋里转发 → BFF:3000/stream → DeepSeek API
App.vue中的核心逻辑:
vue
<script setup>import { ref } from 'vue';const question = ref('');const content = ref('');const stream = ref(true);const update = async () => {// 发送请求到BFF层fetch(`/api/stream?prompt=${question.value}`).then(res => res.json()).then(data => {console.log(data);});}</script><template><div class="container"><div><label>输入:</label><input class="input" v-model="question" /><button @click="update">提交</button></div><div class="output"><div><label>Streaming</label><input type="checkbox" v-model="stream" /></div><div>{{ content }}</div></div></div></template><style>.container {display: flex;flex-direction: column;align-items: start;justify-content: start;height: 100vh;font-size: 0.85rem;}.input {width: 200px;}.output {margin-top: 10px;min-height: 300px;width: 100%;text-align: left;}button {padding: 0 10px;margin-left: 6px;}</style>
项目需要同时启动两个服务:
bash
# 终端1:启动Vite开发服务器(前端)npm run dev# 终端2:启动BFF服务(后端)node server.mjs
配置袋里后如果遇到502,通常是因为:
前端代码中的环境变量只有在构建时才会被替换,如果直接在前端调用大模型API,用户可以通过查看源代码获取到API Key。通过BFF层转发,API Key只存在于服务端,安全性大大提高。
通过BFF层的引入,我们实现了:
这种模式在微前端、大前端架构中非常实用,特别是当我们需要对接多个后端服务或第三方API时,BFF层的价值会更加明显。