news 2026/9/8 13:15:14

Vue3+通义千问SSE流式聊天:从开发到公网部署全流程

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
Vue3+通义千问SSE流式聊天:从开发到公网部署全流程

简介:一份面向 Vue 3 前端开发者的阿里通义千问聊天机器人集成示例。它演示了在 Vue 3 项目中通过 API 接入通义千问、管理消息收发、处理接口返回数据,并最终打包部署到公网的全过程,适合需要为应用增加实时交互能力的初中级开发者参考。压缩包共 2000 个文件,约 7.51MB,主要包含 js、css、scss、ts、vue 等源码与样式文件,以及 json、md 等配置和说明文档,目录结构清晰,便于按需查阅。目前已有 304 人学习下载。配套示例提供了完整的聊天组件代码、HTTP 请求封装思路、消息状态管理方式、接口鉴权与错误处理示例,以及 Vite/Vue CLI 打包相关配置,可帮助读者快速复现“Vue 3 + 通义千问”的聊天场景。资源内还包含多种主题样式文件,可直接用于调整聊天界面外观,省去从零搭建的成本。无论是学习集成流程还是直接改造复用,都有较高参考价值。 前几天又有朋友来问:Vue3项目里怎么接通义千问的聊天接口,而且接完还得能打包丢到公网上给别人用。这个需求听起来简单,实际走一圈会发现它牵扯到接口鉴权、流式输出、跨域、打包配置、公网部署好几件互不相干的事,任何一个环节断了,页面上就一个字都转不出来。

这篇文章我就按自己实际改过几轮的完整流程来写:先用Vue3 + Vite把项目拉起来,把通义千问的流式聊天接口跑通;再把API地址、模型名、密钥这些拆到环境变量里,处理好打包配置;最后给出公网部署的具体方案,包括nginx怎么托管打包产物、怎么把API请求转发到服务端、密钥怎么不暴露在公网上。整个过程不需要复杂的后端框架,但为了让公网访问真正安全可用,我会在最后给出一个轻量代理的兜底方案。

1. 这个聊天项目适合谁,以及动手前必须想清楚的边界

1.1 三种典型场景:个人工具、团队内测、对外Demo

把通义千问封装成一个聊天页面,最常见的就是这三种场景。

第一种是个人工具:自己写一个带历史记录的对话页面,放到服务器上,手机上随手打开就能用,比每次去官方控制台调试要舒服。第二种是团队内测:公司内部做一个AI问答助手,扔到内网或者公网测试服上,让同事、客户试用,反馈问题。第三种是对外Demo:给领导汇报、给投资人演示、或者做一个开源项目,让大家都能体验一把大模型对话的效果。

这三种场景的共同点是:都不需要完整的后端业务系统,只需要一个能跑起来、能对话、还能被公网访问的页面。所以"Vue3 + 通义千问 + 打包 + 公网"这四件事其实是绑在一起的,它们就是一个最小可用产品的完整链路。你在动手之前先确认自己属于哪一类,因为这会直接决定你采用哪种部署方式、密钥能不能放前端,我们后面会展开讲。

1.2 浏览器直连的边界:CORS与密钥暴露

很多第一次做这种项目的人,会直接把API Key写死在代码里,然后在浏览器里fetch通义千问的接口。开发环境这么干通常没问题,因为通义千问的DashScope OpenAI兼容端点允许浏览器跨域调用,本地npm run dev一下,对话就能跑通。

但这里有一条边界必须提前搞清楚:把API Key打包进前端产物、部署到公网,等于把密钥公开送人。任何人打开你的站点,F12看一眼网络请求或者源码,就能把你的Key挖出来,然后拿你的额度去刷接口。

所以我的建议很明确:

  • 本地开发、个人调试:可以前端直连,Key放环境变量,方便快速验证。
  • 公网部署、给别人用:不要在前端打包Key,要么做一个轻量后端代理,要么让用户输入自己的Key临时使用。

