如何在whatsapp-web.js中向前端正确发送QR码字符串响应
问题根因
你这个问题和QR生成延迟没有关系,是代码逻辑存在两个硬伤:
- 响应提前返回:路由末尾的
res.status(200).json("ok")是同步执行的,在WhatsApp客户端异步触发qr事件之前,HTTP响应就已经发给前端了,等QR实际生成时响应通道早就关闭,再调用res.send()只会报「无法在响应发送后设置请求头」的错误,自然无法返回QR内容。 - 实例重复创建:你把WhatsApp Client的初始化写在了路由回调里,每次前端发请求都会新建一个客户端实例,既会无意义占用大量内存,还会导致事件重复绑定、重复触发登录流程等异常。
另外你现在引入的qrcode-terminal仅支持在终端打印二维码,直接返回原始qr字符串前端无法直接扫码使用,需要转成可渲染的图片格式。
修复方案
- 将Client实例改为全局单例,服务启动时只初始化一次,相关事件也只绑定一次
- 移除路由里提前返回的响应逻辑,等拿到QR内容、或确认客户端已登录后再返回结果
- 增加超时判断,避免QR生成失败时前端请求一直挂死
- 用
qrcode依赖包把原始qr字符串转成base64格式图片,前端拿到后可以直接放到img标签渲染 - 把自动发消息的逻辑从
ready事件中拆分出来,单独做接口触发,避免每次服务重启、客户端重连时自动给目标号码发消息造成骚扰
首先安装需要的依赖:
npm i qrcode whatsapp-web.js express
修正后的完整代码如下:
const router = require("express").Router(); const qrcode = require("qrcode"); const { Client, LocalAuth } = require("whatsapp-web.js"); // 全局单例初始化客户端,不要放到路由内 const client = new Client({ authStrategy: new LocalAuth(), }); let cachedQr = null; let isClientReady = false; // 全局事件只绑定一次 client.on("qr", (qrContent) => { cachedQr = qrContent; isClientReady = false; }); client.on("ready", () => { console.log("Client is ready!"); isClientReady = true; cachedQr = null; }); client.on("authenticated", () => { cachedQr = null; }); // 服务启动时只初始化一次客户端 client.initialize(); // 获取二维码/登录状态接口 router.get("/", async (req, res) => { try { // 已登录状态直接返回结果 if (isClientReady) { return res.status(200).json({ status: "ready", msg: "WhatsApp客户端已登录" }); } // 已有缓存的二维码直接返回 if (cachedQr) { const qrImg = await qrcode.toDataURL(cachedQr); return res.status(200).json({ status: "qr", qr: qrImg }); } // 尚未生成二维码时等待事件触发,设置10秒超时 const qrContent = await new Promise((resolve, reject) => { const timer = setTimeout(() => { reject(new Error("二维码生成超时,请重试")); }, 10000); const handleQr = (data) => { clearTimeout(timer); client.off("qr", handleQr); resolve(data); }; client.on("qr", handleQr); }); const qrImg = await qrcode.toDataURL(qrContent); return res.status(200).json({ status: "qr", qr: qrImg }); } catch (err) { return res.status(500).json({ status: "error", msg: err.message }); } }); // 单独封装发消息接口,按需调用 router.post("/send-message", async (req, res) => { if (!isClientReady) { return res.status(400).json({ msg: "客户端未登录,请先扫码授权" }); } try { const numberList = [ { phone: "number1", name: "name1" }, { phone: "number2", name: "name2" }, ]; const result = []; for (const item of numberList) { const text = `teste ${item.name}`; const chatId = item.phone.substring(1) + "@c.us"; await client.sendMessage(chatId, text); result.push({ phone: item.phone, success: true }); } return res.status(200).json({ msg: "发送完成", data: result }); } catch (err) { return res.status(500).json({ msg: "发送失败", error: err.message }); } }); module.exports = router;
补充说明
- 前端拿到接口返回的
qr字段是base64格式的图片地址,直接赋值给img标签的src属性即可渲染出可扫描的二维码,不需要额外做转换。 - 前端可以每隔2-3秒轮询一次该接口,当返回
status: ready时说明扫码成功,停止轮询即可。 - 如果需要返回二维码原始文本而非图片,去掉
qrcode.toDataURL的转换逻辑直接返回原始字符串即可,但前端需要自行引入二维码生成库渲染,不如直接返回base64图片使用方便。
内容的提问来源于stack exchange,提问作者Balahaus Bengala
相关产品推荐
相关产品推荐

