BFF架构实战:用Express+SSE为Vue3搭建安全的流式对话中间层

摘要

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、处理流式解码、转发响应。架构关系如下:

vbnet 复制代码
前端(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开发。


项目结构一览

bash 复制代码
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              # 全局样式

启动链路分两条:

bash 复制代码
# 终端1:启动BFF后端服务
node server.mjs           # 监听3000端口

# 终端2:启动Vite前端开发服务器
npm run dev               # 监听5173端口

两条进程各自独立,通过Vite代理串联。package.json 中的关键依赖:

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.envexpress 提供HTTP服务能力,vue 作为前端框架。


Express BFF层:安全的API Key管理

server.mjs 是整个BFF架构的核心,完整代码如下:

javascript 复制代码
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

ini 复制代码
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就推送一行:

css 复制代码
data: {"choices":[{"delta":{"content":"中"}}]}
data: {"choices":[{"delta":{"content":"国"}}]}
data: [DONE]

如果这些逻辑全部放在前端Vue组件里,需要处理:二进制流解码(TextDecoder)、按行分割并过滤 data: 前缀、JSON截断断句的buffer缓冲、[DONE] 信号终止。每个环节都可能出错,且与业务逻辑混在一起。

BFF层的思路是把这些"脏活"抽象到Node端。前端只需要一行:

javascript 复制代码
fetch('/api/stream?prompt=hello')
  .then(res => res.json())
  .then(data => {
    console.log(data)
  })

前端的复杂度被大幅降低------不再需要关心 ReadableStreamTextDecoder、buffer断句。BFF层负责和LLM服务器打交道,前端只负责发请求和渲染结果。这是一种"把复杂留给自己,把简单留给前端"的设计哲学。


Vite代理:无感解决跨域

浏览器环境下,前端(5173端口)直接请求BFF(3000端口)会触发同源策略------域名、端口、协议只要有一个不同,fetch就会报跨域错误。

Vite开发服务器内置了代理能力。只需在 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/, ''),
      }
    }
  }
})

请求链路拆解:

  1. 前端发请求:fetch('/api/stream?prompt=hello') --- 目标是同源的 /api/stream,不跨域
  2. Vite拦截:识别到 /api 前缀,触发代理规则
  3. 路径改写:/api/stream/stream(去掉 /api 前缀)
  4. 转发到BFF:http://localhost:3000/stream,BFF处理请求

整个过程对前端开发者完全透明。/api 作为请求后端的标志,Vite自动拦截并转发,不需要手动配置CORS头,也不需要在前端代码中写死绝对路径。


完整请求链路

把前端、Vite代理、BFF、LLM API串起来,一次完整的流式对话请求走如下路径:

scss 复制代码
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 三行代码完成挂载:

javascript 复制代码
import { createApp } from 'vue'
import './style.css'
import App from './App.vue'

createApp(App).mount('#app')

App.vue 中,前端只需关注两件事:发请求和展示结果。v-model 做双向绑定,@click 绑定提交事件,{{content}} 渲染LLM返回的内容。

html 复制代码
<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应用的安全性和可维护性。

相关推荐
沙蒿同学5 分钟前
我把架构约定编译成了会变红的测试:Wails v2 + Go + Vue3 桌面脚手架实战
前端·后端·github
cpolar技术支持8 分钟前
本地 Playwright 测试报告怎么远程复盘?Trace Viewer 跑起来后,用 cpolar 分享失败现场
前端·自动化测试·测试工具·cpolar·playwright
wordbaby8 分钟前
企业级后台管理系统路由设计与最佳实践指南
前端
胡志辉的博客17 分钟前
【完全开源】IP 纯净度检测 可一键部署到自己的CF
前端·javascript·chrome·ip·chromium
Hilaku44 分钟前
作为面试官,我最怕遇到什么样的候选人?
前端·javascript·程序员
TiDi1 小时前
Pinia优化重复请求
前端
web3d5201 小时前
01-用 Leafletjs 10 分钟搭一张水利一张图(Vue3 + Vite 实战)
前端·javascript
晚安日记wanna1 小时前
Vue2 的 defineProperty 差在哪四层追问筛掉九成候选人
前端·vue.js·面试
TiDi2 小时前
吸顶导航交互实现
前端
kisshyshy2 小时前
从Props透传到自定义Hook:系统梳理React跨层级通信与逻辑复用
前端·架构·代码规范