Blazor开发时禁用捆绑后CSS的热重载方法
Blazor默认使用了CSS隔离与捆绑,导致CSS修改后不能实时更新(需要重启程序再次捆绑才生效)。即使手工管理CSS放至wwwroot/css目录下,也需要刷新页面才能更新CSS。
解决方法:
- 定时扫描各razor页面对应的CSS变化,有变化时复制集中到一个CSS中。这样可以保留隔离的结构,release模式下可以继续隔离
- 在index.html中增加定时检测功能,检测后台CSS有无变化,有则立即更新CSS,这样可以避免整页刷新
禁用捆绑
@@@code<PropertyGroup> <OutputType>Exe</OutputType> <TargetFrameworks>net8.0</TargetFrameworks> <ImplicitUsings>enable</ImplicitUsings> <Nullable>enable</Nullable> <DisableScopedCssBundling Condition="'$(Configuration)'=='Debug'">true</DisableScopedCssBundling> <!--<ScopedCssEmbeddedResourceNamespace>$(RootNamespace).wwwroot</ScopedCssEmbeddedResourceNamespace>--> <!--<PublishAot>true</PublishAot>--> <!--<InvariantGlobalization>true</InvariantGlobalization>--> </PropertyGroup>
扫描服务类
@@@code@csharp@public interface ICssService { Task<bool> AdjustReloadCss(); } public class CssService : ICssService { private readonly HttpClient _httpClient; public CssService(HttpClient httpClient) { _httpClient = httpClient; //启动扫描 startScan(); } [Conditional("DEBUG")] void startScan() { string _workPath = AppDomain.CurrentDomain.BaseDirectory; int interval = 3; scan(_workPath); Console.WriteLine("ok,scan..."); //文件监视没有工作,那就定时扫描 System.Timers.Timer timer = new System.Timers.Timer(); timer.Interval = interval * 1000; timer.Elapsed += (s, e) => scan(_workPath); timer.Enabled = true; } /// <summary> /// 扫描,只有有一处更新,则重新生成整个文件 /// </summary> /// <param name = "_workPath"></param> void scan(string _workPath) { int idx = _workPath.IndexOf("bin\\Debug"); //只在DEBUG模式下处理 if (idx == -1) return; string root = _workPath.Substring(0, idx - 1); string wwwroot = Path.Combine(root, "wwwroot"); string cssFile = Path.Combine(wwwroot, "css", $"{nameof(UsbClient)}.css"); DateTime lastChangeTime = DateTime.MinValue; if (File.Exists(cssFile)) { lastChangeTime = new FileInfo(cssFile).LastWriteTime; } bool hasChange = false; //todo:尝试使用文件监视 List<string> files = Directory.GetFiles(root, "*.razor.css", SearchOption.AllDirectories).Where(s => { if (s.Substring(root.Length + 1).StartsWith("bin") || s.Substring(root.Length + 1).StartsWith("wwwroot")) return false; return true; }).ToList(); foreach (var s in files) { if (new FileInfo(s).LastWriteTime > lastChangeTime) { hasChange = true; break; } } if (hasChange) { //所有文件合并 File.WriteAllLines(cssFile, files.Select(r => File.ReadAllText(r)).ToArray()); File.WriteAllText(Path.Combine(wwwroot, "data", "cssChangeTime.json"), DateTime.Now.Ticks.ToString()); } } long cssLastChangeTime = 0; public async Task<bool> AdjustReloadCss() { #if DEBUG var newTime = long.Parse(await _httpClient.GetStringAsync(@"data/cssChangeTime.json")); if (newTime > cssLastChangeTime) { cssLastChangeTime = newTime; return true; } #endif return false; } }
在index.html中增加函数
@@@code<!-- 调试时 --> <link href="./css/UsbClient.css" rel="stylesheet"> <script> window.refreshStyle = (s) => { var prefix = window.location.origin + '/css/UsbClient.css'; var links = document.querySelectorAll('link[rel="stylesheet"]'); links.forEach(function (link) { if (link.href.startsWith(prefix)) { var newLink = document.createElement('link'); newLink.rel = 'stylesheet'; newLink.type = 'text/css'; newLink.href = link.href; link.parentNode.replaceChild(newLink, link); } }); } </script>
在相应的Layout中增加判断
@@@code@csharp@
// DEBUG模式下自动刷新CSS(强制重刷) @inject IJSRuntime JSRuntime @inject ICssService cssService #if DEBUG async void adjustReloadCss(object state) { var needReload = await cssService.AdjustReloadCss(); if (needReload) { await JSRuntime.InvokeVoidAsync("refreshStyle", ""); } } private Timer _timer; protected override async Task OnAfterRenderAsync(bool firstRender) { await base.OnAfterRenderAsync(firstRender); _timer = new Timer(adjustReloadCss, null, TimeSpan.Zero, TimeSpan.FromSeconds(2)); } #endif