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

