简介:面向Vue 3开发者,这份资源演示了如何在前端项目中接入阿里通义千问聊天机器人,并支持打包后部署到公网使用。资源包约7.51MB,共2000个文件,其中js、ts、mjs等脚本负责业务逻辑与API调用,css、scss定义界面样式,json、md提供配置参数和说明文档,vue组件则展示了聊天窗口的消息发送、接收与渲染过程。目前已有309人学习下载。借助该示例,开发者可以掌握Vue 3 Composition API配合响应式数据管理聊天状态,使用Axios请求千问接口,并通过Vite或Vue CLI完成构建部署;同时资源包含了聊天组件的封装、密钥配置入口与安全注意事项,能够帮助读者省去从零调研的麻烦,让应用快速具备自然语言交互能力。代码结构清晰,适合需要为项目增加AI对话服务的初中级前端工程师,也可作为在现有项目中集成通义千问的二次开发基础。
1. 一个 Vue3 聊天页面直连通义千问,为什么能打包上公网却没人敢这么干
vue3 调用阿里通义千问聊天例子,听起来就是把一个聊天窗口接到大模型上,按理说前端调用一个 HTTP 接口就行了。真正动手时才会发现,本地开发一切顺利,一打包部署到公网就翻车:跨域拦截、密钥暴露、流式响应中断、部署服务器配置不对……这些坑一个接一个。这篇笔记基于我自己做过的模拟项目X和他人分享的经验,把这整条链路——从通义千问接口选型、Vue3 里的流式调用代码,到打包后部署公网必须处理的跨域与反向代理——完整走一遍,给出能直接抄作业的命令和配置。适合已经会 Vue3 基础、想快速把大模型聊天接进自己的项目里并能上线演示的开发者;如果你正在纠结密钥要不要放前端,后面第五、六章会给你一条清晰的升级路径。
2. 先把通义千问接入方式定下来:API 兼容协议、密钥与最小验证命令
2.1 通义千问的接入选型:SDK、HTTP 直连还是兼容协议
聊到调用通义千问,目前摆在开发者面前的主要有三条路:官方 HTTP 接口、官方 SDK、以及 OpenAI 兼容协议。在这三者之间做选择,直接决定了你 Vue3 项目里要写多少代码、踩多少坑。
官方 HTTP 接口是最直接的方式,请求结构清晰,返回格式固定,适合只做一次性验证或者写后端服务。官方 SDK(Python/Java 等)适合后后端语言栈,但前端 JavaScript 的官方 SDK 生态相对不完整,很多时候会发现它内部还是包了一层 HTTP 请求,与其被这层封装限制,不如自己写 fetch。
我在实际项目里更推荐第三种:OpenAI 兼容协议。原因有两个:第一,它和前端生态里的各种现成库、示例代码天然兼容,遇到问题能搜到的资料量完全不是一个数量级;第二,通义千问的这个兼容层本身就是为了迁移场景设计的,请求结构和响应格式都足够清晰。在 Vue3 里最普通的 fetch 就能完成调用,不需要额外引入重量级 SDK。
需要注意的是,不管是哪种方式,本质上都要走 HTTPS 请求到阿里云百炼的网关,密钥用的都是 DashScope API Key。前端直连场景下,这个 key 最终一定会暴露在浏览器里,这点我后面专门用一章来聊安全和规避方案,你现在只需记住这个"迟早要面对"的前提。
2.2 用一条 curl 命令先验证连通性:模型、密钥与参数
在你写任何 Vue3 代码之前,强烈建议先用一条 curl 命令把连通性和接口格式验证通了。这能帮你把"问题在链路哪一段"划分清楚——是本地网络问题、密钥无效、模型名写错,还是参数格式不对。我自己的习惯是,前端代码开工前,至少先花十分钟把这条命令跑通。
通义千问兼容协议的接口地址是https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions,认证方式是 Bearer Token。下面是一条最基础的聊天非流式请求:
curl 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": "system", "content": "你是一个乐于助人的助手。"}, {"role": "user", "content": "用一句话介绍你自己"} ], "stream": false }'这里有几个参数需要说明。model字段,qwen-plus 是均衡型模型,日常聊天和演示够用;追求极致速度用 qwen-turbo,需要更强推理能力用 qwen-max,最终效果差异可以通过调整 model 来对比。messages是对话历史数组,role 有 system、user、assistant 三种,前端在做多轮对话时必须把历史消息一并传上去,否则模型没有任何上下文记忆。stream在验证连通性时先设 false,拿到完整的 JSON 响应最为直观,等后端链路确认没问题了,再改成 true 做流式。
如果你用的是 Windows 环境,curl 命令同样可用(Win10+ 自带),但环境变量$DASHSCOPE_API_KEY的语法不同,更稳妥的做法是直接把 key 字符串粘贴到命令里替换。不过我仍然建议设置环境变量,因为后面所有示例都会引用这个变量,少一点复制粘贴就少一点密钥泄露的几率。
正常响应会返回一个 choices 数组,其中 message.content 就是模型生成的全文。如果密钥无效,返回 401;模型名拼错,返回 404 或类似 InvalidParameter 的错误信息;如果触发内容安全策略,会有专门的错误码。这些信息足够帮助你完成最基础的排查了。
2.3 非流式与流式响应的数据结构:前端解析前必须先知道的事
用一次非流式请求确认连通性之后,还要看一遍流式返回的数据格式,因为前端聊天体验必须用流式,它决定了用户是盯着一个光标干等几秒,还是能像 ChatGPT 一样看到字一个一个蹦出来。这两者的差异在前端代码里不是同一套循环逻辑。
非流式(stream: false)的响应是一个完整的 JSON,核心结构如下:
{ "choices": [ { "message": { "role": "assistant", "content": "你好,我是通义千问..." }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 20, "completion_tokens": 30, "total_tokens": 50 } }而流式(stream: true)的响应是 SSE 格式,即一段段以data:为前缀的文本块,每个块里只有增量内容,最后以一个特殊的data: [DONE]结束。每个分块的 JSON 结构里,choices[0].delta.content保存的是这一小段新增的文本。前端要做的事,就是把这些 delta 一段一段拼接起来。
这两个格式的差异,直接决定了你前端代码里的解析逻辑长什么样。下面第三章就是基于流式格式来写的 Vue3 调用实现。如果你为图省事在公网环境用非流式,用户体验会很差——你想想一个聊天框转圈三秒钟才整段弹出文字,跟逐字输出给人的"活"的感觉差太多了。
3. 在 Vue3 里写聊天调用:fetch 流式读取、安全 header 与组件拆分
3.1 为什么用 fetch 而不是 Axios 来处理 SSE 流式输出
确定对接协议和数据结构之后,进入 Vue3 代码实现。很多同学第一反应是用 axios,因为项目里到处都是它。但针对流式聊天这个场景,我更推荐用浏览器原生 fetch,原因说得直白一点:axios 的底层 XHR 在处理 SSE 流式响应时,事件模型和 parse 方式都不如 fetch 的 ReadableStream 那么直观可控。
fetch 配合response.body.getReader()可以拿到一个真正的流读取器,每一块数据到达就立刻处理,天然契合 SSE。axios 处理流式需要额外依赖onDownloadProgress之类的 hack,还要注意它可能在响应未结束时对数据做 JSON.parse 导致报错。所以,不要在 Vue3 聊天功能里用 axios 做流式读取——这是我在踩过一次坑之后给你的直接结论。
以下是这个方案的核心技术栈:Vue3 组合式 API(<script setup>语法)、fetch 原生请求、SSE 增量解析。后端接口固定为通义千问兼容协议,不做多余封装,目标是让代码在本地开发环境和部署后的生产环境都能用同一套逻辑跑通。
3.2 最小可跑通的调用代码:流式拼接、中断与错误处理
下面是一个封装好的流式聊天请求函数,可以直接放进你的src/api/chat.js里。它负责发起请求、逐段解析 SSE 增量、通过回调把文本实时传给页面,同时处理错误和中断:
export async function streamChat({ messages, apiKey, model, onMessage, onDone, onError, signal }) { const url = 'https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions' const response = await fetch(url, { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${apiKey}` }, body: JSON.stringify({ model: model || 'qwen-plus', messages, stream: true }), signal }) if (!response.ok) { const errText = await response.text() throw new Error(`请求失败(${response.status}):${errText}`) } 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 trimmed = line.trim() if (!trimmed.startsWith('data:')) continue const data = trimmed.slice(5).trim() if (data === '[DONE]') { onDone && onDone() return } try { const json = JSON.parse(data) const delta = json.choices[0]?.delta?.content || '' if (delta) onMessage && onMessage(delta) } catch (e) { // 遇到解析失败的分片,留到下一轮缓冲区再处理 buffer = line + '\n' + buffer } } } }这段代码里有三个细节值得展开讲。第一,TextDecoder('utf-8')配合{ stream: true }是为了处理多字节字符被截断在分块边界的问题——比如一个中文字符的 UTF-8 编码可能横跨两个分块,如果不用这个方式解码,就会出现乱码。第二,buffer 机制是把不完整的行暂存到下一轮拼接,因为网络分块不保证正好按\n切开。第三,signal 参数透传给 fetch,让外部组件可以随时中断请求。
在 Vue3 组件里,调用这个函数的方式很直接。下面是一个最小示例:
import { ref } from 'vue' import { streamChat } from '../api/chat' const inputText = ref('') const chatContent = ref('') const isLoading = ref(false) let abortController = null async function sendMessage() { if (!inputText.value.trim() || isLoading.value) return const userMsg = inputText.value chatContent.value = '' inputText.value = '' isLoading.value = true abortController = new AbortController() try { await streamChat({ messages: [ { role: 'system', content: '你是一个有帮助的AI助手。' }, { role: 'user', content: userMsg } ], apiKey: import.meta.env.VITE_DASHSCOPE_API_KEY, model: 'qwen-plus', signal: abortController.signal, onMessage: (delta) => { chatContent.value += delta }, onDone: () => { isLoading.value = false } }) } catch (err) { if (err.name === 'AbortError') { console.log('请求已手动中断') } else { console.error('聊天请求失败:', err) } isLoading.value = false } } function stopChat() { if (abortController) { abortController.abort() } }这里import.meta.env.VITE_DASHSCOPE_API_KEY是从环境变量读取密钥,配置文件在项目根目录的.env或者.env.development里。注意 Vite 项目环境变量必须以VITE_为前缀才会暴露给前端代码,所以密钥文件一定要加进.gitignore,避免提交到仓库后被别人看到。密钥的常见获取方式是在阿里云百炼控制台里申请 API Key,拿到的是sk-开头的一串字符,这些信息记得不要写死在代码里。
3.3 一个 Vue3 聊天页面的配套组件结构:从 input 到渲染
有了请求封装和发送函数,还缺一个完整的组件结构把聊天页面组织起来。下面是一个实际可用的单文件组件,涵盖了消息列表渲染、输入框、发送和中断按钮:
<template> <div class="chat-container"> <div class="chat-messages"> <div v-for="(msg, index) in messageList" :key="index" class="message-row" :class="msg.role"> <div class="message-bubble">{{ msg.content }}</div> </div> </div> <div class="chat-input-area"> <textarea v-model="inputText" placeholder="请输入你的问题,Enter 发送,Shift+Enter 换行" @keydown.enter.prevent="handleEnter" ></textarea> <button @click="sendMessage" :disabled="isLoading">发送</button> <button v-if="isLoading" @click="stopChat">停止</button> </div> </div> </template> <script setup> import { ref } from 'vue' const inputText = ref('') const messageList = ref([]) const isLoading = ref(false) function handleEnter(e) { if (e.shiftKey) return sendMessage() } async function sendMessage() { // 调用 streamChat 的完整逻辑,见上一小节 } </script>这个组件结构里,messageList是渲染的核心数据源,每次发送时 push 一条 user 消息,然后建立一个空的 assistant 消息占位,流式回调里不断往这个占位消息的 content 上追加文本。这种方式能保持 Vue 的响应式更新按最小粒度触发,不会因为频繁修改整个列表而卡顿。
这里有一个非常值得养成习惯的细节:输入框用@keydown.enter.prevent做发送拦截,同时判断e.shiftKey让换行不受干扰。这个小细节在用户体验上相当重要,否则用户按 Shift+Enter 本来想换行,结果也把消息发出去了,或者按 Enter 时不是发送而是触发表单默认行为导致页面刷新。这类用户体验细节,第一次写可能注意不到,但做一两次聊天类项目就会很敏感。
4. 打包公网部署的硬骨头:跨域、构建配置与 Nginx 转发
4.1 前端直连通义千问的跨域限制:为什么本地好好的,部署到线上就崩
整个方案最容易让新手崩溃的环节,就是本地跑得好好的,打包部署到公网后,浏览器一把请求拦住,控制台报 CORS error。本质原因很简单:你的前端页面运行在https://your-domain.com,而通义千问接口在https://dashscope.aliyuncs.com,这属于跨域请求。
浏览器跨域策略会先发一个 OPTIONS 预检请求,服务器需要返回对应的 CORS 头才能放行。但阿里云百炼的公网网关在实践上默认不会对你的域名开放跨域白名单,所以在部署环境直接 fetch,基本必挂。有些同学会说,那本地怎么不挂呢——因为 Vite 开发服务器帮你做了代理,浏览器看到的请求是同源的。
做公网部署时,你会面对一个残酷的事实:单纯改前端配置解决不了生产环境的跨域,必须在服务端加一层转发。
4.2 Vite 构建配置:代理只在开发期有效,公网部署要换方案
Vite 开发服务器代理是本地调试的标配。在vite.config.js里这样配置:
export default defineConfig({ server: { proxy: { '/api': { target: 'https://dashscope.aliyuncs.com', changeOrigin: true, rewrite: (path) => path.replace(/^\/api/, '/compatible-mode') } } } })这个配置的含义是:前端请求/api/v1/chat/completions时,Vite 把它代理到https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions。changeOrigin: true让请求头里的 Host 变成目标域名,从而通过服务端的域名校验。
但请记住,这个代理只在开发环境有效。当你执行npm run build之后,产出的是纯静态文件,不再有 Vite 服务器的代理能力,线上所有的请求都直接由浏览器发出。所以生产环境的跨域问题,必须在真正的 Web 服务器(如 Nginx)上解决,而不是在 Vite 配置里想办法。这是"跳过构建产物、直接改 dev server 配置"最容易混淆的地方,也是很多新手部署后一脸茫然的根源。
4.3 Nginx 部署方案:静态资源托管与 API 反代,一份能抄的配置
公网部署的常见做法是,前端构建产物丢给 Nginx 托管静态资源,同时让 Nginx 把/api/路径的请求反向代理到通义千问接口。这样一来,浏览器请求的是同源/api/...,不存在跨域问题;真正的跨域请求发生在 Nginx 服务器端,而服务端到服务端的请求不受浏览器 CORS 策略约束。
下面是一份可直接落地的 Nginx 配置文件核心片段:
server { listen 80; server_name your-domain.com; root /var/www/chat-app/dist; index index.html; # 前端路由:Vue Router 的 history 模式需要这个配置 location / { try_files $uri $uri/ /index.html; } # API 反向代理到通义千问 location /api/ { proxy_pass https://dashscope.aliyuncs.com/compatible-mode/; proxy_set_header Host dashscope.aliyuncs.com; proxy_set_header Authorization $http_authorization; proxy_http_version 1.1; proxy_set_header Connection ""; chunked_transfer_encoding off; proxy_buffering off; proxy_cache off; proxy_read_timeout 600s; } }逐条拆解几个关键配置。proxy_pass末尾的/compatible-mode/是带 URI 的写法,含义是把/api/之后的路径拼接到目标地址后面,所以前端请求/api/v1/chat/completions最终会到达https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions,正好对应兼容协议的地址。proxy_set_header Authorization $http_authorization这段是把浏览器带来的 Authorization 头原样传给通义千问,这样密钥仍然存在前端,Nginx 不存储、不校验,只是透传——这个方法应对演示够用,但离安全还有距离,后面第六章的处理可以再进一步。
proxy_buffering off和proxy_cache off这两行是流式响应的命门。Nginx 默认会缓冲后端返回的数据,导致前端在全部文本到达前看不到任何增量输出。只有关掉缓冲,SSE 才能真正做到逐字推送。proxy_read_timeout 600s是防止长对话时 Nginx 在两次读取之间间隔太久而掐断连接,具体数值可以根据你的实际对话长度调整。
修改 Nginx 配置后别忘了执行nginx -t检查语法,然后systemctl reload nginx重载配置。这里还有一个隐藏细节:如果前端代码里请求地址写的是全量 URL(https://dashscope.aliyuncs.com/...),那 Nginx 的/api/转发配置根本不会生效,必须把前端请求地址改成相对路径/api/v1/chat/completions。这里的坑就是,本地开发靠 Vite 代理转发,线上靠 Nginx 转发,两处的路径规则要保持一致,最省事的做法是请求封装时用相对路径。
5. 打包公网最容易翻车的 5 个坑:现象、原因与解决
5.1 部署后请求返回 404,明明本地开发一切正常
现象:本地运行 Vue3 项目一切正常,npm run build后部署到 Nginx,页面能打开,但发消息时网络面板显示请求挂在某个路径上返回 404。
原因:最常见的是前端代码把请求地址写死了全量 URL,导致请求根本没走到 Nginx 配置的/api/转发规则上;另一个常见场景是proxy_pass的路径拼接规则没配对,导致实际请求的路径里多了一段前缀或少了一段。
解决:统一前后端路径。前端请求地址一律使用/api/v1/chat/completions这种相对路径。检查 Nginx 的location /api/块和proxy_pass末尾是否带路径,用curl http://localhost/api/v1/chat/completions在服务器本机验证一下路由是否通了。
5.2 流式聊天首次返回正常,后续乱码且速度变慢
现象:第一次发送消息,文字逐字出现,一切正常。第二三轮开始,输出里偶尔出现个别乱码,响应速度也明显变慢。
原因:前端代码里没有处理好 SSE 分块的行缓冲和 UTF-8 解码。一个中文字符的字节可能被 TCP 分块拆开,如果直接用JSON.parse处理每个分块,就会遇到"半截字符"导致解析失败,进而丢块。速度变慢则可能是因为某个分块解析失败后,把未读缓冲反复拼接,造成了无效循环。
解决:严格按照 3.2 节里buffer + decoder.decode(value, { stream: true })的模式来做行缓冲。注意 buffer 的作用是保存"不完整的行",而不是"解析失败的行"。如果发现某行确实解析失败,它大概率分块边界问题,配合 decoder 的流式解码模式一起使用,乱码问题就能解决。
5.3 API Key 写进打包产物,源码一眼看光
现象:打包部署后,打开浏览器开发者工具,在 Sources 面板或者 Network 面板的请求 Header 里,能直接看到完整的Authorization: Bearer sk-xxx。
原因:有的人把 key 直接写死在api/chat.js里或者在.env文件中没有加.gitignore,或者把 key 通过import.meta.env.VITE_DASKSCOPE_API_KEY暴露出来。Vite 会把构建时存在于前端代码里的环境变量直接打进 bundle,公网任何人只要看打包后的 JS 文件就能提取 key。
解决:这事的严重程度取决于你的用途。如果是个人演示项目、key 只按量计费且设置了预算上限,还可以接受;如果是正式产品,必须在服务端做一层转发,让 key 只存放在服务端环境变量里。具体做法在第六章展开。至少,你要去阿里云百炼控制台给 key 设置额度上限,并且养成密钥周期性更换的习惯。
5.4 部署域名还是 HTTP,浏览器直接拦截混合内容
现象:用 IP 地址直接访问部署的网站时,页面加载正常,但发消息时 Chrome 控制台提示 Mixed Content 错误,请求在浏览器层直接被拦截。
原因:通义千问接口强制 HTTPS,而你的网站走的是http://协议。浏览器默认不允许在一个 HTTPS 页面里发起 HTTP 请求,反过来,HTTP 页面里虽然可以请求 HTTPS,但很多浏览器对"HTTP 页面 + HTTPS 接口 + 跨域"的组合限制更严格,实际表现就是不让你发出去。
解决:给部署域名启用 HTTPS,用免费证书(Let's Encrypt 或者云厂商的免费版)配置到 Nginx 上,然后把server_name改成域名,并加一条从 HTTP 跳转到 HTTPS 的规则。对于临时用 IP 访问的场景,最稳妥的做法是意识到这个限制的存在,尽早配置域名和证书——这也是上公网演示的基本门槛。在快节奏的部署过程中,这一步经常被当成"细节"跳过,结果等正式演示时才暴露。
5.5 云端安全组全开,结果被刷爆账单
现象:把云服务器所有端口对外开放,或者把安全组配置成了/0全放行。某天早上起来发现账单比平时高了很多,查看监控发现 API 调用量暴增。
原因:如果 API Key 暴露在了前端,且没有设置消费上限,那公网上任何人都可以通过你的 key 调用模型。服务器端口全开只是一个诱因,真正的漏斗在密钥上。更隐蔽的情况是,云监控能看到服务器网络流量不大,但模型调用量居高不下——这说明攻击者绕过你的静态站点,直接拿着 key 在本地脚本里调用接口。
解决:两层防护,一是密钥本身设置额度上限和异常告警,二是把服务器安全组收敛为只放行 80/443 端口。在实际项目中,我在第一次接触这个场景时就是先"优化"了安全组,后来才在消费账单里发现问题,尽量不要走我这条弯路——先设限额再开门。
6. 把密钥藏到服务端的升级路径:一个 Nginx 代理即可完成的改造
6.1 前端只发对话,把 key 留给代理去填
前面章节的临时方案是浏览器携带 Authorization 头,通过 Nginx 原样透传到通义千问。第六感的同学可能已经意识到,那只是换了验证方式,并没有藏住密钥——只要是浏览器里有这个 key,就一定会被提取。真正做到可以安全上公网的路径,应该是把密钥从浏览器代码里彻底移除,让服务端帮你填。
改造思路不复杂:前端请求/api/chat时只带对话内容(messages),不带密钥。Nginx(或者你的后端服务)收到请求后,把密钥从环境变量读取出来,拼上 Authorization 头,再转发到通义千问。密钥自始至终只存在于服务端。
如果你没有独立后端,用 Nginx 配合一个小脚本可能是最快的方案。但纯 Nginx 无法动态读取环境变量拼 Header,所以更常见也是更可靠的方案是加一个轻量网关服务(Node/Java/Python 都可以)。我一般用的是一层 Node 服务,原理如下:
// server.js —— 一个极简的 Node 转发层 import express from 'express' import fetch from 'node-fetch' const app = express() app.use(express.json()) app.post('/api/chat', async (req, res) => { const { messages, model } = req.body const upstreamRes = await fetch('https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions', { method: 'POST', headers: { 'Content-Type': 'application/json', 'Authorization': `Bearer ${process.env.DASHSCOPE_API_KEY}` }, body: JSON.stringify({ model: model || 'qwen-plus', messages, stream: true }) }) res.setHeader('Content-Type', 'text/event-stream') res.setHeader('Cache-Control', 'no-cache') res.setHeader('Connection', 'keep-alive') // 透传上游的流式响应 upstreamRes.body.pipe(res) }) app.listen(3000, () => console.log('proxy server running on :3000'))注意这段代码是示意性质,生产环境要补全错误处理、请求超时、鉴权、限流等逻辑。这个服务部署在同一台服务器上,然后 Nginx 的/api/只转发到http://127.0.0.1:3000即可。这样浏览器端拿不到密钥,跨域问题也彻底消失——因为浏览器请求的是同源/api/,Node 服务内部再去请求通义千问,服务端到服务端没有 CORS 概念。
6.2 改造后的完整调用链:Dev 环境与生产环境分别怎么走
改造后的架构需要理清两条链路。开发环境,浏览器 → Vite Dev Server(代理) → Node 转发服务 → 通义千问;生产环境,浏览器 → Nginx → Node 转发服务 → 通义千问。两条链路里,浏览器都是只发消息,不暴露密钥。Node 服务在哪运行都在同一台云服务器上,密钥只存在于那台服务器的.env文件中。
对比一下两种方案的取舍。用 Node 转发层,多一个服务要维护、多一层故障点,但换来的是密钥安全、限流可以自己做、前端代码和生产环境不需要在密钥管理上操心。如果你只是做个课程演示或者内部小工具,Nginx 直连方案可能更省事,但要清楚它是"能跑"与"安全"之间的选择。
6.3 验证这套方案是否完善的三个检查点,以及建议保留的习惯
当你完成以上改造,可以用三个检查点来判断方案是否真正完善。第一,到打包后的产物 JS 文件里搜一下sk-,确认找不到任何密钥字符串;第二,浏览器开发者工具的 Network 面板里,请求头 Authorization 不再是Bearer sk-开头,而是已经移除或改成了别的鉴权方式;第三,在服务器上直接执行curl https://dashscope.aliyuncs.com/... -H "Authorization: Bearer $DASHSCOPE_API_KEY"验证密钥仍在服务端环境变量里生效。
最后分享一个小习惯:给密钥设置消费上限。在阿里云百炼的控制台里给 API Key 设置每月消费额度,外加异常告警。不用等方案做到第六层,从第一天写代码起就可以先把这件事做了。还有一点是我实践中自己保存下来的做法:每次构建部署时把.env文件单独管理,绝不进入构建产物,在 CI/CD 流程里直接注入。这个做法能少掉很多不必要的麻烦。
这些检查点都过了,你的 Vue3 调用通义千问的聊天应用才算真正达到了"可以打包公网使用"的标准——不只是能访问,而是密钥、流式体验、部署链路都站得住。希望这篇笔记能帮你把这条路的坑都趟平,让你少走几段我走过的弯路。
本文还有配套的精品资源,点击获取