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

26 阅读5分钟

摘要

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)
    无KeyKey               验证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.envexpress 提供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)
  })

前端的复杂度被大幅降低——不再需要关心 ReadableStreamTextDecoder、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/, ''),
      }
    }
  }
})

请求链路拆解:

  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串起来,一次完整的流式对话请求走如下路径:

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应用的安全性和可维护性。