这篇文章会按这个思路来组织,开发阶段怎么省事怎么来,上线阶段怎么稳妥怎么来。

2. 前置准备:申请API Key、选模型、用curl验证接口

2.1 开通DashScope控制台并创建API Key

第一步是去阿里云百炼控制台开通DashScope模型服务。用你的阿里云账号登录,找到通义千问相关的模型服务,开通之后在"API-KEY管理"页面创建一个新的API Key。

这个Key是一个sk-开头的字符串,创建之后只显示一次,记得立刻复制保存。如果在控制台找不到创建入口,去"模型广场"或者"开通管理"里先完成服务开通,一般几秒钟就生效。

Key拿到手之后,我强烈建议先在系统环境变量里临时挂一下,方便后面curl验证:

export DASHSCOPE_API_KEY="sk-你的key"

Windows PowerShell用户用:

$env:DASHSCOPE_API_KEY="sk-你的key"

2.2 模型选择与OpenAI兼容接口格式

通义千问DashScope提供了OpenAI兼容接口,也就是说,你不需要去学一套新的调用方式,/chat/completions这个路径、请求体结构、返回结构,跟OpenAI的Chat Completions基本一致。

模型名需要自己选,常见的有这几个:

模型名定位适用场景
qwen-turbo入门级,速度最快、成本最低简单问答、翻译、摘要
qwen-plus均衡型,能力与成本平衡日常聊天、通用助手、推荐使用
qwen-max旗舰级,综合能力最强复杂推理、长文本生成
qwen-long长文本专项处理超长上下文、文档分析

聊天项目我一般默认用qwen-plus,响应质量足够,价格也合理。如果只是做个Demo或者高频测试,用qwen-turbo更省钱。你可以在代码里把这个模型名做成环境变量,后面想换随时改,不用动逻辑。

2.3 先用curl验证接口,别急着写代码

我踩过不少次"代码写完了才发现Key不对、模型名不对"的坑,所以现在养成一个习惯:所有第三方接口接入,先用curl验证一遍,再动前端代码。这样一旦有问题,能明确区分是接口问题还是前端问题。

用下面的命令直接发一个非流式请求:

curl -X POST 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions' \ -H "Authorization: Bearer $DASHSCOPE_API_KEY" \ -H 'Content-Type: application/json' \ -d '{ "model": "qwen-plus", "messages": [ {"role": "user", "content": "你好,请用一句话介绍你自己"} ], "stream": false }'

如果网络正常、Key没问题,你会收到一个JSON响应,里面choices[0].message.content就是模型的回复。这一步能通,后面所有工作都有基础。

再验证一下流式接口,把请求体的stream改成true,响应会变成一坨data: {...}格式的文本,最后一行是data: [DONE]

data: {"choices":[{"delta":{"role":"assistant","content":"你好"}}]} data: {"choices":[{"delta":{"content":"!"}}]} data: [DONE]

我看到这个[DONE]的时候,就知道流式链路是通的,可以放心进入下一步了。

3. Vue3聊天核心实现:消息状态、SSE流式解析与完整对话闭环

3.1 消息结构和上下文截断策略

我推荐把聊天逻辑封装成一个组合式函数,不要在组件里堆一堆方法。先建一个src/config.js,统一管理接口地址和模型名:

export const API_BASE = import.meta.env.VITE_APP_API_BASE || '/api'; export const API_KEY = import.meta.env.VITE_APP_DASHSCOPE_KEY || ''; export const MODEL = import.meta.env.VITE_APP_MODEL || 'qwen-plus'; // 开发环境如果配了 Key,就直接连 DashScope;否则走代理 export const ENDPOINT = API_KEY ? 'https://dashscope.aliyuncs.com/compatible-mode/v1' : API_BASE;

消息结构我建议用最简单的一种:

{ role: 'user', content: '你好' } { role: 'assistant', content: '你好!有什么可以帮你?', reasoning: '' }

