项目地址
- 在线预览:frontend-ai-assistant-two.vercel.app/
- GitHub 源码:github.com/hewq/dify-c…
说明:当前在线版本部署在 Vercel,国内网络访问可能不稳定。如果打不开,可以直接查看 GitHub 源码并在本地启动。
前言
上一篇我们用 Vite + React 做了一个最小 AI 聊天页面,实现了:
用户输入问题
↓
前端直接调用 Dify API
↓
展示 AI 回答
这个版本能跑,但有一个非常严重的问题:
Dify API Key 暴露在浏览器里。
只要打开浏览器 DevTools,就可能在 Network 请求里看到:
Authorization: Bearer app-xxxxxxxx
这在正式项目里是绝对不能接受的。
这一篇我们就来解决这个问题:
用 Express 做一个 BFF 代理层,把 Dify API Key 放到服务端。
最终调用链路会变成:
React 前端
↓
Express BFF
↓
Dify API
为什么不能把 API Key 放在前端?
很多刚开始做 AI 应用的人会直接在前端写:
fetch('https://api.dify.ai/v1/chat-messages', {
headers: {
Authorization: `Bearer ${API_KEY}`,
},
})
如果只是本地 Demo,这样可以快速验证。
但只要项目上线,这种写法就有风险。
原因是:
1. 前端代码会被打包到浏览器
2. 浏览器环境没有真正的秘密变量
3. 用户可以通过 DevTools 查看请求头
4. Key 泄露后,别人可以直接消耗你的额度
5. 你无法对请求做鉴权、限流、日志和风控
所以更合理的结构是:
浏览器只请求自己的后端接口
后端接口再携带 API Key 调用 Dify
这就是我们这一篇要做的 BFF 层。
什么是 BFF?
BFF 是 Backend For Frontend,直译就是“为前端服务的后端”。
在这个项目里,BFF 的职责很简单:
1. 接收前端问题
2. 从服务端环境变量读取 Dify API Key
3. 调用 Dify Chat API
4. 把 Dify 的回答返回给前端
也就是说,前端不再关心 Dify 的真实 Key 和真实接口细节。
前端只需要请求:
/api/chat
而不是直接请求:
https://api.dify.ai/v1/chat-messages
本篇目标
完成后项目会变成:
Vite React 前端
↓
/api/chat
↓
Express 服务端
↓
Dify Chat Messages API
并实现:
1. Dify API Key 不再暴露到浏览器
2. 前端调用自己的 /api/chat 接口
3. Express 负责请求 Dify
4. 支持 conversationId 多轮对话
5. 本地可以一键启动前后端
第一步:安装服务端依赖
在项目根目录执行:
npm install express cors dotenv
npm install -D tsx concurrently @types/express @types/cors
几个依赖的作用:
express:启动本地 API 服务
cors:处理跨域
dotenv:读取 .env 环境变量
tsx:直接运行 TypeScript 服务端代码
concurrently:同时启动前端和后端
第二步:调整环境变量
上一篇我们在 .env.local 里写了:
VITE_DIFY_API_KEY=xxx
这一篇要改掉。
在项目根目录创建或修改:
.env
写入:
PORT=3001
DIFY_API_KEY=你的 Dify 应用 API Key
DIFY_API_URL=https://api.dify.ai/v1/chat-messages
DIFY_USER=vite-demo-user
注意,这里不要加 VITE_ 前缀。
因为我们不希望它暴露给浏览器。
同时确认 .gitignore 里有:
.env
.env.local
真实 Key 一定不要提交到 GitHub。
第三步:创建 Express 服务
新建:
server/index.ts
写入:
import express from 'express'
import cors from 'cors'
import dotenv from 'dotenv'
dotenv.config()
const app = express()
app.use(cors())
app.use(express.json())
const PORT = Number(process.env.PORT || 3001)
const DIFY_API_KEY = process.env.DIFY_API_KEY
const DIFY_API_URL =
process.env.DIFY_API_URL || 'https://api.dify.ai/v1/chat-messages'
const DIFY_USER = process.env.DIFY_USER || 'vite-demo-user'
if (!DIFY_API_KEY) {
throw new Error('Missing DIFY_API_KEY in .env')
}
app.get('/health', (_req, res) => {
res.json({ ok: true })
})
app.post('/api/chat', async (req, res) => {
try {
const { message, conversationId } = req.body
if (!message || typeof message !== 'string') {
return res.status(400).json({ error: 'message is required' })
}
const difyResponse = await fetch(DIFY_API_URL, {
method: 'POST',
headers: {
Authorization: `Bearer ${DIFY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
inputs: {},
query: message,
response_mode: 'blocking',
conversation_id: conversationId || '',
user: DIFY_USER,
}),
})
if (!difyResponse.ok) {
const errorText = await difyResponse.text()
return res.status(difyResponse.status).json({
error: 'Dify API request failed',
detail: errorText,
})
}
const data = await difyResponse.json()
return res.json({
answer: data.answer,
conversationId: data.conversation_id,
raw: data,
})
} catch (error) {
console.error('[POST /api/chat]', error)
return res.status(500).json({
error: 'Internal server error',
detail: error instanceof Error ? error.message : 'Unknown error',
})
}
})
app.listen(PORT, () => {
console.log(`API server running at http://localhost:${PORT}`)
})
现在服务端提供了两个接口:
GET /health 健康检查
POST /api/chat 聊天接口
第四步:修改 package.json scripts
打开 package.json,把 scripts 调整成:
{
"scripts": {
"dev": "vite",
"server": "tsx watch server/index.ts",
"dev:all": "concurrently "npm run server" "npm run dev"",
"build": "tsc -b && vite build",
"preview": "vite preview"
}
}
之后本地开发就可以执行:
npm run dev:all
它会同时启动:
Vite 前端:http://localhost:5173
Express 后端:http://localhost:3001
第五步:配置 Vite 代理
现在前端运行在 5173,后端运行在 3001。
前端请求 /api/chat 时,需要代理到后端。
打开:
vite.config.ts
改成:
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
},
})
这样前端请求:
/api/chat
实际会被 Vite 转发到:
http://localhost:3001/api/chat
浏览器里看到的仍然是 /api/chat,不会看到 Dify 官方地址。
第六步:修改前端 API 调用
上一篇的 src/api/dify.ts 是直接调用 Dify。
现在改成调用自己的后端:
export interface ChatResponse {
answer: string
conversationId: string
}
export async function sendMessageToDify(
message: string,
conversationId?: string
): Promise<ChatResponse> {
const response = await fetch('/api/chat', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
message,
conversationId,
}),
})
if (!response.ok) {
const errorText = await response.text()
throw new Error(errorText || '请求失败')
}
return response.json()
}
注意:
现在前端代码里已经没有:
DIFY_API_KEY
Authorization
https://api.dify.ai
这说明 API Key 已经从浏览器环境移到了服务端。
第七步:启动并测试
启动项目:
npm run dev:all
先测试后端健康检查:
http://localhost:3001/health
应该返回:
{"ok":true}
然后打开前端:
http://localhost:5173
输入:
前端架构主要包括哪些内容?
如果能正常回答,说明链路跑通:
React 页面
↓
/api/chat
↓
Express
↓
Dify
↓
知识库 + LLM
第八步:在浏览器里确认 Key 没有暴露
打开浏览器 DevTools,进入 Network。
发送一次问题。
你应该看到请求地址是:
/api/chat
而不是:
https://api.dify.ai/v1/chat-messages
请求头里也不应该出现:
Authorization: Bearer app-xxx
因为 Authorization 现在是在 Express 服务端发给 Dify 的,浏览器看不到。
这是这一篇最重要的完成标准。
当前版本架构
现在项目架构变成了:
Browser
↓
Vite React App
↓
/api/chat
↓
Express BFF
↓
Dify Chat API
↓
Knowledge Retrieval + LLM
相比上一篇,最大的变化是增加了:
Express BFF
它让我们获得了几个能力:
隐藏 Dify API Key
统一错误处理
后续支持日志
后续支持限流
后续支持鉴权
后续支持数据库
后续支持流式代理
所以 BFF 不只是“转发接口”,它是项目后续工程化的基础。
为什么还要保留 conversationId?
Dify 的多轮对话依赖 conversation_id。
第一次请求时,我们传:
conversation_id: ''
Dify 会返回一个新的:
conversation_id
前端保存后,下一次请求继续带给后端:
body: JSON.stringify({
message,
conversationId,
})
后端再转成 Dify 需要的字段:
conversation_id: conversationId || ''
这样多轮对话能力就保留下来了。
这一版还有哪些不足?
现在 API Key 安全问题解决了,但还有几个明显不足:
1. 回答仍然是 blocking 模式
当前服务端传的是:
response_mode: 'blocking'
用户必须等 Dify 完整生成回答后才能看到结果。
下一篇会改成:
response_mode: 'streaming'
实现像 ChatGPT 一样逐字输出。
2. 服务端代码还比较粗糙
现在所有逻辑都在 server/index.ts 里。
后面会拆成:
server/config/env.ts
server/routes/chat.ts
server/services/dify.ts
server/utils/errors.ts
3. 没有请求取消
如果用户想停止生成,目前还做不到。
后面会用 AbortController 来实现。
4. 没有 Markdown 和引用来源
这些还是后续文章的内容。
常见问题
1. 报 Missing DIFY_API_KEY
说明 .env 里没有配置:
DIFY_API_KEY=xxx
或者你修改 .env 后没有重启服务端。
2. 前端请求 404
检查 Vite 代理是否配置:
server: {
proxy: {
'/api': {
target: 'http://localhost:3001',
changeOrigin: true,
},
},
}
3. Dify 返回鉴权失败
检查 DIFY_API_KEY 是否是应用 API Key,不是 DeepSeek API Key。
这里很容易搞混:
Dify 应用 API Key:给我们的程序调用 Dify 应用
DeepSeek API Key:配置在 Dify 后台,给 Dify 调模型
4. CORS 报错
本地开发时前端走 Vite proxy,一般不会有 CORS 问题。
如果你直接从 localhost:5173 请求 localhost:3001,才需要 CORS。
当前服务端已经加了:
app.use(cors())
本篇总结
这一篇我们完成了一个非常关键的架构升级:
前端直连 Dify
↓
前端请求 Express BFF
↓
Express 调用 Dify
这一步解决了 Dify API Key 暴露问题,也为后续扩展打好了基础。
现在项目已经具备了一个 AI 应用最基本的安全结构:
浏览器不保存敏感 Key
服务端统一代理外部 AI API
前端只调用自己的业务接口
下一篇我们继续升级体验:
把 blocking 模式改成 streaming,让 AI 回答像 ChatGPT 一样逐字输出。