You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在whatsapp-web.js中向前端正确发送QR码字符串响应

问题根因

你这个问题和QR生成延迟没有关系,是代码逻辑存在两个硬伤:

  • 响应提前返回:路由末尾的res.status(200).json("ok")是同步执行的,在WhatsApp客户端异步触发qr事件之前,HTTP响应就已经发给前端了,等QR实际生成时响应通道早就关闭,再调用res.send()只会报「无法在响应发送后设置请求头」的错误,自然无法返回QR内容。
  • 实例重复创建:你把WhatsApp Client的初始化写在了路由回调里,每次前端发请求都会新建一个客户端实例,既会无意义占用大量内存,还会导致事件重复绑定、重复触发登录流程等异常。
    另外你现在引入的qrcode-terminal仅支持在终端打印二维码,直接返回原始qr字符串前端无法直接扫码使用,需要转成可渲染的图片格式。
修复方案
  1. 将Client实例改为全局单例,服务启动时只初始化一次,相关事件也只绑定一次
  2. 移除路由里提前返回的响应逻辑,等拿到QR内容、或确认客户端已登录后再返回结果
  3. 增加超时判断,避免QR生成失败时前端请求一直挂死
  4. 用qrcode依赖包把原始qr字符串转成base64格式图片,前端拿到后可以直接放到img标签渲染
  5. 把自动发消息的逻辑从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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 04:27:25