☰
Vue3接入通义千问聊天实战:从本地调试到公网部署避坑指南
2026/10/15 1:38:23 网站建设 项目流程

简介:面向Vue 3前端开发者,这份示例工程演示如何在真实项目中接入阿里通义千问聊天机器人,覆盖接口调用、消息收发、组件封装以及打包部署到公网的关键思路,适合需要快速实现智能问答或客服场景的开发者参考。压缩包共2000个文件,以js、css、scss、ts、json等前端工程文件为主,包含源码、样式配置、构建产物以及Markdown说明文档,整体7.51MB,解压后可直接对照项目结构学习和二次开发。目前已有309人学习浏览。资源的价值在于通过一个完整可运行的例子,展示Vue 3中使用响应式数据管理聊天消息、通过HTTP请求对接大模型API,以及使用Vite等工具将项目打包发布到服务器的流程;同时提供了可复用的组件写法与接口地址替换思路,能帮助开发者避开通义千问接入中的常见坑,快速集成到自己的业务中。

1. Vue3 调用通义千问聊天:本地能跑,不代表公网能用

本地开发好好的,打包上传到服务器,聊天请求全部失败——这是 Vue3 调用通义千问最常见也最容易忽略的翻车点。标题里那句“可以打包公网使用”才是难点所在:你要同时处理模型接口的鉴权、浏览器跨域,以及 API Key 不会被扒走这三件事。这篇笔记写给两类人:一是只想快速跑通一个通义千问聊天页面的 Vue3 开发者,二是已经跑通但部署到公网就各种报错、想一次把坑踩完的人。我会按真实落地顺序讲,从最小请求、流式封装,到 Nginx 反代和 Key 隔离,最后给你一份自测清单。

2. Vue3 项目与通义千问鉴权:先跑通一个最小请求

2.1 用 Vite 创建 Vue3 项目,浏览器端为什么不用官方 SDK

先建一个干净的 Vue3 工程。我习惯直接用 Vite 官方脚手架,省去配置 Webpack 的时间,而且 Vite 的 dev server 自带代理能力,后面会用到。

npm create vite@latest qwen-chat -- --template vue cd qwen-chat npm install

这段命令做了三件事:用 vue 模板创建qwen-chat目录、进入项目、安装基础依赖。装完以后npm run dev就能看到一个标准 Vue3 页面。注意这里不需要额外安装通义千问的官方 SDK。

原因很简单:官方 SDK 主要面向 Node.js 后端环境,直接 import 到浏览器里会把一堆 Node 依赖带进打包产物,体积变大,还可能遇到 polyfill 问题。我一般推荐浏览器端用原生 fetch,走通义千问的 OpenAI 兼容模式接口。理由有三个:打包体积可控、请求逻辑透明好排查、以后切换模型时前端代码几乎不用动。

接下来在项目根目录建一个.env文件,把密钥放进去,开发期调试用:

# .env VITE_DASHSCOPE_API_KEY=sk-你的密钥

这里要提醒一句:VITE_前缀的环境变量会被 Vite 在构建时直接打进前端代码,所以这个 Key 只适合本地开发。公网部署前必须换方案,我放在第 4 章重点讲。现在先让它跑起来。

2.2 Base URL、鉴权与最小请求:先关掉流式跑通一次

通义千问的 OpenAI 兼容模式接口,地址是固定的三段式:域名 +/compatible-mode/v1+ 具体路径。聊天补全的完整 URL 是https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions。鉴权方式就是标准的Authorization: Bearer <API Key>,模型名在qwen-turbo、qwen-plus、qwen-max里选。

// src/api/qwen.js const BASE_URL = "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions"; export async function chatOnce(messages) { const res = await fetch(BASE_URL, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + import.meta.env.VITE_DASHSCOPE_API_KEY, }, body: JSON.stringify({ model: "qwen-plus", messages: messages, stream: false, }), }); if (!res.ok) { const text = await res.text(); throw new Error("HTTP " + res.status + ": " + text); } const data = await res.json(); return data.choices[0].message.content; }

这段代码的逻辑很直白:BASE_URL必须写全,少了/compatible-mode/v1会直接 404;messages是一个数组,里面是{ role, content }结构,role可以是system、user、assistant,至少要有一条user消息;stream: false表示一次性返回完整文本,适合先验证链路通不通。返回结构里data.choices[0].message.content就是模型生成的回答。

