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

Flutter通过WebSocket实时读取多JSON文件实现方案问询

多JSON文件实时同步Flutter实现方案

问题修正点

  • 移除Flutter端冗余的HTTP初始请求(WebSocket连接后服务端会主动推送初始数据)
  • 修复Flutter代码中Widget树未闭合的语法错误
  • 优化服务端Watcher逻辑,全局监听文件而非每个连接创建单独Watcher,避免资源浪费
  • 完善Flutter端WebSocket重连时的资源清理,防止内存泄漏

修正后的WebSocket服务端代码

const WebSocket = require('ws');
const chokidar = require('chokidar');
const fs = require('fs').promises;

const jsonFilePath1 = 'C:/xampp/htdocs/Flutter/data.json';
const jsonFilePath2 = 'C:/xampp/htdocs/Flutter/data2.json';

// 创建两个WebSocket服务实例
const wss1 = new WebSocket.Server({ port: 8080 });
const wss2 = new WebSocket.Server({ port: 8081 });

// 封装发送文件内容到所有客户端的函数
async function sendFileContent(wss, filePath) {
  try {
    const data = await fs.readFile(filePath, 'utf8');
    wss.clients.forEach(client => {
      if (client.readyState === WebSocket.OPEN) {
        client.send(data);
      }
    });
  } catch (err) {
    console.error(`读取文件 ${filePath} 失败:`, err);
  }
}

// 初始化第一个文件的监听和初始发送
(async () => {
  await sendFileContent(wss1, jsonFilePath1);
  const watcher1 = chokidar.watch(jsonFilePath1);
  watcher1.on('change', async () => {
    console.log(`文件 ${jsonFilePath1} 已更新`);
    await sendFileContent(wss1, jsonFilePath1);
  });
})();

// 初始化第二个文件的监听和初始发送
(async () => {
  await sendFileContent(wss2, jsonFilePath2);
  const watcher2 = chokidar.watch(jsonFilePath2);
  watcher2.on('change', async () => {
    console.log(`文件 ${jsonFilePath2} 已更新`);
    await sendFileContent(wss2, jsonFilePath2);
  });
})();

// 处理新连接,发送当前文件内容
wss1.on('connection', async (ws) => {
  console.log('WebSocket 服务端1已连接客户端');
  await sendFileContent(wss1, jsonFilePath1);
  ws.on('close', () => {
    console.log('WebSocket 服务端1客户端已断开');
  });
});

wss2.on('connection', async (ws) => {
  console.log('WebSocket 服务端2已连接客户端');
  await sendFileContent(wss2, jsonFilePath2);
  ws.on('close', () => {
    console.log('WebSocket 服务端2客户端已断开');
  });
});

console.log('WebSocket服务端已启动,监听端口8080和8081');

修正后的Flutter客户端代码

import 'dart:async';
import 'dart:convert';
import 'package:flutter/material.dart';
import 'package:web_socket_channel/web_socket_channel.dart';
import 'package:web_socket_channel/status.dart' as status;

void main() {
  runApp(const MyApp());
}

class MyApp extends StatelessWidget {
  const MyApp({super.key});

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      title: 'JSON实时同步',
      theme: ThemeData(
        useMaterial3: true,
        primarySwatch: Colors.blue,
      ),
      home: const HomePage(),
    );
  }
}

class HomePage extends StatefulWidget {
  const HomePage({super.key});

  @override
  State<HomePage> createState() => _HomePageState();
}

class _HomePageState extends State<HomePage> {
  String _title1 = 'Loading...';
  String _status1 = '';
  List _items1 = [];

  String _title2 = 'Loading...';
  String _status2 = '';
  List _items2 = [];

  WebSocketChannel? _channel1;
  WebSocketChannel? _channel2;
  Timer? _reconnectTimer1;
  Timer? _reconnectTimer2;

  @override
  void initState() {
    super.initState();
    _connectWebSocket1();
    _connectWebSocket2();
  }

  void _connectWebSocket1() {
    // 关闭旧连接防止资源泄漏
    _channel1?.sink.close(status.goingAway);
    
    _channel1 = WebSocketChannel.connect(Uri.parse('ws://localhost:8080'));

    _channel1?.stream.listen(
      (data) => _handleWebSocketData(data, 1),
      onError: (error) {
        print('WebSocket1错误: $error');
        _scheduleReconnect1();
      },
      onDone: () {
        print('WebSocket1连接已关闭');
        _scheduleReconnect1();
      },
    );
  }

  void _connectWebSocket2() {
    // 关闭旧连接防止资源泄漏
    _channel2?.sink.close(status.goingAway);
    
    _channel2 = WebSocketChannel.connect(Uri.parse('ws://localhost:8081'));

    _channel2?.stream.listen(
      (data) => _handleWebSocketData(data, 2),
      onError: (error) {
        print('WebSocket2错误: $error');
        _scheduleReconnect2();
      },
      onDone: () {
        print('WebSocket2连接已关闭');
        _scheduleReconnect2();
      },
    );
  }

  void _scheduleReconnect1() {
    _reconnectTimer1?.cancel();
    _reconnectTimer1 = Timer(const Duration(seconds: 5), () {
      if (mounted) _connectWebSocket1();
    });
  }

  void _scheduleReconnect2() {
    _reconnectTimer2?.cancel();
    _reconnectTimer2 = Timer(const Duration(seconds: 5), () {
      if (mounted) _connectWebSocket2();
    });
  }

