基于 PicoServer 的 Live Reload 方案
热重载的本质热重载解决的问题文件保存后浏览器自动刷新。传统流程修改 → 保存 → 切换窗口 → F54 步热重载后修改 → 保存2 步核心流程文件变更 → 服务端检测 → WebSocket 推送 → 浏览器刷新二、实现2.1 文件监控与防抖using PicoServer;using System.Timers;using System.IO;string staticRoot “wwwroot”;var app new WebAPIServer();var watcher new FileSystemWatcher(staticRoot){EnableRaisingEvents true,IncludeSubdirectories true};var timer new System.Timers.Timer(200) { AutoReset false };timer.Elapsed async (_, _) await app.WsBroadcastAsync(“reload”);void OnChange(object s, FileSystemEventArgs e){var name Path.GetFileName(e.Name);if (string.IsNullOrEmpty(name) || name.StartsWith(‘.’) || name.StartsWith(‘~’))return; // 过滤 IDE 临时文件timer.Stop(); timer.Start(); // 200ms 内无新事件才触发}watcher.Changed OnChange;watcher.Created OnChange;watcher.Deleted OnChange;watcher.Renamed OnChange;防抖的必要性一次保存操作可能触发 Changed、Created、Renamed 多个事件防抖将其合并为一次广播避免页面重复刷新。2.2 WebSocket 广播app.enableWebSocket true;app.WsOnConnectionChanged (_, _) Task.CompletedTask;PicoServer 开启 WebSocket 后通过以下方法广播await app.WsBroadcastAsync(“reload”);2.3 客户端脚本注入在返回 HTML 时自动注入 WebSocket 客户端代码开发者无需手动添加if (filePath.EndsWith(“.html”, StringComparison.OrdinalIgnoreCase)){var content await File.ReadAllTextAsync(filePath);var script “”“”;content content.Contains(/body) ? content.Replace(/body, script /body) : content script; await res.WriteAsync(content, text/html);}2.4 路径放行/ws-reload 路径需要交给 WebSocket 处理静态文件中间件应放行if (path “/ws-reload”) return true;三、完整示例using PicoServer;using System.Timers;var app new WebAPIServer();app.enableWebSocket true;app.WsOnConnectionChanged (_, _) Task.CompletedTask;string staticRoot “wwwroot”;var watcher new FileSystemWatcher(staticRoot) { EnableRaisingEvents true };var timer new System.Timers.Timer(200) { AutoReset false };timer.Elapsed async (_, _) await app.WsBroadcastAsync(“reload”);watcher.Changed (s, e) { timer.Stop(); timer.Start(); };app.AddMiddleware(async (req, res) {var path req.Url?.AbsolutePath ?? “”;if (path “/ws-reload”) return true;// 解析文件路径 var reqPath path.TrimStart(/); if (string.IsNullOrEmpty(reqPath)) reqPath index.html; var filePath Path.GetFullPath(Path.Combine(staticRoot, reqPath)); // 安全检查 if (!filePath.StartsWith(staticRoot) || !File.Exists(filePath)) { res.StatusCode 404; await res.WriteAsync(Not Found); return false; } // HTML 文件注入脚本后返回 if (filePath.EndsWith(.html, StringComparison.OrdinalIgnoreCase)) { var content await File.ReadAllTextAsync(filePath); var script script new WebSocket(ws://location.host/ws-reload).onmessage e { if (e.data reload) location.reload(); }; /script ; content content.Contains(/body) ? content.Replace(/body, script /body) : content script; await res.WriteAsync(content, text/html); } else { // 非 HTML 文件直接返回 await res.SendFileAsync(filePath, false); } return false;});app.StartServer(8080);Console.WriteLine(“热重载已启动: http://localhost:8080”);await Task.Delay(Timeout.Infinite);四、扩展方向CSS 热更新广播 css 消息客户端仅刷新样式链接多设备同步局域网内多端同时刷新差异化刷新根据文件类型决定刷新策略如 .css 增量更新.cshtml 全页刷新五、关于 AOT热重载工具本身也应当保持轻量。Node.js 工具链通常需要安装运行时并下载数百 MB 依赖。通过 .NET AOT 编译整个热重载服务器可打包为 4-10 MB 的单文件无需安装 .NET 运行时拷贝即用。六、总结组件 代码量文件监控 防抖 15 行WebSocket 广播 1 行脚本注入 10 行总计 26 行26 行代码解决开发中最频繁的中断问题。