模型怎么选也有讲究。开发调试阶段用qwen-turbo,成本低、响应快,先验证代码逻辑;正式聊天页面换qwen-plus,综合能力更稳;qwen-max效果最好但贵且慢,适合对回复质量要求高的场景,不太适合做高频聊天。第一次跑通建议用qwen-turbo,把页面和网络问题排查干净后再换。

3. 流式聊天封装:把 SSE 增量文本拼成完整的对话流

3.1 用 fetch 读取流式返回:解析 data 行的完整循环

聊天场景不开流式,用户要盯着页面等整段答案生成完,体验很难受。把请求体里的stream改成true后,接口返回的是 SSE 流,也就是一行一行以data:开头的文本块,最后以data: [DONE]结束。真正的难点在于:网络包是分块到的,一个 chunk 里可能只有半行 JSON,中文还可能被截断。

// src/api/qwen.js export async function chatStream(messages, onDelta) { const res = await fetch(BASE_URL, { method: "POST", headers: { "Content-Type": "application/json", "Authorization": "Bearer " + import.meta.env.VITE_DASHSCOPE_API_KEY, }, body: JSON.stringify({ model: "qwen-plus", messages, stream: true, }), }); if (!res.ok) { const text = await res.text(); throw new Error("HTTP " + res.status + ": " + text); } const reader = res.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 lineText = line.trim(); if (!lineText.startsWith("data:")) continue; const payload = lineText.slice(5).trim(); if (payload === "[DONE]") return; try { const json = JSON.parse(payload); const delta = json.choices?.[0]?.delta?.content; if (delta) onDelta(delta); } catch (err) { console.warn("skip bad chunk:", payload); } } } }

这段代码的核心是getReader()读取字节流,然后用TextDecoder解码。decoder.decode(value, { stream: true })这个写法必须保留,否则中文在多字节截断时会出乱码。每次拿到新数据先累加到buffer,再按\n切行,lines.pop()把最后一段可能不完整的半行留到下一轮拼接。对每一行,只处理data:开头的内容,去掉前缀后如果遇到[DONE]就结束。正常数据是一个 JSON 字符串,解析后取choices[0].delta.content,这就是当前这一小段增量文本,把它交给回调函数,由页面实时拼接显示。

3.2 在 Vue 组件里接入:消息历史与五个必调参数

组件侧的逻辑比想象中简单。用一个ref数组维护对话历史,发送时把用户消息推进去,再预占一条空的 assistant 消息,流式回调把增量往这条消息上拼。

// src/components/ChatBox.vue 的 script setup 片段 import { ref } from "vue"; import { chatStream } from "../api/qwen"; const messages = ref([ { role: "system", content: "你是简洁的助手" }, ]); const inputText = ref(""); const loading = ref(false); async function send() { const userMsg = { role: "user", content: inputText.value }; messages.value.push(userMsg); inputText.value = ""; loading.value = true; const reply = { role: "assistant", content: "" }; messages.value.push(reply); try { await chatStream(messages.value, (delta) => { reply.content += delta; }); } finally { loading.value = false; } }

messages.value直接传给chatStream,多轮对话的历史上下文就都带上去了,不需要额外维护。assistant 消息先以空字符串占位,每收到一段 delta 就用+=拼接,因为reply是响应式对象的属性,页面上的显示会跟着实时更新。finally里把 loading 复位,保证出错时按钮也能恢复。

除stream外,有几个参数值得调一调,下面这张表是通用参考:

参数建议值影响什么时候动它
temperature0.7~0.9越高回复越发散,越低越稳定写代码、总结类需求下调到 0.3~0.5
top_p0.6~0.9与 temperature 联动控制采样范围一般按默认,不一定要调
max_tokens1024~2048单次回答的最大长度保护长文任务加大,普通聊天 1024 够
streamtrue是否流式返回聊天用 true,批量离线任务用 false
stop可选遇到该字符串停止生成结构化输出时用,例如传"\n\n"

注意top_p和temperature不要同时大幅调高,两个一起调会把输出搞得很乱。我自己的习惯是固定top_p不动,只调temperature,问题定位起来也更干净。

4. 打包公网前的三个必改配置:代理、CORS 与 Key 保护

4.1 开发环境用 Vite proxy 隐藏 Key:先让页面不再直连模型网关

