资讯动态

Vue3调用通义千问API的生产级部署方案

发布时间:2026/10/18 3:48:31 来源:尧图企业网站定制
简介本资源是一个基于 Vue 3 实现阿里通义千问 API 集成的可部署聊天应用示例面向前端开发者尤其是熟悉 Vue.js 框架、希望快速接入大模型交互能力的中高级学习者。项目完整覆盖从组件封装、消息响应式管理、API 调用到生产环境打包上线的全流程解决 Vue 应用中嵌入智能问答机器人时常见的跨域、鉴权、状态同步与静态部署等实际问题。压缩包共含 2000 个文件以 1054 个 JavaScript 文件含逻辑处理与 API 封装、514 个 CSS 样式文件含多种主题配色如 gruvbox、papercolor 等、256 个 SCSS 源文件及 39 个 TypeScript 类型定义为主辅以配置、文档与构建相关文件整体大小为 7.51MB结构清晰、模块分层明确。目前已有 312 人学习下载读者可直接复用其聊天组件结构、多主题样式体系、API 请求封装模式及 Vite 打包配置快速构建具备公网访问能力的 Vue 3通义千问落地原型。1. Vue3 调用通义千问 API 的实战落地一个能真正打包部署到公网的轻量级聊天界面你是不是也试过在 Vue3 项目里调用大模型 API本地npm run dev跑得飞起一执行npm run build打包完扔到 Nginx 或静态托管平台比如 GitHub Pages、Vercel、阿里云 OSSCDN页面打开空白、控制台报 CORS、或者请求直接 403/401这不是玄学——这是典型的「开发环境友好生产环境翻车」。本例不是教你怎么在浏览器里调用通义千问的 SDK那根本行不通而是实打实走通一条前端可控、后端无侵入、部署零配置、公网可访问的链路Vue3 前端只负责 UI 和请求转发所有鉴权、签名、流式响应处理、跨域代理逻辑全部下沉到一个极简 Node.js 中间层这个中间层甚至可以打包进前端静态资源一起部署比如用 Vite 插件注入也能独立跑在任意支持 HTTP 的轻量服务上。它不依赖 Express/Koa 全栈框架不引入 Redis 或数据库5 分钟就能搭好适合某高校课程设计、某公司内部工具、某图像处理 Demo 的快速集成。如果你要的是「能上线、能演示、能被非技术人员访问」的真实聊天界面而不是一个只能在 localhost:3000 看着 console.log 打印 response 的 demo这篇就是为你写的。2. 为什么不能直接在 Vue3 前端调用通义千问 OpenAPI2.1 浏览器安全沙箱的本质限制CORS 与密钥泄露风险通义千问官方 OpenAPI如https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation明确要求携带Authorization请求头格式为Bearer ${api_key}且必须通过 HTTPS 发起 POST 请求。但浏览器出于同源策略和 CORS 安全模型对以下两类行为天然禁止前端硬编码 API Key任何将sk-xxx明文写在.vue或env.d.ts里的做法只要代码被构建并部署Key 就等同于公开。抓包、查看源码、甚至搜索引擎缓存都能直接拿到。这不是“可能泄露”而是“必然泄露”。某开发者曾因在 GitHub 公开仓库提交.env文件3 小时内被刷走 2000 次调用配额账单暴涨。跨域请求被拦截即使你设法绕过密钥问题比如用 proxy通义千问服务端未在响应头中设置Access-Control-Allow-Origin: *或具体域名浏览器会直接阻断请求并在 DevTools Console 报错CORS header Access-Control-Allow-Origin missing。这不是前端代码写错了是服务端策略决定的——大厂 API 默认不开放浏览器直连这是行业共识。提示官方文档从未承诺支持浏览器端直调。其 SDKalibaba/dashscope的 Node.js 版本与 Web 版本是两套实现Web 版本质是包装了fetch但依然无法绕过上述限制。2.2 正确解法BFFBackend for Frontend模式的最小化实现我们不需要一个微服务集群只需要一个「胶水层」它接收 Vue3 前端发来的干净请求例如{ prompt: 你好 }在服务端完成从环境变量或配置中心读取DASHSCOPE_API_KEY绝不暴露给前端构造符合通义千问规范的请求体含model,input,parameters添加Authorization头与Content-Type: application/json转发请求并透传响应包括流式text/event-stream对错误状态码400/401/429/500做统一 JSON 格式降级返回。这个胶水层可以是一个独立的server.js基于原生http模块仅 80 行一个 Vite 插件开发时自动启动代理生产时生成静态 fallback 页面或直接复用 Vite 自带的proxy配置仅限开发环境生产仍需真实后端。本例选择第一种纯 Node.js 原生 HTTP 服务。它不依赖任何框架内存占用 15MB可一键部署到函数计算FC、轻量应用服务器SLB、甚至树莓派。关键在于——它和你的 Vue3 项目完全解耦前端只当它是个普通 REST 接口。2.3 通义千问 API 关键参数与响应结构解析通义千问text-generation接口核心字段如下以qwen-max模型为例字段类型必填说明modelstring✅固定为qwen-max或qwen-plus不可省略input.messagesarray✅消息数组每项含roleuser/assistant和content字符串parameters.temperaturenumber❌控制随机性0.0~1.0默认 0.85parameters.top_pnumber❌核采样阈值0.0~1.0默认 0.8parameters.max_tokensnumber❌最大输出 token 数1~4096默认 1024响应体为 JSON关键路径output.text完整回复文本非流式output.choices[0].message.content同上若启用stream: true则响应头为Content-Type: text/event-stream数据按 SSEServer-Sent Events格式分块推送每块为data: {output:{...}}\n\n。注意前端不能直接消费 SSE 流。fetch()的response.body是 ReadableStream但 Vue3 组件中需手动getReader().read()并拼接更稳妥的做法是让 BFF 层把流式响应转成普通 JSON 数组或由 BFF 缓存最终结果再返回。3. 从零搭建 Vue3 通义千问 BFF 全链路3.1 初始化 Vue3 项目并配置基础聊天 UI使用 Vite 创建标准 Vue3 项目推荐 TypeScriptnpm create vitelatest my-qwen-chat -- --template vue-ts cd my-qwen-chat npm install替换src/App.vue为极简聊天界面含输入框、发送按钮、消息列表!-- src/App.vue -- script setup langts import { ref, onMounted } from vue interface Message { id: string role: user | assistant content: string timestamp: Date } const messages refMessage[]([]) const inputText ref() const isLoading ref(false) // 模拟初始化欢迎消息 onMounted(() { messages.value.push({ id: welcome, role: assistant, content: 你好我是基于通义千问的聊天助手。请问我能帮你什么, timestamp: new Date() }) }) const sendMessage async () { if (!inputText.value.trim() || isLoading.value) return // 添加用户消息 const userMsg: Message { id: Date.now().toString(), role: user, content: inputText.value, timestamp: new Date() } messages.value.push(userMsg) inputText.value isLoading.value true try { // 调用 BFF 接口 const res await fetch(/api/chat, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ prompt: userMsg.content }) }) if (!res.ok) throw new Error(HTTP ${res.status}) const data await res.json() const assistantMsg: Message { id: (Date.now() 1).toString(), role: assistant, content: data.response || 抱歉我暂时无法回答。, timestamp: new Date() } messages.value.push(assistantMsg) } catch (err) { console.error(Chat error:, err) messages.value.push({ id: (Date.now() 2).toString(), role: assistant, content: 网络错误请稍后重试。, timestamp: new Date() }) } finally { isLoading.value false } } /script template div classchat-container div classmessages div v-formsg in messages :keymsg.id classmessage :classmsg.role strong{{ msg.role user ? 我 : 助手 }}/strong p{{ msg.content }}/p /div /div div classinput-area input v-modelinputText keyup.entersendMessage placeholder输入消息... :disabledisLoading / button clicksendMessage :disabledisLoading {{ isLoading ? 发送中... : 发送 }} /button /div /div /template style scoped .chat-container { max-width: 800px; margin: 0 auto; padding: 20px; font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, sans-serif; } .messages { height: 60vh; overflow-y: auto; border: 1px solid #eee; padding: 15px; margin-bottom: 15px; } .message { margin-bottom: 12px; line-height: 1.5; } .message.user { text-align: right; } .message.assistant { background: #f5f5f5; padding: 10px 14px; border-radius: 8px; display: inline-block; max-width: 80%; } .input-area { display: flex; gap: 10px; } .input-area input { flex: 1; padding: 10px; border: 1px solid #ccc; border-radius: 4px; } .input-area button { padding: 10px 20px; background: #007bff; color: white; border: none; border-radius: 4px; cursor: pointer; } .input-area button:disabled { background: #ccc; cursor: not-allowed; } /style这段代码做了三件事定义Message类型与响应式messages列表onMounted注入欢迎语避免白屏冷启动sendMessage方法先本地渲染用户消息 → 调用/api/chat→ 渲染助手回复或错误提示。注意这里fetch(/api/chat)是相对路径后续需通过 Vite 的proxy或 Nginx 重写指向真实 BFF 地址。3.2 编写极简 BFF 服务server.js在项目根目录新建server.js与package.json同级内容如下// server.js const http require(http) const url require(url) const https require(https) const querystring require(querystring) // 从环境变量读取密钥生产环境务必通过 .env 或系统变量注入 const DASHSCOPE_API_KEY process.env.DASHSCOPE_API_KEY || your_api_key_here const DASHSCOPE_API_URL https://dashscope.aliyuncs.com/api/v1/services/aigc/text-generation/generation // 创建 HTTP 服务 const server http.createServer((req, res) { const parsedUrl url.parse(req.url, true) // 仅处理 POST /api/chat if (req.method POST parsedUrl.pathname /api/chat) { let body req.on(data, chunk body chunk) req.on(end, () { try { const { prompt } JSON.parse(body) if (!prompt || typeof prompt ! string || prompt.trim().length 0) { sendError(res, 400, prompt is required and must be a non-empty string) return } // 构造通义千问请求体 const payload { model: qwen-max, input: { messages: [ { role: user, content: prompt.trim() } ] }, parameters: { temperature: 0.7, top_p: 0.8, max_tokens: 1024 } } // 配置 HTTPS 请求选项 const options { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${DASHSCOPE_API_KEY} } } // 发起请求 const apiReq https.request(DASHSCOPE_API_URL, options, (apiRes) { let responseData apiRes.setEncoding(utf8) apiRes.on(data, chunk { responseData chunk }) apiRes.on(end, () { try { const data JSON.parse(responseData) if (apiRes.statusCode 200 data.output?.text) { // 成功返回精简结构 res.writeHead(200, { Content-Type: application/json; charsetutf-8 }) res.end(JSON.stringify({ response: data.output.text.trim(), model: data.usage?.model || qwen-max, tokens: data.usage?.total_tokens || 0 })) } else { // 通义千问返回错误如 400/401 sendError(res, apiRes.statusCode, data.message || Qwen API error) } } catch (e) { sendError(res, 500, Parse Qwen response failed: ${e.message}) } }) }) apiReq.on(error, (err) { console.error(Qwen API request error:, err) sendError(res, 500, Qwen API network error: ${err.message}) }) // 写入请求体 apiReq.write(JSON.stringify(payload)) apiReq.end() } catch (e) { sendError(res, 400, Invalid JSON in request body: ${e.message}) } }) return } // 其他路径返回 404 res.writeHead(404, { Content-Type: application/json }) res.end(JSON.stringify({ error: Not Found })) }) function sendError(res, statusCode, message) { res.writeHead(statusCode, { Content-Type: application/json; charsetutf-8 }) res.end(JSON.stringify({ error: message })) } // 监听端口 const PORT process.env.PORT || 3001 server.listen(PORT, () { console.log(✅ BFF server running on http://localhost:${PORT}) console.log( Vue3 frontend should proxy /api/chat to this address) })这段代码的关键点零依赖仅用 Node.js 原生模块http,https,url,querystring无需npm install任何包环境变量隔离DASHSCOPE_API_KEY从process.env读取开发时可通过.env文件注入生产时由部署平台如阿里云 FC注入错误兜底对 JSON 解析失败、网络超时、通义千问返回非 200 等场景均返回结构化 JSON 错误前端可统一处理响应瘦身不透传原始通义千问响应只提取output.text、model、tokens三个前端真正需要的字段减少传输体积。运行方式# 开发时在新终端中启动 BFF DASHSCOPE_API_KEYsk-xxx node server.js # 此时 BFF 运行在 http://localhost:30013.3 配置 Vite 开发代理vite.config.ts为了让npm run dev时 Vue3 前端能调用本地 BFF修改vite.config.ts// vite.config.ts import { defineConfig } from vite import vue from vitejs/plugin-vue export default defineConfig({ plugins: [vue()], server: { port: 3000, proxy: { /api/chat: { target: http://localhost:3001, // 指向 BFF changeOrigin: true, secure: false, rewrite: (path) path.replace(/^\/api/, ) // 去掉 /api 前缀 } } } })此时执行npm run dev前端运行在http://localhost:3000浏览器访问/api/chat会被 Vite 自动代理到http://localhost:3001/api/chatBFF 收到请求后构造通义千问调用并返回结果注意rewrite配置确保 BFF 的server.js中parsedUrl.pathname /api/chat成立。若去掉rewriteBFF 需匹配/api/chat路径但实际收到的是/chat因代理去掉了/api需同步调整 BFF 逻辑。4. 生产环境部署如何让打包后的 dist 目录真正跑在公网4.1 理解 Vite 构建产物与静态服务的本质执行npm run build后Vite 生成dist/目录内含index.html入口 HTMLassets/文件夹JS/CSS/图片等资源文件名含 hash所有资源均为纯静态文件无需 Node.js 运行时。但问题来了index.html中的fetch(/api/chat)在生产环境指向哪里如果直接把dist/丢到 Nginx/api/chat会 404因为 Nginx 不知道这个路径该转发给谁。解决方案只有两个方案 A推荐Nginx 反向代理—— 将location /api/chat代理到已部署的 BFF 服务如http://bff.example.com方案 BBFF 与前端同服—— 让 BFF 服务同时托管dist/目录并将/路由返回index.html/api/chat路由走 BFF 逻辑。本例采用方案 B因其部署最简单只需一个进程、一个端口、一个域名适合中小项目。4.2 改造 server.js同时提供静态文件服务与 API修改server.js添加静态文件服务逻辑基于原生fs模块// server.js续写替换原 server.listen 部分 const fs require(fs) const path require(path) // 读取 dist 目录Vite 构建后 const DIST_DIR path.join(__dirname, dist) const INDEX_HTML path.join(DIST_DIR, index.html) // 检查 dist 是否存在 if (!fs.existsSync(DIST_DIR)) { console.warn(⚠️ dist directory not found. Run npm run build first.) console.log( Tip: This server can serve both static files and API.) } // 静态文件服务函数 function serveStaticFile(res, filePath) { const extname String(path.extname(filePath)).toLowerCase() const mimeTypes { .html: text/html, .js: application/javascript, .css: text/css, .json: application/json, .png: image/png, .jpg: image/jpg, .gif: image/gif, .svg: image/svgxml, .woff: font/woff, .woff2: font/woff2 } const contentType mimeTypes[extname] || application/octet-stream fs.readFile(filePath, (err, content) { if (err) { if (err.code ENOENT) { res.writeHead(404) res.end(404 Not Found) } else { res.writeHead(500) res.end(Server Error) } } else { res.writeHead(200, { Content-Type: contentType }) res.end(content, utf8) } }) } // 修改请求处理器区分 API 与静态资源 const server http.createServer((req, res) { const parsedUrl url.parse(req.url, true) // 1. 优先处理 API 路由 if (req.method POST parsedUrl.pathname /api/chat) { // ... 原有 API 处理逻辑保持不变... let body req.on(data, chunk body chunk) req.on(end, () { try { const { prompt } JSON.parse(body) if (!prompt || typeof prompt ! string || prompt.trim().length 0) { sendError(res, 400, prompt is required and must be a non-empty string) return } const payload { model: qwen-max, input: { messages: [ { role: user, content: prompt.trim() } ] }, parameters: { temperature: 0.7, top_p: 0.8, max_tokens: 1024 } } const options { method: POST, headers: { Content-Type: application/json, Authorization: Bearer ${DASHSCOPE_API_KEY} } } const apiReq https.request(DASHSCOPE_API_URL, options, (apiRes) { let responseData apiRes.setEncoding(utf8) apiRes.on(data, chunk { responseData chunk }) apiRes.on(end, () { try { const data JSON.parse(responseData) if (apiRes.statusCode 200 data.output?.text) { res.writeHead(200, { Content-Type: application/json; charsetutf-8 }) res.end(JSON.stringify({ response: data.output.text.trim(), model: data.usage?.model || qwen-max, tokens: data.usage?.total_tokens || 0 })) } else { sendError(res, apiRes.statusCode, data.message || Qwen API error) } } catch (e) { sendError(res, 500, Parse Qwen response failed: ${e.message}) } }) }) apiReq.on(error, (err) { console.error(Qwen API request error:, err) sendError(res, 500, Qwen API network error: ${err.message}) }) apiReq.write(JSON.stringify(payload)) apiReq.end() } catch (e) { sendError(res, 400, Invalid JSON in request body: ${e.message}) } }) return } // 2. 其他请求尝试作为静态文件服务 let filePath parsedUrl.pathname / ? INDEX_HTML : path.join(DIST_DIR, parsedUrl.pathname) // 安全检查防止路径遍历攻击如 /../../etc/passwd if (filePath.indexOf(DIST_DIR) ! 0) { res.writeHead(403) res.end(Forbidden) return } // 如果文件不存在返回 index.html支持 Vue Router history 模式 fs.access(filePath, fs.constants.F_OK, (err) { if (err) { // 文件不存在返回 index.htmlSPA fallback if (fs.existsSync(INDEX_HTML)) { serveStaticFile(res, INDEX_HTML) } else { res.writeHead(404) res.end(404 Not Found. Build dist first.) } } else { serveStaticFile(res, filePath) } }) }) // 监听端口 const PORT process.env.PORT || 3001 server.listen(PORT, () { console.log(✅ Production server running on http://localhost:${PORT}) console.log( Visit http://localhost:${PORT} to see your Vue3 Qwen app) console.log( Make sure to run npm run build before starting this server) })改造要点引入fs和path模块读取dist/目录新增serveStaticFile函数根据文件扩展名设置Content-Type请求处理器增加 fallback 逻辑若请求路径不是/api/chat则尝试读取对应静态文件若文件不存在返回index.html支持 Vue Router history 模式添加路径遍历防护if (filePath.indexOf(DIST_DIR) ! 0)确保不会读取dist外的文件。部署命令# 1. 构建前端 npm run build # 2. 设置环境变量并启动服务 DASHSCOPE_API_KEYsk-xxx PORT8080 node server.js此时访问http://your-server-ip:8080即可看到完整聊天界面所有请求均由同一进程处理。4.3 阿里云轻量应用服务器Lighthouse一键部署脚本为降低部署门槛提供一个deploy.sh脚本Linux 环境#!/bin/bash # deploy.sh APP_DIR/var/www/qwen-chat BACKUP_DIR/var/www/qwen-chat-backup-$(date %Y%m%d-%H%M%S) echo Starting deployment... # 1. 备份旧版本 if [ -d $APP_DIR ]; then echo Backing up current version... mkdir -p $BACKUP_DIR cp -r $APP_DIR/* $BACKUP_DIR/ 2/dev/null || true fi # 2. 创建新目录 mkdir -p $APP_DIR # 3. 复制构建产物 cp -r ./dist/* $APP_DIR/ # 4. 复制 server.js 和 package.json最小依赖 cp server.js package.json $APP_DIR/ # 5. 安装生产依赖仅需 node cd $APP_DIR echo Installing production dependencies... npm install --production 2/dev/null || echo ⚠️ npm install skipped (no dependencies) # 6. 写入环境变量实际应使用 systemd 或 PM2 管理 echo DASHSCOPE_API_KEYsk-xxx .env echo PORT80 .env # 7. 启动服务使用 nohup 后台运行 echo Starting server on port 80... nohup node server.js server.log 21 # 8. 输出访问地址 IP$(curl -s http://169.254.169.254/latest/meta-data/public-ipv4) echo ✅ Deployment success! echo Access your app at: http://$IP echo Logs: tail -f $APP_DIR/server.log使用方式chmod x deploy.sh ./deploy.sh注意生产环境强烈建议用PM2替代nohup支持进程守护、日志轮转、负载均衡。此处为简化演示。5. 避坑指南那些让我重启三次服务器的血泪经验5.1 现象本地开发一切正常部署到公网后fetch报TypeError: Failed to fetch原因前端fetch(/api/chat)在公网环境下浏览器实际请求的是http://your-domain.com/api/chat而你的 BFF 服务监听的是http://localhost:3001仅本机可访问。Nginx 或云服务器防火墙未开放对应端口或 BFF 未绑定0.0.0.0。解决修改server.js的server.listen显式绑定0.0.0.0server.listen(PORT, 0.0.0.0, () { // ← 关键 console.log(✅ Server listening on http://0.0.0.0:${PORT}) })同时检查云服务器安全组规则放行对应端口如 8080的 TCP 入方向流量。5.2 现象通义千问返回{message:Unauthorized,code:Unauthorized}原因DASHSCOPE_API_KEY环境变量未正确注入。常见错误包括在server.js中写死const DASHSCOPE_API_KEY sk-xxxGit 提交泄露使用.env文件但未安装dotenv包本例未用故不生效部署时未在系统级设置环境变量如export DASHSCOPE_API_KEYxxxAPI Key 已过期或被禁用登录 DashScope 控制台确认。解决永远不要硬编码 Key。改用系统环境变量# Linux/macOS export DASHSCOPE_API_KEYsk-xxx node server.js在server.js开头添加调试日志console.log( DASHSCOPE_API_KEY loaded:, !!DASHSCOPE_API_KEY) if (!DASHSCOPE_API_KEY) { console.error(❌ API Key is empty! Please set DASHSCOPE_API_KEY environment variable.) process.exit(1) }5.3 现象聊天消息显示乱码如 或????或中文被截断原因Node.jshttps.request默认不设置响应编码responseData为 BuffertoString()未指定编码导致 UTF-8 解析失败或通义千问响应头未声明charsetutf-8但内容实为 UTF-8。解决强制在https.request的回调中设置编码apiRes.setEncoding(utf8) // ← 关键必须加这一行 apiRes.on(data, chunk { responseData chunk // chunk 已是 string非 Buffer })同时确保sendError和最终res.end()也声明 charsetres.writeHead(200, { Content-Type: application/json; charsetutf-8 })5.4 现象输入长文本后BFF 报FATAL ERROR: Ineffective mark-compacts near heap limit Allocation failed - JavaScript heap out of memory原因Node.js 默认内存上限约 1.4GB而通义千问流式响应或大模型输出可能产生大量临时字符串触发 GC 压力。尤其在低配服务器如 1G 内存轻量服务器上极易发生。解决启动时增加内存限制node --max-old-space-size2048 server.js--max-old-space-size2048将内存上限设为 2GB。若仍不足需优化 BFF不缓存整个响应体改为边收边写SSE 流式透传限制max_tokens参数如设为 512对prompt做长度截断如prompt.substring(0, 2000)。5.5 现象用户刷新页面后聊天记录丢失原因当前实现将messages存在组件ref中属内存状态刷新即清空。这不是 Bug是设计选择——若需持久化必须引入后端存储如 localStorage、IndexedDB 或真实数据库。解决客户端轻量方案在App.vue中添加localStorage同步逻辑// 读取 onMounted(() { const saved localStorage.getItem(qwen-messages) if (saved) { try { messages.value JSON.parse(saved) } catch (e) { console.warn(Failed to parse saved messages, e) } } else { messages.value.push({ /* welcome message */ }) } }) // 保存每次新增消息后 const addMessage (msg: Message) { messages.value.push(msg) localStorage.setItem(qwen-messages, JSON.stringify(messages.value)) } // sendMessage 中调用 addMessage 而非直接 push注意localStorage有 5MB 限制且同源共享适合单用户短期记录。6. 进阶技巧为生产环境添加请求限流与 Token 统计6.1 为什么需要限流一个真实案例某高校课程设计项目上线后被学生批量脚本调用每秒 20 请求导致通义千问配额当日耗尽API 返回429 Too Many Requests全体用户无法使用。而 DashScope 控制台的「调用配额」是全局的无法按 IP 或用户粒度控制。我们必须在 BFF 层自己做限流。6.2 基于内存的简易令牌桶实现无依赖在server.js顶部添加限流模块// rate-limiter.js可内联到 server.js class SimpleRateLimiter { constructor(options {}) { this.max options.max || 10 // 每分钟最多 10 次 this.windowMs options.windowMs || 60 * 1000 // 时间窗口 60 秒 this.headers new Map() // key: ip, value: { count, resetTime } } isAllowed(ip) { const now Date.now() const record this.headers.get(ip) || { count: 0, resetTime: now this.windowMs } if (now record.resetTime) { p a hrefhttps://download.csdn.net/download/guoruijun_2012_4/90480244 stylecolor:#ec7500;font-size:14px; 本文还有配套的精品资源点击获取 /a img altmenu-r.4af5f7ec.gif srchttps://csdnimg.cn/release/wenkucmsfe/public/img/menu-r.4af5f7ec.gif stylewidth:16px;margin-left:4px;vertical-align:text-bottom;cursor:text; /p

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