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

如何通过Node.js Express监听Firestore变化并向React实时推送数据

实现Firestore实时更新向React客户端推送的解决方案

核心问题

你当前使用的HTTP POST请求属于请求-响应模式,单次请求仅能返回一次响应,完全无法适配Firestore onSnapshot这种持续监听集合变化的场景。要实现服务器向客户端主动推送实时数据,必须改用支持长连接、主动推送的技术,推荐两种方案:Server-Sent Events(SSE)或WebSocket。


方案一:Server-Sent Events(SSE)

SSE是基于HTTP的单向推送技术,实现成本低,适合仅需服务器向客户端推送更新的场景。

后端(Express)代码改造

const express = require('express');
const admin = require('firebase-admin');
const app = express();

// 配置CORS(适配前端跨域请求)
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000'); // 替换为你的前端地址
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.header('Access-Control-Allow-Credentials', 'true');
  if (req.method === 'OPTIONS') return res.sendStatus(200);
  next();
});

// SSE接口:监听Orders集合并推送实时更新
app.get('/api/orders/sse', async (req, res) => {
  // 设置SSE专属响应头
  res.setHeader('Content-Type', 'text/event-stream');
  res.setHeader('Cache-Control', 'no-cache');
  res.setHeader('Connection', 'keep-alive');

  // 先发送初始全量数据
  const ordersCol = admin.firestore().collection('Orders');
  const initialSnapshot = await ordersCol.get();
  const initialList = initialSnapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
  res.write(`data: ${JSON.stringify({ list: initialList })}\n\n`);

  // 持续监听集合变化,推送更新数据
  const unsubscribe = ordersCol.onSnapshot(snapshot => {
    const updatedList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
    // SSE消息格式要求:data: 开头,结尾必须加\n\n
    res.write(`data: ${JSON.stringify({ list: updatedList })}\n\n`);
  }, err => {
    console.error('Firestore监听错误:', err);
    res.end();
  });

  // 客户端断开连接时,清理Firestore监听
  req.on('close', () => {
    unsubscribe();
    res.end();
  });
});

app.listen(3001, () => console.log('服务器运行于 http://localhost:3001'));

前端(React)代码改造

import { useEffect, useState } from 'react';

export default function OrdersList() {
  const [orderList, setOrderList] = useState([]);
  const token = '你的用户授权token'; // 替换为实际token获取逻辑

  useEffect(() => {
    // 建立SSE连接
    const eventSource = new EventSource('http://localhost:3001/api/orders/sse', {
      headers: { Authorization: `Bearer ${token}` }
    });

    // 接收服务器推送的更新
    eventSource.onmessage = (e) => {
      const res = JSON.parse(e.data);
      setOrderList(res.list);
    };

    // 处理连接错误
    eventSource.onerror = (err) => {
      console.error('SSE连接异常:', err);
      eventSource.close();
    };

    // 组件卸载时关闭SSE连接
    return () => eventSource.close();
  }, [token]);

  return (
    <div className="orders-container">
      {orderList.map(order => (
        <div key={order.id} className="order-item">
          <p>订单ID:{order.id}</p>
          <p>订单状态:{order.status}</p>
        </div>
      ))}
    </div>
  );
}

方案二:WebSocket(双向通信)

如果需要客户端向服务器发送指令(比如过滤条件),或者需要双向交互,推荐使用WebSocket。这里用ws库实现。

后端(Express)代码改造

const express = require('express');
const admin = require('firebase-admin');
const WebSocket = require('ws');
const http = require('http');

const app = express();
const server = http.createServer(app);
const wss = new WebSocket.Server({ server });

// 配置CORS
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Origin', 'http://localhost:3000');
  res.header('Access-Control-Allow-Headers', 'Content-Type, Authorization');
  res.header('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');
  res.header('Access-Control-Allow-Credentials', 'true');
  if (req.method === 'OPTIONS') return res.sendStatus(200);
  next();
});

// 处理WebSocket连接
wss.on('connection', (ws, req) => {
  // 验证客户端token
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return ws.close(401, '未授权');

  // 发送初始数据
  const sendInitialData = async () => {
    const ordersCol = admin.firestore().collection('Orders');
    const snapshot = await ordersCol.get();
    const list = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
    ws.send(JSON.stringify({ type: 'INIT', list }));
  };
  sendInitialData();

  // 监听Firestore变化并推送
  const unsubscribe = admin.firestore().collection('Orders').onSnapshot(snapshot => {
    const updatedList = snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
    ws.send(JSON.stringify({ type: 'UPDATE', list: updatedList }));
  }, err => {
    console.error('Firestore监听错误:', err);
    ws.close(500, '服务器内部错误');
  });

  // 处理客户端发送的消息(双向交互示例)
  ws.on('message', (data) => {
    const msg = JSON.parse(data);
    if (msg.type === 'FILTER') {
      // 这里可以根据客户端发送的过滤条件,调整Firestore监听逻辑
      console.log('收到过滤条件:', msg.condition);
    }
  });

  // 客户端断开时清理资源
  ws.on('close', () => unsubscribe());
});

server.listen(3001, () => console.log('服务器运行于 http://localhost:3001'));

前端(React)代码改造

import { useEffect, useState } from 'react';

export default function OrdersList() {
  const [orderList, setOrderList] = useState([]);
  const [wsConn, setWsConn] = useState(null);
  const token = '你的用户授权token';

  useEffect(() => {
    // 建立WebSocket连接
    const socket = new WebSocket('ws://localhost:3001', {
      headers: { Authorization: `Bearer ${token}` }
    });

    socket.onopen = () => console.log('WebSocket连接成功');

    // 接收服务器推送
    socket.onmessage = (e) => {
      const res = JSON.parse(e.data);
      if (res.type === 'INIT' || res.type === 'UPDATE') {
        setOrderList(res.list);
      }
    };

    socket.onerror = (err) => {
      console.error('WebSocket异常:', err);
      socket.close();
    };

    socket.onclose = () => console.log('WebSocket连接关闭');

    setWsConn(socket);

    // 组件卸载时关闭连接
    return () => socket.close();
  }, [token]);

  // 向服务器发送过滤指令示例
  const sendFilter = () => {
    if (wsConn && wsConn.readyState === WebSocket.OPEN) {
      wsConn.send(JSON.stringify({
        type: 'FILTER',
        condition: { status: 'pending' }
      }));
    }
  };

  return (
    <div className="orders-container">
      <button onClick={sendFilter}>只看待处理订单</button>
      {orderList.map(order => (
        <div key={order.id} className="order-item">
          <p>订单ID:{order.id}</p>
          <p>订单状态:{order.status}</p>
        </div>
      ))}
    </div>
  );
}

关键注意事项

  • CORS配置:必须确保后端允许前端域名跨域访问,否则会出现请求失败。
  • 资源清理:客户端断开连接时,一定要取消Firestore的onSnapshot监听,避免内存泄漏。
  • 身份验证:无论是SSE还是WebSocket,都要在连接时验证用户token,防止未授权访问。

内容的提问来源于stack exchange,提问作者Cloud Music

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.25 16:47:52