role只有userassistant两种,系统提示词在发送时临时拼到messages最前面。reasoning字段是给推理模型用的,普通模型用不到,但建议先留一个空字段,后面我们会用到。

上下文不能无限往上堆,否则Token很快就超了。我实际处理的办法是:每次请求只带上最近10条消息。这个数量对日常对话来说足够,又能控制Token成本。

const history = messages.value .filter(m => m.role !== 'system') .slice(-10) .map(({ role, content }) => ({ role, content }));

3.2 fetch按行解析SSE流式输出

流式输出这块是重头戏。很多教程会直接让你用axios,但axios对流式支持不好,而浏览器原生的fetch能拿到response.body这个ReadableStream,配合TextDecoder正好可以逐段读取流式数据。

核心逻辑是这样的:

async function send(text) { const input = text.trim(); if (!input || loading.value) return; const history = messages.value .filter(m => m.role !== 'system') .slice(-10) .map(({ role, content }) => ({ role, content })); history.push({ role: 'user', content: input }); messages.value.push({ role: 'user', content: input }); messages.value.push({ role: 'assistant', content: '', reasoning: '' }); loading.value = true; const controller = new AbortController(); abortController.value = controller; const assistant = messages.value[messages.value.length - 1]; try { const response = await fetch(`${ENDPOINT}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', ...(API_KEY ? { Authorization: `Bearer ${API_KEY}` } : {}) }, body: JSON.stringify({ model: MODEL, messages: [ { role: 'system', content: '你是通义千问,请用简洁友好的方式回答用户问题。' }, ...history ], stream: true }), signal: controller.signal }); if (!response.ok) { throw new Error(`HTTP ${response.status}`); } const reader = response.body.getReader(); const decoder = new TextDecoder('utf-8'); let buffer = ''; while (true) { const { done, value } = await reader.read(); if (done) break; buffer += decoder.decode(value, { stream: true }); const lines = buffer.split('\n'); buffer = lines.pop(); for (const line of lines) { const data = line.trim(); if (!data.startsWith('data:')) continue; const payload = data.slice(5).trim(); if (payload === '[DONE]') continue; try { const json = JSON.parse(payload); const delta = json.choices?.[0]?.delta; if (delta?.content) { assistant.content += delta.content; } } catch (_) { // 忽略解析不完整的片段 } } } } catch (e) { if (e.name !== 'AbortError') { assistant.content += `\n\n[请求失败] ${e.message}`; } } finally { loading.value = false; abortController.value = null; } }

这里有几个细节值得展开:

第一,read()返回的二进制块要用TextDecoder('utf-8')来解码,否则中文会乱码。第二,SSE数据是按行传输的,但一次网络包可能包含多个data:行,也可能在一行的中间断开,所以一定要先用split('\n')按行切,再把最后一块可能不完整的buffer留下来,跟下一轮的数据拼在一起。第三,AbortController用来实现"停止生成"按钮,用户点停止时调用controller.abort(),请求就会终止,我们只在e.name !== 'AbortError'的时候才提示请求失败,避免停止操作被视为报错。

3.3 处理reasoning_content这类特殊字段

如果你选用的模型是qwq-32b这类推理模型,流式返回里会多一个字段:delta.reasoning_content,这是模型的思考过程。它和正常的回答内容content是分开的,如果不管它,你会在页面上看到思考内容和回答混在一起,观感很差。

处理方式是在解析时把两类内容分流:

if (delta?.reasoning_content) { assistant.reasoning += delta.reasoning_content; } if (delta?.content) { assistant.content += delta.content; }

然后在模板里把reasoning单独渲染成灰色小字,或者折叠起来。普通模型不会返回这个字段,所以这份代码对两种模型都兼容。

3.4 自动滚动和Markdown渲染

流式输出过程中,消息列表需要跟着内容自动往下滚,不然用户看到的永远是上半屏。我用一个watch监听所有消息内容拼接后的字符串,内容一变就滚到底部:

const listEl = ref(null); watch( () => messages.value.map(m => m.content + (m.reasoning || '')).join(''), async () => { await nextTick(); if (listEl.value) { listEl.value.scrollTop = listEl.value.scrollHeight; } } );

另一个必须处理的是大模型输出的Markdown格式。模型默认返回的是带#-`的Markdown文本,如果你直接用{{ content }}展示,用户看到的就是一坨带井号的原始文本,很劝退。

我是用marked把Markdown转成HTML,再用DOMPurify做一层过滤,防止模型输出恶意脚本:

npm install marked dompurify
import { marked } from 'marked'; import DOMPurify from 'dompurify'; function renderMarkdown(text) { return DOMPurify.sanitize(marked.parse(text)); }

模板里这样用:

<div class="assistant-content" v-html="renderMarkdown(msg.content)"></div>

提示:v-html渲染的内容一定过DOMPurify,不要直接把模型输出当成可信HTML。模型受提示词影响可能会输出一些意料之外的标签,安全过滤是底线。

4. 打包前必改的四处配置:base、环境变量、路由与体积

4.1 把base改成相对路径,部署到哪里都不慌

很多Vue3项目在本地跑得好好的,一打包部署到服务器子目录,页面就白屏。原因多半是Vite默认的base/,所有静态资源都从根路径加载,而你的站点可能跑在https://域名/chat/这样的子路径下。

解决方式是在vite.config.js里把base改成相对路径:

import { defineConfig } from 'vite'; import vue from '@vitejs/plugin-vue'; export default defineConfig({ base: './', plugins: [vue()] });

./意味着所有资源引用都变成相对当前路径,无论你把dist放在域名根目录、子目录,还是反代到任意路径,都不会白屏。这是打包部署第一步要改的东西。

4.2 环境变量拆分:开发直连、生产走代理

我的环境变量配置习惯是拆成两个文件。

.env.development

VITE_APP_API_BASE=/api VITE_APP_MODEL=qwen-plus VITE_APP_DASHSCOPE_KEY=sk-你的开发key

.env.production

VITE_APP_API_BASE=/api VITE_APP_MODEL=qwen-plus # 注意:生产环境不写 VITE_APP_DASHSCOPE_KEY,Key 放到服务端

这样开发环境因为有Key,会直连DashScope,调试方便;生产环境没有Key,前端请求统一走/api,由nginx或后端转发到真实接口。这个"开发直连、生产代理"的模式,是前后端分离项目里比较稳妥的做法。

Vite有一个特点:只有以VITE_开头的环境变量才会被打进前端代码,所以你的生产构建产物里不会有服务端的那把Key。

4.3 history路由和nginx的try_files回退

如果聊天页面用到了Vue Router,而且用的是createWebHistory()模式,打包部署后会遇到一个问题:用户访问https://域名/chat没问题,但刷新一下https://域名/chat/conversation/123就404了。

原因是前端是单页应用,服务器上并没有conversation/123这个真实文件,请求打到nginx后找不到对应资源,自然返回404。解决办法是在nginx的location /里加一段:

location / { try_files $uri $uri/ /index.html; }

这段配置的意思是:先试着找真实的文件,找不到就统统回退到index.html,把路由交给前端去处理。这是SPA部署的标配,忘了写必踩坑。

如果你坚持要省心,也可以直接用createWebHashHistory(),URL里多个#,但不会出现刷新404的问题。

4.4 打包产物检查

改完以上配置,执行:

npm run build

打包完成后,打开dist目录检查几个地方:

  • index.html里的资源路径是不是./assets/...,而不是/assets/...
  • 看有没有dist/assets目录,里面的JS、CSS文件是否生成了带hash的文件名。
  • 如果项目里有dist/config.js之类的文件,检查有没有把生产Key打进去,如果打进去了立刻删掉并重新配置。

我自己习惯在打包前先把dist目录删掉,避免旧文件混进新产物,影响判断。

5. 公网部署实战:三种路线、nginx反向代理与密钥安全兜底

5.1 三种部署路线对比

拿到dist产物后,公网部署有几种常见路线,我按适用场景整理了一张表:

部署方案成本是否支持API反代适合场景
云服务器 + nginx约30-100元/月支持生产环境、长期使用
Vercel / Netlify免费额度支持Serverless函数个人项目、临时演示
对象存储 / CDN静态托管按量计费不支持纯静态Demo、前端展示

如果是公司项目或者你自己长期用,我推荐云服务器 + nginx,因为后面加接口代理、HTTPS证书、日志监控都方便。如果只是临时给朋友看个效果,Vercel这种平台分钟级就能上线,成本为零。

5.2 nginx托管页面并转发API代理

假设你有一台云服务器,把dist目录上传到服务器上,比如放到/var/www/qwen-chat/dist,然后写一份nginx配置:

server { listen 80; server_name ai.example.com; root /var/www/qwen-chat/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; proxy_buffering off; proxy_read_timeout 300s; } }

重点说三个配置项。

第一个是proxy_pass http://127.0.0.1:3000;,它把前端请求的/api/xx转发到本机3000端口服务。这样前端代码里VITE_APP_API_BASE=/api、请求路径/api/chat/completions就会被代理到http://127.0.0.1:3000/chat/completions,由服务端再转发给DashScope,实现"浏览器永远不知道真实API地址和Key"的效果。

第二个是proxy_buffering off;,这行至关重要。SSE流式输出是边生成边推送的,如果nginx开启了缓冲区,它会攒一批数据再一次性发给浏览器,聊天体验就从"打字机效果"变成"卡顿一下出一大段"。关闭缓冲后,数据一到就转发,流式效果才正常。

第三个是proxy_read_timeout 300s;。大模型生成长回答时,后端可能几十秒没有返回数据,nginx默认的60秒超时会导致请求被掐断,生成到一半就报错。调到300秒是个比较稳妥的数值。

改完配置执行nginx -t检查语法,没问题就nginx -s reload重载。记得域名要解析到这台服务器,如果是裸IP访问,server_name可以写IP,但HTTPS证书会麻烦一些。

5.3 轻量后端代理参考

nginx转发指向的127.0.0.1:3000需要起一个服务。如果你不想引入Java、Go这些重型框架,用Node.js自带的能力就能搞定一个最小代理,十几行代码:

// server.mjs import express from 'express'; const app = express(); const UPSTREAM = 'https://dashscope.aliyuncs.com/compatible-mode/v1'; const DASHSCOPE_KEY = process.env.DASHSCOPE_KEY; app.use(express.json({ limit: '1mb' })); app.post('/chat/completions', async (req, res) => { if (!DASHSCOPE_KEY) { res.status(500).json({ error: 'DASHSCOPE_KEY not set' }); return; } const upstream = await fetch(`${UPSTREAM}/chat/completions`, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${DASHSCOPE_KEY}` }, body: JSON.stringify(req.body) }); res.status(upstream.status); upstream.body.pipe(res); }); app.listen(3000, () => { console.log('proxy running at http://127.0.0.1:3000'); });

启动时把Key放到环境变量里:

DASHSCOPE_KEY=sk-你的key node server.mjs

提示:这个代理代码没有做任何鉴权,意思是任何知道你这个接口地址的人都可以直接调用。公网环境下至少加一个简单的访问令牌,比如前端请求头带一个自定义token、或者限制只允许你自己的域名来源。别嫌麻烦,公网扫描器比你想的勤快得多。

5.4 上线后的验证清单

部署完成后,我每次都会按这套清单走一遍,缺一不可:

  • 浏览器无痕模式访问公网地址,确认页面能打开、静态资源加载正常。
  • 发一条消息,确认模型回复是逐字输出的,不是等待很久后一次性出现。
  • 刷新页面,确认当前对话路径不会404。
  • 打开开发者工具Network面板,确认没有直接发起对dashscope.aliyuncs.com的请求;如果看到了,说明前端还在直连,Key可能已经暴露。
  • 用手机流量再访问一次,排除内网环境导致的错觉。
  • 确认Key是在服务端环境变量里设置的,而不是写在前端代码或仓库里。

这套流程我第一次全走完大概花了半小时,但之后就再没出现过"本地正常、上线白屏、聊天断流"这类经典问题。

一点实际操作体会

这个项目最让我舒坦的地方在于:整个链路没有一项是"黑科技",但每一项都值得认真对待。前端流式解析那一块,TextDecoder和按行切分的处理方式,换成别的接口也能复用;nginx那段proxy_buffering offtry_files,几乎能平移到任何SPA项目上。

如果你打算正式对外用,我个人的建议是:别嫌麻烦,花一个小时把那个轻量代理部署上去,把Key从打包产物里摘出来。公网环境里一切自动化扫描都比你想的活跃,密钥一旦泄漏,损失的也不只是几块钱的Token费用。先把安全底线守住,后面这个聊天页面才能真正变成能放心丢给别人用的东西。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/8 13:14:35

AI Agent冲击下,传统SaaS如何转型?生存危机与破局思路

AI 时代&#xff0c;传统 SaaS 行业面临的生存危机与转型思路最近和几个做 SaaS 的朋友聊天&#xff0c;大家都有一个共同的感受&#xff1a;AI 大模型出来之后&#xff0c;老客户开始问一些以前从来没问过的问题——"你们这功能 AI 能做吗&#xff1f;""为什么…

作者头像 李华
网站建设 2026/9/8 13:14:10

AI编程助手Skills实战:从机制原理到踩坑记录

这两年AI编程助手迭代得实在太快&#xff0c;我日常用的工具已经从"能补全代码的编辑器"变成了"带项目理解能力的Agent"。但真正让我觉得质变发生的&#xff0c;其实是各家开始推 skills 之后。一开始我也以为这就是个"预置提示词"的花样&…

作者头像 李华
网站建设 2026/9/8 13:13:24

RISC-V开发板驱动视觉机械臂:YOLOE+Agent+MCP闭环实践

1. 项目整体思路&#xff1a;为什么把这四样拼在一起先直接回答标题里的问题&#xff1a;RISC-V 确实能跑机器人&#xff0c;但要看跑成什么样。我手里这块 VisionFive 2 是 StarFive 出的 RISC-V 开发板&#xff0c;四核 Cortex-A55&#xff0c;8GB 内存&#xff0c;带一个 2T…

作者头像 李华
网站建设 2026/9/8 13:12:47

源端并行解析 × 目标端多通道入库:KFS 同步架构深度解读

源端并行解析 目标端多通道入库&#xff1a;KFS 同步架构深度解读 一、为什么"增量同步"成了数据库的生死线十年前做数据同步&#xff0c;工程师最在意的是"能不能把数据搬过去"&#xff1b;今天做数据同步&#xff0c;工程师最在意的是"能不能跟得上…

作者头像 李华
网站建设 2026/9/8 13:11:00

移动端AI聊天引擎搭建:SSE流式输出与WebView打包实战

之前做移动端 AI 聊天产品时&#xff0c;最让我头疼的不是大模型本身&#xff0c;而是手机端的流式渲染、软键盘弹出、WebView 缓存不一致这些细节。这次我把整个免费 AI 聊天引擎的手机端从零搭了出来&#xff0c;不废话&#xff0c;直接分享一套能跑通的最小闭环&#xff0c;…

作者头像 李华
网站建设 2026/9/8 13:10:57

千笔与灵感AI横评:谁更懂MBA论文写作全流程?

先说个开场白。我这两周把市面上叫得上名字的AI论文平台几乎都跑了一遍&#xff0c;最终锁定了两个最有代表性的放在一起做深度横评——千笔专业学术智能体&#xff0c;和灵感AI。理由很简单&#xff1a;一个是垂直学术场景的智能体方案&#xff0c;一个是通用AI写作平台里呼声…

作者头像 李华