摘要
Vue3通过Express BFF层安全调用DeepSeek,Vite代理解决跨域。拆解BFF概念、SSE流式转发、跨域代理,展示大前端用Node中间层保护API Key并简化前端复杂度的实战路径。
为什么需要BFF
纯前端调用LLM API存在两个硬伤:API Key暴露在浏览器端,右键查看源码就能拿到;SSE流式输出的二进制流解码、data: 协议解析、断句缓冲等逻辑堆在前端,代码臃肿且难以维护。
BFF(Backend For Frontend)把这些问题收敛到一层Node中间层。前端只负责发请求和渲染,BFF负责持有API Key、处理流式解码、转发响应。架构关系如下:
前端(Vue3/React) → Node(BFF) → LLM服务器(DeepSeek)
无Key 有Key 验证Key
(预留图片位置:BFF三层架构图——前端5173端口 → BFF 3000端口 → DeepSeek API,标注每层职责和Key存在状态)
大前端工程师自己写Node服务来满足自身需求,而不是等后端团队改接口。Vite创建的Vue项目本身就包含Node生态(package.json、node_modules),天然适合嵌入BFF开发。
项目结构一览
stream-bff/
├── index.html # Vite入口
├── package.json # vue3 + express + dotenv
├── vite.config.js # 代理配置
├── server.mjs # BFF服务(Express)
├── .env.local # API Key等敏感信息
├── .gitignore # 忽略node_modules、*.local
└── src/
├── main.js # createApp挂载
├── App.vue # 前端组件
├── HelloWorld.vue # 模板组件
└── style.css # 全局样式
启动链路分两条:
# 终端1:启动BFF后端服务
node server.mjs # 监听3000端口
# 终端2:启动Vite前端开发服务器
npm run dev # 监听5173端口
两条进程各自独立,通过Vite代理串联。package.json 中的关键依赖:
{
"dependencies": {
"dotenv": "^17.4.2",
"express": "^5.2.1",
"vue": "^3.5.34"
},
"devDependencies": {
"@vitejs/plugin-vue": "^6.0.6",
"vite": "^8.0.12"
}
}
dotenv 负责从 .env.local 加载环境变量到 process.env,express 提供HTTP服务能力,vue 作为前端框架。
Express BFF层:安全的API Key管理
server.mjs 是整个BFF架构的核心,完整代码如下:
import * as dotenv from 'dotenv'
import express from 'express'
// 从 .env.local 加载环境变量到 process.env
dotenv.config({
path: ['.env.local', '.env']
})
const app = express()
const port = 3000
// 根路由:验证服务可用
app.get('/', (req, res) => {
res.send('Hello World!')
})
// 流式输出的BFF层路由
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}`
},
body: JSON.stringify({
model: 'deepseek-v4-flash',
stream: true,
messages: [{ role: 'user', content: prompt }]
})
})
// response.body 是 DeepSeek 返回的 ReadableStream
// BFF可直接管道化传递给前端
console.log(response.body)
} catch (err) {
// 错误处理
}
})
app.listen(port, () => {
console.log(`服务器在${port}端口启动了`)
})
关键变化在于API Key的读取方式:从 import.meta.env.VITE_DEEPSEEK_API_KEY(前端,打包时内联到JS)变成了 process.env.VITE_DEEPSEEK_API_KEY(Node进程,不暴露给浏览器)。环境变量定义在 .env.local:
VITE_DEEPSEEK_API_KEY=sk-xxxxxxxxxxxxxxxx
VITE_DEEPSEEK_API_BASE_URL=https://api.deepseek.com/v1
VITE_DEEPSEEK_MODEL=deepseek-v4-flash
dotenv.config() 从文件加载到 process.env,配合 .gitignore 中的 *.local 规则,确保敏感信息不会被提交到Git仓库。
SSE流式:为什么放到BFF层
SSE(Server-Sent Events)是服务器向客户端推送事件的协议。DeepSeek开启 stream: true 后,不再是等全部生成完再一次性返回JSON,而是每生成一个token就推送一行:
data: {"choices":[{"delta":{"content":"中"}}]}
data: {"choices":[{"delta":{"content":"国"}}]}
data: [DONE]
如果这些逻辑全部放在前端Vue组件里,需要处理:二进制流解码(TextDecoder)、按行分割并过滤 data: 前缀、JSON截断断句的buffer缓冲、[DONE] 信号终止。每个环节都可能出错,且与业务逻辑混在一起。
BFF层的思路是把这些"脏活"抽象到Node端。前端只需要一行:
fetch('/api/stream?prompt=hello')
.then(res => res.json())
.then(data => {
console.log(data)
})
前端的复杂度被大幅降低——不再需要关心 ReadableStream、TextDecoder、buffer断句。BFF层负责和LLM服务器打交道,前端只负责发请求和渲染结果。这是一种"把复杂留给自己,把简单留给前端"的设计哲学。
Vite代理:无感解决跨域
浏览器环境下,前端(5173端口)直接请求BFF(3000端口)会触发同源策略——域名、端口、协议只要有一个不同,fetch就会报跨域错误。
Vite开发服务器内置了代理能力。只需在 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',
secure: false,
rewrite: path => path.replace(/^\/api/, ''),
}
}
}
})
请求链路拆解:
- 前端发请求:
fetch('/api/stream?prompt=hello')— 目标是同源的/api/stream,不跨域 - Vite拦截:识别到
/api前缀,触发代理规则 - 路径改写:
/api/stream→/stream(去掉/api前缀) - 转发到BFF:
http://localhost:3000/stream,BFF处理请求
整个过程对前端开发者完全透明。/api 作为请求后端的标志,Vite自动拦截并转发,不需要手动配置CORS头,也不需要在前端代码中写死绝对路径。
完整请求链路
把前端、Vite代理、BFF、LLM API串起来,一次完整的流式对话请求走如下路径:
index.html
└─→ main.js (createApp挂载)
└─→ App.vue (fetch /api/stream)
└─→ Vite代理 (拦截/api,改写路径,转发到localhost:3000)
└─→ server.mjs (Express路由 /stream)
└─→ fetch DeepSeek API (携带API Key)
└─→ 流式响应返回
└─→ 逐层回传至App.vue渲染
index.html 是Vite的入口HTML,通过 <script type="module" src="/src/main.js"> 加载Vue应用。main.js 三行代码完成挂载:
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'
createApp(App).mount('#app')
App.vue 中,前端只需关注两件事:发请求和展示结果。v-model 做双向绑定,@click 绑定提交事件,{{content}} 渲染LLM返回的内容。
<script setup>
fetch('/api/stream?prompt=hello')
.then(res => res.json())
.then(data => {
console.log(data)
})
</script>
<template>
<div class="container">
<div>
<label>输入:</label>
<input type="text" class="input" v-model="question">
<button @click="update">提交</button>
</div>
<div class="output">
<div>
<label>Streaming</label>
<input type="checkbox" v-model="stream" />
<div>{{content}}</div>
</div>
</div>
</div>
</template>
核心收获
这个项目的本质是一次"职责分离"的实践。BFF层的引入,让三个角色各司其职:
前端(Vue3) 只关心用户交互和数据展示,不再触碰二进制流、data: 协议解析、API Key管理等底层细节。
BFF层(Express) 承担所有与LLM服务器的通信逻辑——持有API Key、发起流式请求、处理响应管道。它既是"安全网关"(Key不暴露),也是"复杂度缓冲"(脏活累活由它做)。
Vite代理 在开发阶段解决跨域问题,让前端可以用同源路径访问BFF,无需手动处理CORS。
一个Vue3项目,一个Express文件,几行Vite配置,就搭建起了一个安全、简洁、可维护的AI对话应用架构。这不是大厂专属的重型方案,而是大前端工程师在日常开发中随手可用的"轻量级最佳实践"。
本文基于Vue3 + Vite + Express + DeepSeek API的完整项目实践,所有代码均来自实际项目文件。从BFF概念到跨域代理,完整展示了前端工程师如何用Node中间层提升AI应用的安全性和可维护性。