前端直接 fetch 完整网关地址有两个问题:浏览器跨域策略大概率触发 CORS 拦截;就算放行,API Key 也会出现在 Network 面板里,任何人都能看见。开发阶段最稳的做法是让页面只请求相对路径/api/...,由 Vite dev server 做代理转发,顺带在代理端注入 Authorization 头。

// vite.config.js import { defineConfig } from "vite"; import vue from "@vitejs/plugin-vue"; export default defineConfig({ plugins: [vue()], server: { proxy: { "/api": { target: "https://dashscope.aliyuncs.com/compatible-mode/v1", changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, ""), headers: { Authorization: "Bearer " + process.env.VITE_DASHSCOPE_API_KEY, }, }, }, }, });

这段配置要配合前端代码一起改。前端请求地址从完整 URL 改成/api/chat/completions,Vite 收到请求后,rewrite把/api前缀去掉,然后把剩余路径拼到target后面,最终转发到https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions。changeOrigin: true会把请求的 Host 头改成目标域名,防止网关按 Host 校验拒绝。headers里注入的 Authorization 是 dev server 进程读取的,浏览器端看不到 Key。

改完以后,前端的 fetch 代码可以删掉 Authorization 头,请求里只保留Content-Type。这样开发环境里即使打开 Network 面板,看到的也只是本地请求。

4.2 生产环境用 Nginx 反向代理:SSE 不卡、Key 不落地、路径不错位

vite build打包出来的是纯静态文件,没有 Node 运行时,所以公网部署必须有一个 Web 服务器来托管静态资源,同时为/api做反向代理。最常见的做法是 Nginx。

