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

如何在ActionFunction中获取useLoaderData的client对象并避免重复触发loader

问题解答

一、优化Action中使用MQTT Client的方案

核心误区

你当前把服务器端的MQTT Client序列化后传到客户端,再通过表单提交回服务器的做法完全错误:MQTT Client是带网络连接上下文的服务器端对象,序列化会丢失所有连接状态,既无效又存在安全风险,而且违背了Remix“服务器端函数共享资源”的设计逻辑。

正确实现方式

1. 服务器端缓存MQTT Client实例

用单例模式封装MQTT连接,避免每次Loader都新建连接(浪费资源),同时让Loader和Action能共享同一个实例:

// utils/mqtt-client.ts
import mqtt from "mqtt";

let mqttClient: mqtt.MqttClient | null = null;
let connectionPromise: Promise<mqtt.MqttClient> | null = null;

export async function getMqttClient() {
  if (mqttClient) return mqttClient;
  if (connectionPromise) return connectionPromise;

  connectionPromise = new Promise((resolve, reject) => {
    const client = mqtt.connect('172.31.100.181', options);
    client.on('connect', () => {
      mqttClient = client;
      resolve(client);
    });
    client.on('error', (err) => {
      connectionPromise = null;
      reject(err);
    });
  });

  return connectionPromise;
}

2. 简化Loader函数

Loader不再返回Client对象,只返回连接状态即可(Client在服务器端通过工具类获取):

export const loader: LoaderFunction = async ({ request }: LoaderArgs) => {
  let error = null;
  let connected = false;
  try {
    await getMqttClient(); // 仅确认连接状态
    connected = true;
  } catch (e: any) {
    error = `Client connect error: ${e.message}`;
    console.error('Client connect error: ', e.message);
  }

  return json({
    error,
    connected
  });
};

3. 简化表单与Action逻辑

表单无需传递Client,直接用标准submit按钮;Action中通过工具类获取Client执行操作:

<fetcher.Form method="post" action="/mqtt">
  <button type="submit" name="_action" value="publish_all">All devices</button>
  <button type="submit" name="_action" value="publish_bedroom">Bedroom devices</button>
</fetcher.Form>

Action函数:

export const action: ActionFunction = async ({ request }) => {
  const formData = await request.formData();
  const action = formData.get('_action');

  try {
    const client = await getMqttClient();
    switch (action) {
      case 'publish_all':
        client.publish('iotbso/feeds/test', 'publish all message');
        break;
      case 'publish_bedroom':
        client.publish('bedroom/feeds', 'bedroom message');
        break;
      default:
        return json({ success: false, message: 'Unknown action' }, { status: 400 });
    }
    return json({ success: true, message: 'Publish successful' });
  } catch (e: any) {
    return json({ success: false, message: `Publish error: ${e.message}` }, { status: 500 });
  }
};

二、解决Loader重复触发的问题

原因分析

你之前用fetcher.submit手动提交表单时,默认会触发当前路由的Loader重新执行;同时旧Loader每次都会新建MQTT连接,放大了重复触发的负面影响。

解决方法

  1. 使用标准submit按钮:上面优化后的表单用了标准<button type="submit">,无需手动调用fetcher.submit,Remix会自动处理表单提交,且不会无意义触发Loader(因为Loader不再返回动态资源,仅返回状态)。
  2. 手动提交时禁用Loader刷新:如果必须用fetcher.submit,添加reloadDocument: false选项阻止Loader触发:
onClick={() => {
  fetcher.submit(
    { '_action': 'publish_all' },
    { 
      method: "post", 
      action: "/mqtt",
      reloadDocument: false
    }
  );
}}

额外优化

  • 资源泄漏防护:在服务器关闭时断开MQTT连接:
// server.ts 或项目入口文件
process.on('exit', async () => {
  const client = await getMqttClient();
  client.end();
});
  • 多用户隔离:如果是多用户应用,可改用Remix会话存储为每个用户保存独立Client实例,并在会话过期时自动断开连接。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.24 12:17:36