  void _handleWebSocketData(dynamic data, int blockNumber) {
    try {
      final decodedData = json.decode(data);
      setState(() {
        if (blockNumber == 1) {
          _items1 = decodedData["blocks"][0]["items"];
          _title1 = decodedData["blocks"][0]["title"];
          _status1 = decodedData["blocks"][0]["status"];
        } else if (blockNumber == 2) {
          _items2 = decodedData["blocks"][0]["items"];
          _title2 = decodedData["blocks"][0]["title"];
          _status2 = decodedData["blocks"][0]["status"];
        }
      });
    } catch (e) {
      print('解析WebSocket数据失败: $e');
    }
  }

  Color _getStatusColor(String status) {
    switch (status) {
      case 'red':
        return Colors.red;
      case 'green':
        return Colors.green;
      case 'yellow':
        return Colors.yellow;
      default:
        return Colors.grey;
    }
  }

  @override
  void dispose() {
    _reconnectTimer1?.cancel();
    _reconnectTimer2?.cancel();
    _channel1?.sink.close(status.goingAway);
    _channel2?.sink.close(status.goingAway);
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    double screenWidth = MediaQuery.of(context).size.width;
    double containerWidth = screenWidth * 0.3;
    return Scaffold(
      body: SafeArea(
        child: Padding(
          padding: const EdgeInsets.only(top: 100, bottom: 50, left: 50, right: 50),
          child: Row(
            crossAxisAlignment: CrossAxisAlignment.start,
            children: [
              // 第一个数据容器
              Container(
                width: containerWidth,
                decoration: BoxDecoration(
                  color: Colors.grey.shade200,
                  border: Border.all(color: Colors.black),
                  borderRadius: BorderRadius.circular(10),
                ),
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Expanded(
                          child: Center(
                            child: Text(
                              _title1,
                              style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ),
                        ),
                        Container(
                          width: 20,
                          height: 20,
                          decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            color: _getStatusColor(_status1),
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 10),
                    ..._items1.map((item) => Container(
                          margin: const EdgeInsets.only(top: 16.0),
                          padding: const EdgeInsets.all(16.0),
                          decoration: BoxDecoration(
                            color: Colors.white,
                            border: Border.all(color: Colors.black),
                            borderRadius: BorderRadius.circular(10),
                          ),
                          width: double.infinity,
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text("ID: ${item['id']}"),
                              Text("Name: ${item['name']}"),
                              Text("Description: ${item['description']}"),
                            ],
                          ),
                        )),
                  ],
                ),
              ),
              const SizedBox(width: 50),
              // 第二个数据容器
              Container(
                width: containerWidth,
                decoration: BoxDecoration(
                  color: Colors.grey.shade200,
                  border: Border.all(color: Colors.black),
                  borderRadius: BorderRadius.circular(10),
                ),
                padding: const EdgeInsets.all(16.0),
                child: Column(
                  crossAxisAlignment: CrossAxisAlignment.start,
                  children: [
                    Row(
                      children: [
                        Expanded(
                          child: Center(
                            child: Text(
                              _title2,
                              style: const TextStyle(
                                fontSize: 20,
                                fontWeight: FontWeight.bold,
                              ),
                            ),
                          ),
                        ),
                        Container(
                          width: 20,
                          height: 20,
                          decoration: BoxDecoration(
                            shape: BoxShape.circle,
                            color: _getStatusColor(_status2),
                          ),
                        ),
                      ],
                    ),
                    const SizedBox(height: 10),
                    ..._items2.map((item) => Container(
                          margin: const EdgeInsets.only(top: 16.0),
                          padding: const EdgeInsets.all(16.0),
                          decoration: BoxDecoration(
                            color: Colors.white,
                            border: Border.all(color: Colors.black),
                            borderRadius: BorderRadius.circular(10),
                          ),
                          width: double.infinity,
                          child: Column(
                            crossAxisAlignment: CrossAxisAlignment.start,
                            children: [
                              Text("ID: ${item['id']}"),
                              Text("Name: ${item['name']}"),
                              Text("Description: ${item['description']}"),
                            ],
                          ),
                        )),
                  ],
                ),
              ),
            ],
          ),
        ),
      ),
    );
  }
}

关键实现说明

  1. 服务端优化:
    • 全局创建文件Watcher,避免每个客户端连接重复创建监听,减少资源消耗
    • 使用异步文件读取API(fs.promises)避免回调地狱
    • 文件变更时主动推送给所有在线客户端,保证所有连接的客户端都能同步更新
  2. 客户端优化:
    • 移除冗余HTTP请求,依赖WebSocket连接后的初始推送完成数据加载
    • 重连前先关闭旧连接,防止内存泄漏
    • 使用mounted状态判断确保组件存活时才执行重连操作
    • 用...map替代for-in循环构建列表项,符合Flutter最佳实践
    • 修复原代码中Widget树未正确闭合的语法错误
  3. 运行流程:
    • 启动Node.js服务端,监听文件变更和WebSocket连接
    • 启动Flutter客户端,自动建立WebSocket连接
    • 服务端主动推送当前JSON文件内容到客户端,完成初始加载
    • 当JSON文件被修改时,服务端检测到变更并推送最新内容,客户端自动更新UI

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 22:39:50