server { listen 80; server_name your-domain.example; root /var/www/qwen-chat/dist; index index.html; location /api/ { proxy_pass https://dashscope.aliyuncs.com/compatible-mode/v1/; proxy_http_version 1.1; proxy_set_header Host $host; proxy_set_header Authorization "Bearer 这里填服务端保管的Key"; proxy_set_header Connection ""; proxy_buffering off; proxy_cache off; proxy_read_timeout 300s; } location / { try_files $uri $uri/ /index.html; } }

这段配置里有三个细节值得展开。第一,proxy_pass的地址末尾带不带斜杠,拼接结果不一样。这里写成https://dashscope.aliyuncs.com/compatible-mode/v1/,Nginx 收到/api/chat/completions后,会用location /api/匹配的部分替换成目标路径,最终请求的是/compatible-mode/v1/chat/completions,正好对上。

第二,proxy_buffering off和proxy_cache off必须加。Nginx 默认会缓冲后端响应,SSE 是边生成边返回的流,开着缓冲会让前端一直等不到增量文本,甚至超时。关掉之后,模型生成的内容能实时透传到浏览器。proxy_read_timeout 300s是给长回答留出时间,避免默认 60 秒断开。

第三,Authorization 头写在 Nginx 配置里,Key 只存在于服务器,用户的浏览器拿不到。这个做法已经满足一般演示场景的安全要求。如果你还要做调用统计和多 Key 轮换,那就再加一层 Node 网关,我放到最后一章讲。

另外,如果你用的是 history 路由,location /里的try_files是必须的,否则用户刷新/chat这类子路由会直接 404。Nginx 配置改完以后,记得先nginx -t再nginx -s reload。

5. 避坑:通义千问接入与公网打包的四条排查记录

5.1 现象一:开发环境一切正常,打包后请求全部 404

本地npm run dev聊天正常,npm run build部署后页面能开,但发送消息就 404。

原因很典型:前端请求用的是/api/...相对路径,开发时 Vite proxy 把它转发到了模型网关;打包后 proxy 不存在了,/api请求直接落在静态服务器上,自然 404。很多人把开发代理误当成生产能力,这是最容易踩的坑。

解决:生产环境必须在 Web 服务器上配好反代,让/api指向模型网关。部署后先用 curl 验证接口通没通,再打开页面测试:

curl -X POST https://your-domain.example/api/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"qwen-turbo","messages":[{"role":"user","content":"hi"}],"stream":false}'

如果 curl 能拿到正常 JSON 回复,说明反代链路没问题,剩下的就是前端逻辑。

5.2 现象二:浏览器报 CORS,但代理已经配了

控制台报Access-Control-Allow-Origin,请求被浏览器拦下,但 Nginx 代理明明配好了。

先做一个判断:打开 Network 面板,看请求 URL 是你自己的域名还是dashscope.aliyuncs.com。如果是后者,说明前端 fetch 里写的是完整网关地址,请求根本没走代理,CORS 拦截是必然的。另一种情况是代理配了,但前端页面用的协议或域名和server_name不一致,请求被浏览器当成跨域。

解决:前端所有请求统一改成相对路径/api/...;Nginx 里确认proxy_pass目标写对了。改完 Nginx 配置后执行nginx -t检查语法,再 reload 使配置生效。

5.3 现象三:流式输出一直 loading,或文本一次性全部出现

对话框一直转圈,等很久之后整段文本突然全部显示出来,甚至直接超时。

两个原因。第一,Nginx 默认开启了缓冲,SSE 增量数据被攒在缓冲区里,前端拿不到,看起来就像卡死了;等缓冲满了才一次性吐出,表现成整段文本突然出现。第二,proxy_read_timeout默认只有 60 秒,回答稍长就会断流。

解决:Nginx 的location /api/下加proxy_buffering off、proxy_cache off、proxy_read_timeout 300s。另外确认前端解析流式的代码用的是res.body.getReader()逐行解析,而不是res.json()一把梭——后者拿不到增量,流式等于白开。

5.4 现象四:API Key 被扒,账单异常

有人拿你的 Key 疯狂调用,账单飙涨,或者某天接口突然返回 401。

原因:所有以VITE_开头的环境变量都会被 Vite 在构建时原样写进静态资源,浏览器 devtools 里一搜就有。.env文件在服务端不代表 Key 在服务端,打包后的dist/assets里的 JS 文件包含完整 Key 文本。

解决:生产环境不要让 Key 出现在构建产物里。开发期用 Vite proxy 注入,生产期用 Nginx 反代注入,或者用 Node 网关存 Key。部署后跑这条命令检查产物:

grep -r "sk-" dist/

如果搜到了 Key,说明已经泄露,立刻去控制台吊销并重新生成。另外几个常见 HTTP 状态码也值得记住:401 是 Key 无效或已吊销;429 是触发限流,降低请求频率;400 是参数或模型名写错;500 多为网关侧临时问题,重试即可。

6. 从“能聊天”到“能演示”:Key 隔离、频控与一行自测命令

如果你的目标是给同事演示或者放出去给别人试用,光有 Nginx 反代还不够,还得管住调用成本。我一般会在 Nginx 后面加一层轻薄的 Node 网关,Key 从配置文件里挪到进程环境变量,顺便做频控和日志。

// server/index.js(示意,框架随意) import express from "express"; import { createProxyMiddleware } from "http-proxy-middleware"; const app = express(); const KEY = process.env.DASHSCOPE_API_KEY; app.use( "/api/chat/completions", createProxyMiddleware({ target: "https://dashscope.aliyuncs.com/compatible-mode/v1", changeOrigin: true, pathRewrite: { "^/api": "" }, headers: { Authorization: "Bearer " + KEY }, // 这里可以挂频控中间件,比如每 IP 每分钟 20 次 }) ); app.listen(3000);

这层网关带来三个实际好处:Key 只存在于 Node 进程里,Nginx 配置里都不用再写;可以对/api加简单频控,防止演示页面被刷;还可以在代理层记日志,看看每天实际消耗了多少 tokens。代码本身不长,但把“能聊天”升级成了“能放心给别人用”。

部署完以后,我固定会用两行命令做最终验收。第一行检查产物里有没有泄露 Key,第二行用 curl 直接看流式返回。

grep -r "sk-" dist/ && echo "Key 泄露!" || echo "产物干净" curl -N https://your-domain.example/api/chat/completions \ -H "Content-Type: application/json" \ -d '{"model":"qwen-turbo","messages":[{"role":"user","content":"说你好"}],"stream":true}'

curl -N关闭缓冲,你能看到文本一段段打出来,说明 SSE 链路是通的。前端再打开页面真发一条消息,Network 面板里确认只有你自己域名的请求,没有直连模型网关的调用,就算彻底验收通过了。

我这里没有写复杂的状态管理,也没有接任何对话 UI 库,因为这套流程的核心从来不是前端有多花哨,而是从 Vue3 到通义千问再到公网这条链路别在中途断掉。我每次做这类聊天 Demo,固定流程就是先用qwen-turbo跑通链路,再换qwen-plus调效果,build 后先 grep Key 再 curl 流式,最后才开页面。这套习惯帮我省掉了大半的玄学排错时间,希望帮到你。

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

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询