
1. 项目概述一次从桌面到浏览器的技术远征如果你和我一样是个在WPFWindows Presentation Foundation领域摸爬滚打了多年的老C#开发者那么最近几年你肯定也感受到了那股来自Web的、无法忽视的浪潮。桌面应用依然强大但“随时随地打开浏览器就能用”的便利性对用户和业务来说吸引力太大了。我们手头往往积累了大量成熟的WPF项目它们架构精良、逻辑复杂、用户体验出色但似乎被“锁”在了Windows桌面上。直接重写成本高、风险大无异于一场豪赌。有没有一种方法能让我们珍视的WPF代码资产平滑地“驶入”Web的海洋最近我和团队就完成了一次这样的“技术迁徙”将一个名为“英雄联盟客户端”的复杂WPF演示项目完整地迁移到了OpenSilver平台上使其能够直接在浏览器中运行。这不仅仅是一个技术实验更是一次验证WPF技术栈在Web时代生命力的实战。项目本身是一个功能丰富的客户端模拟器包含了主页、商城、个人资料、社交、设置等多个高度定制化的模块UI交互复杂大量使用了WPF的CustomControl、数据绑定、命令和资源系统。我们的目标很明确在不重写核心业务逻辑和UI代码的前提下实现从.exe到.html的无缝过渡。OpenSilver就是这个问题的答案。它是一个开源的、用于将Silverlight和WPF应用迁移到现代Web的平台其核心是使用WebAssemblyWasm和C#/.NET来在浏览器中运行你的XAML和C#代码。听起来很美好但实际迁移中会遇到哪些坑架构该如何调整性能表现如何这正是本文想要分享的。我将以这个“英雄联盟客户端”项目为蓝本拆解从WPF到OpenSilver迁移的全过程涵盖架构设计、关键技术点、实操步骤以及我们踩过的那些“坑”。无论你是正在评估迁移可行性还是已经着手准备希望这些一线经验能为你提供一张可靠的“航海图”。2. 迁移前的核心评估与架构适配在动第一行代码之前充分的评估是成功迁移的一半。盲目开始很容易陷入“代码泥潭”。我们的评估主要围绕以下几个核心维度展开它们直接决定了迁移的可行性和工作量。2.1 技术栈兼容性深度剖析OpenSilver的目标是提供与WPF高度兼容的API子集但“高度兼容”不等于“完全一致”。第一步就是厘清项目依赖的核心技术点并逐一核对。UI框架与XAML解析这是重中之重。我们的项目重度依赖CustomControl自定义控件而非UserControl。幸运的是OpenSilver对CustomControl的支持非常出色包括Themes/Generic.xaml中的控件模板、DefaultStyleKey等机制都能正常工作。这意味着我们数百个自定义控件的样式和行为逻辑有望大部分保留。需要警惕的是WPF中一些较为“古老”或复杂的Trigger、DataTrigger在OpenSilver中的支持可能有限或行为有差异我们后期大量改用VisualStateManager来规避这个问题。数据绑定与转换Binding、INotifyPropertyChanged、IValueConverter是WPF的命脉。OpenSilver对此的支持是核心卖点几乎可以无缝迁移。我们的ViewModel层和大量的值转换器如日期格式化、状态枚举转颜色无需修改。但要注意绑定模式Mode中的一些边缘情况以及x:Reference等高级绑定语法需要在实际迁移中测试。命令与交互ICommand接口如RelayCommand被完美支持。然而WPF中常用的System.Windows.Interactivity或Microsoft.Xaml.Behaviors库用于处理交互行为如EventTrigger在OpenSilver的WebAssembly环境中是不可用的。我们的项目原本就倾向于在CustomControl的代码后台中直接处理事件并调用ICommand这反而成了一种优势。如果你的项目重度依赖Blend行为需要提前规划替代方案例如将行为逻辑重构到ViewModel或自定义控件的代码中。依赖注入与模块化我们项目使用了自研的基于.NET Standard的轻量级DI框架Jamesnet.Foundation用于管理View和ViewModel的映射与生命周期。OpenSilver运行在.NET Standard/.NET 6上标准的Microsoft.Extensions.DependencyInjection或我们自研的框架都能正常运行。关键在于要确保你的DI容器注册和解析逻辑不依赖于任何WPF特有的类型如Dispatcher或者做好抽象。第三方库与本地功能这是最大的风险点。项目引用的NuGet包需要逐一检查。纯.NET Standard或.NET类库如Newtonsoft.Json、AutoMapper通常没问题。但任何依赖特定Windows API如P/Invoke调用user32.dll、WPF原生渲染、或者系统托盘等桌面特性的库都无法在浏览器中工作。我们的项目相对“纯净”主要业务逻辑不涉及系统级调用这是能顺利迁移的前提。2.2 项目结构重组策略WPF项目通常是单个桌面应用程序。迁移到OpenSilver意味着要构建一个Web应用。项目结构需要调整以适应新的输出目标和开发流程。我们最终采用了与参考文章类似的、清晰分离的三项目结构LeagueOfLegends.Core (.NET Standard 2.0类库)这是所有迁移工作的核心。我们将原WPF项目中所有与平台无关的代码移入此处。包括所有的ViewModel、模型Model、业务逻辑服务。所有的CustomControl控件定义XAML和C#代码。所有的IValueConverter、自定义MarkupExtension。通用的工具类、扩展方法。核心的依赖注入注册逻辑。 这个类库不引用任何WPF或OpenSilver的UI程序集只引用.NET Standard兼容的库。它是我们代码资产的“纯净”核心。LeagueOfLegends.WPF (.NET Framework/.NET 6 WPF应用)这是一个“瘦客户端”。它引用上面的Core项目并包含App.xaml和MainWindow.xaml等WPF应用的启动入口。WPF平台特定的资源字典如果需要但尽量把样式放到Core的控件模板中。任何暂时无法剥离的、WPF特有的适配代码作为过渡。 这个项目的作用是1) 保证我们的Core库在传统WPF环境下依然能正常工作方便调试和对比2) 作为迁移过程中的“安全网”。LeagueOfLegends.OpenSilver (.NET 6 OpenSilver应用)这是新的Web前端项目。它同样引用Core项目并包含OpenSilver的启动类继承自Application。MainPage.xaml作为Web应用的根页面。OpenSilver平台特定的初始化逻辑如服务注册、路由配置等。浏览器环境特有的适配层例如将“打开文件对话框”替换为基于HTML5的文件上传组件。关键决策点为什么不直接把原WPF项目改成OpenSilver项目因为依赖关系会乱套。通过创建Core类库我们强制进行了“关注点分离”清晰界定了哪些代码是平台无关的可迁移资产哪些是平台特定的需要适配或重写。这为未来可能迁移到其他平台如移动端 via .NET MAUI也打下了基础。2.3 构建与调试环境搭建开发体验的顺畅度直接影响效率。OpenSilver提供了两种主要的开发模式在浏览器中调试真实环境这是最终目标。OpenSilver项目编译后会生成标准的HTML、CSS、JavaScript和一个.wasm文件包含你的.NET代码。你需要一个本地Web服务器来运行。简单的话可以用dotnet run启动项目它通常会启动一个Kestrel服务器。更接近生产环境的是将发布后的文件部署到IIS、Nginx或任何静态文件服务器。调试时浏览器开发者工具中的“.NET”选项卡需要安装相应扩展可以让你像调试JavaScript一样设置C#断点、查看调用堆栈体验非常棒。在WPF模拟器中调试快速迭代OpenSilver SDK包含一个“Simulator”或“Live Preview”功能。它本质上是一个特殊的WPF宿主应用程序可以加载并运行你的OpenSilver XAML/C#代码并模拟浏览器环境。这是迁移初期最宝贵的工具它的启动速度远超启动浏览器和Web服务器并且可以直接使用Visual Studio的所有调试功能断点、监视、即时窗口。当你需要快速验证一个控件在OpenSilver下的渲染效果或行为时用模拟器能节省大量时间。我们的策略是在模拟器中完成大部分UI和基础逻辑的调试稳定后再到真实浏览器中进行集成和兼容性测试。3. 核心技术点迁移与适配实战评估和架构准备好后就进入了真刀真枪的代码迁移阶段。下面是我们遇到的核心技术挑战及解决方案。3.1 CustomControl的迁移样式与模板的重生如前所述我们的UI完全由CustomControl构建。迁移过程相对平滑但细节决定成败。1. 控件库项目引用变更在Core类库中我们需要将原WPF项目中对PresentationFramework、WindowsBase等的引用替换为OpenSilver对应的兼容库。实际上OpenSilver提供了一个名为OpenSilver的NuGet包它包含了WPF API的兼容实现。我们在Core库中安装这个包注意选择与你的OpenSilver项目版本匹配的预览版。安装后大部分System.Windows.Controls下的类型都能正常解析。2. 处理XAML命名空间这是第一个容易出错的地方。WPF的XAML头通常是ResourceDictionary xmlnshttp://schemas.microsoft.com/winfx/2006/xaml/presentation xmlns:xhttp://schemas.microsoft.com/winfx/2006/xaml在OpenSilver的Core库中我们通常不需要修改这个命名空间因为OpenSilver的设计就是兼容这个URI的。但是当你在OpenSilver应用项目中引用这些控件时一切会正常工作。如果遇到解析问题可以尝试使用OpenSilver特定的设计时命名空间但通常不需要。3. 样式与模板的调整StaticResourcevsDynamicResourceOpenSilver对DynamicResource的支持有限尤其是在性能敏感的Web环境。我们审查了所有控件模板将能确定的DynamicResource都改为StaticResource。对于需要换肤的主题资源我们通过数据绑定和IValueConverter来实现动态切换而不是依赖资源字典的动态查找。模板绑定TemplateBinding在控件模板内部绑定到控件的依赖属性这是CustomControl的基石。OpenSilver完全支持TemplateBinding行为与WPF一致。复杂画笔和视觉效果WPF中诸如RadialGradientBrush、VisualBrush或BitmapEffect等高级渲染特性在OpenSilver中可能不支持或效果不同。我们的项目使用了较多的线性渐变和阴影DropShadowEffect。OpenSilver支持基本的LinearGradientBrush和BoxShadow通过特定属性或转换器模拟但需要测试渲染结果。对于不支持的复杂效果我们降级为简单的纯色或CSS3能实现的阴影方案。4. 代码后台的兼容性控件的C#代码后台需要检查所有使用的API。例如Dispatcher.Invoke在OpenSilver中UI线程调度可以通过Dispatcher.CheckAccess()和BeginInvoke完成但API可能略有不同。我们封装了一个简单的UIThreadHelper类内部根据编译符号#if OPENSILVER来调用不同平台的Dispatcher。DependencyProperty定义和使用方式与WPF几乎完全相同这是迁移顺利的关键。OverrideMetadata在静态构造函数中为依赖属性重写元数据此操作在OpenSilver中同样有效。3.2 视觉状态管理从Trigger到VisualStateManager的进化WPF中控制控件状态变化如鼠标悬停、按下、禁用常用Style.Triggers或ControlTemplate.Triggers。虽然OpenSilver支持一部分Trigger但更推荐且兼容性更好的是使用VisualStateManagerVSM。VSM在WPF、Silverlight、UWP/WinUI中都是一等公民OpenSilver对其有良好支持。迁移操作 我们系统地扫描了所有控件模板将Trigger重构为VisualState和VisualTransition。例如一个按钮的悬停效果WPF Trigger方式ControlTemplate TargetTypeButton Border x:NameBorder BackgroundLightGray ContentPresenter / /Border ControlTemplate.Triggers Trigger PropertyIsMouseOver ValueTrue Setter TargetNameBorder PropertyBackground ValueDarkGray/ /Trigger /ControlTemplate.Triggers /ControlTemplateOpenSilver VSM方式ControlTemplate TargetTypeButton Border x:NameBorder BackgroundLightGray ContentPresenter / VisualStateManager.VisualStateGroups VisualStateGroup x:NameCommonStates VisualState x:NameNormal/ VisualState x:NameMouseOver Storyboard ObjectAnimationUsingKeyFrames Storyboard.TargetNameBorder Storyboard.TargetPropertyBackground DiscreteObjectKeyFrame KeyTime0 ValueDarkGray/ /ObjectAnimationUsingKeyFrames /Storyboard /VisualState /VisualStateGroup /VisualStateManager.VisualStateGroups /Border /ControlTemplate然后在控件代码中在适当的时候如OnMouseEnter、OnMouseLeave调用VisualStateManager.GoToState(this, MouseOver, true)。实操心得虽然迁移VSM需要一些工作量但它带来了额外好处1)状态定义更清晰VisualStateGroup让互斥状态如Normal/Pressed/Disabled管理更简单2)支持平滑过渡VisualTransition可以轻松添加状态切换时的动画3)更好的跨平台一致性为将来可能的其他前端框架迁移铺平道路。3.3 数据绑定与命令系统的微调数据绑定是MVVM的桥梁我们在此处遇到的挑战不多但有几个注意事项绑定延迟DelayWPF的Binding有Delay属性用于防止频繁更新如搜索框。OpenSilver早期版本可能不支持。我们的解决方案是在ViewModel中使用Rx.NETReactive Extensions的Throttle操作符来处理文本输入或者使用自定义的Behavior在OpenSilver中需用附加属性模拟来实现防抖。绑定到索引器绑定到集合的索引器如{Binding MyList[0]}在OpenSilver中可能需要额外验证。我们倾向于在ViewModel中暴露一个单独的属性来避免此类绑定。命令执行上下文ICommand.Execute方法的参数object parameter传递需要小心。在WPF中可以通过CommandParameter绑定传递复杂对象。在OpenSilver中同样支持但要确保传递的对象是可序列化的如果涉及某些边界情况或者考虑使用RelativeSource绑定到DataContext中的某个属性来获取参数。事件到命令的绑定如前所述没有System.Windows.Interactivity。我们采用两种模式对于简单控件如Button直接使用Command和CommandParameter属性。对于自定义控件或复杂交互在控件的代码后台监听事件如PointerPressed然后从DataContext中获取ICommand并执行。我们通常会为控件定义一个ICommand类型的依赖属性以便在XAML中绑定。// 在CustomControl的代码后台 protected override void OnPointerPressed(PointerRoutedEventArgs e) { base.OnPointerPressed(e); var command MyCustomCommand; // 这是一个DependencyProperty绑定自ViewModel var parameter MyCommandParameter; // 同样是一个DependencyProperty if (command?.CanExecute(parameter) true) { command.Execute(parameter); } e.Handled true; }3.4 资源字典与动态主题切换WPF中常使用ResourceDictionary来集中管理颜色、笔刷、样式等资源并通过MergedDictionaries或动态加载来实现主题切换。OpenSilver支持ResourceDictionary但动态切换的逻辑需要调整。策略静态资源将基础的、不变的颜色和样式定义在App.xaml或Page级别的资源字典中。这些资源在应用启动时加载OpenSilver处理得很好。动态主题我们放弃了在运行时通过代码加载和替换整个资源字典的方式因为这在WebAssembly环境中可能引发性能问题和复杂的资源查找逻辑。取而代之的是我们采用了一种“变量绑定”策略在ViewModel或一个全局的ThemeService中定义一系列代表主题颜色的属性如PrimaryColor、BackgroundColor。在控件模板中不再使用StaticResource引用固定的颜色而是使用Binding绑定到这些全局主题属性。当用户切换主题时ThemeService发出属性变更通知所有绑定到这些属性的UI元素自动更新。!-- 传统StaticResource方式 -- Border Background{StaticResource PrimaryBrush}/ !-- 动态绑定方式 -- Border Background{Binding Source{x:Static local:ThemeService.Instance}, PathPrimaryBrush}/这种方式更符合MVVM模式且避免了资源字典动态加载的兼容性问题。当然这要求对现有的控件模板进行一些改造。4. 浏览器环境下的特定挑战与解决方案脱离Windows桌面运行在沙盒化的浏览器环境中带来了全新的挑战。4.1 文件系统与本地IO的抽象WPF应用可以自由访问文件系统在权限范围内。浏览器则严格限制。任何涉及System.IO.File、Directory的代码都需要重写。我们的方案配置文件/用户数据改用浏览器提供的存储API。OpenSilver封装了IsolatedStorage类似于Silverlight但更现代的做法是使用Blazor生态中常用的IJSRuntime调用JavaScript的localStorage、sessionStorage或IndexedDB。我们封装了一个IBrowserStorageService接口在WPF实现中用文件IO在OpenSilver实现中用JS互操作。文件上传WPF中用OpenFileDialog。在OpenSilver中我们需要在XAML中放置一个input typefileHTML元素通过OpenSilver的HtmlPresenter控件或自定义控件然后通过JS互操作读取文件内容并传递回C#。文件下载无法直接保存到用户指定路径。我们使用IJSRuntime调用URL.createObjectURL和a标签的click()方法来触发浏览器下载。// 在OpenSilver项目中的服务实现 public class BrowserFileService : IFileService { private readonly IJSRuntime _jsRuntime; public async Task SaveFileAsync(string fileName, byte[] data) { // 将byte[]转换为base64或Blob通过JS触发下载 await _jsRuntime.InvokeVoidAsync(downloadFile, fileName, Convert.ToBase64String(data)); } } // 对应的wwwroot/js/fileInterop.js function downloadFile(filename, base64Data) { const link document.createElement(a); link.href data:application/octet-stream;base64, base64Data; link.download filename; document.body.appendChild(link); link.click(); document.body.removeChild(link); }4.2 网络请求与跨域问题WPF中使用HttpClient或WebClient。OpenSilver中同样可以使用HttpClient因为.NET运行时在WebAssembly中提供了此实现。但是浏览器有严格的同源策略CORS。关键点API地址确保你的后端API服务器正确配置了CORS头允许你的OpenSilver应用所在域名进行跨域请求。认证与Cookie如果应用涉及身份认证需要处理Cookie或Token在浏览器环境中的存储与发送。HttpClient默认会处理同域请求的Cookie但跨域时需要后端配合设置Access-Control-Allow-Credentials: true且前端请求需设置HttpClientHandler的UseCookies和AllowAutoRedirect等属性或在OpenSilver中通过JS互操作来管理。WebSocket如果使用实时通信OpenSilver支持WebSocket用法与.NET中类似。4.3 性能优化与加载体验一个复杂的WPF应用编译成WebAssembly后初始下载的.wasm文件可能很大几MB到几十MB。虽然浏览器会缓存但首次加载速度是用户体验的关键。优化措施链接器裁剪确保OpenSilver项目文件.csproj中启用了链接器PublishTrimmedtrue/PublishTrimmed。这能移除未使用的代码显著减小程序集大小。但需要仔细测试因为过度裁剪可能误删通过反射调用的代码。延迟加载将非核心的功能模块如设置页面、帮助文档拆分成独立的程序集DLL并在用户首次访问时再动态加载。OpenSilver支持基于路由的延迟加载。资源优化图片、字体等静态资源应进行压缩WebP格式、字体子集化。将资源放在wwwroot目录下并通过相对路径引用让Web服务器提供高效的静态文件服务。启动画面在.wasm加载和.NET运行时初始化期间显示一个友好的启动画面或加载动画。OpenSilver应用模板通常自带一个简单的加载界面你可以自定义它来提升体验。4.4 与JavaScript的互操作性尽管OpenSilver让你能用C#写大部分代码但有时必须与JavaScript世界交互例如使用现有的JS图表库、地图组件或者调用浏览器特有的API。使用IJSRuntime这是Blazor/OpenSilver中与JS交互的标准方式。注入IJSRuntime服务然后调用InvokeAsyncT或InvokeVoidAsync方法。// 调用JS函数 var result await _jsRuntime.InvokeAsyncstring(jsFunctions.sayHello, OpenSilver); // 定义可供JS调用的C#方法需要继承IJSInProcessRuntime等具体看版本封装第三方JS库对于复杂的JS库最佳实践是创建一个C#封装类内部通过IJSRuntime调用JS对外提供强类型的C# API。这保持了项目代码的整洁和类型安全。5. 部署、调试与持续集成迁移完成后的最后一步是让应用在真实环境中跑起来。5.1 部署到生产环境OpenSilver项目编译发布后会生成一个标准的静态网站文件集合index.html入口文件。wwwroot文件夹包含所有的CSS、JavaScript、图片等资源。_framework文件夹包含核心的blazor.webassembly.js引导文件、.wasm文件以及所有的.NET程序集DLL。你可以将这些文件直接部署到任何静态网站托管服务上例如Azure Static Web Apps与GitHub Actions集成自动化部署体验极佳。GitHub Pages免费适合演示项目。任何Web服务器如IIS、Nginx、Apache。只需将站点根目录指向包含这些文件的文件夹即可。重要配置确保Web服务器为.wasm、.dll等文件配置正确的MIME类型。例如在IIS中需要为.wasm添加MIME类型application/wasm。5.2 调试技巧与问题排查迁移过程中调试是家常便饭。除了之前提到的模拟器调试浏览器调试是最终手段。浏览器开发者工具.NET调试安装“Microsoft Edge DevTools for .NET”或“Chromium-based browsers .NET debugging support”扩展后可以在“Sources”或“.NET”标签中看到你的C#源代码并设置断点。控制台输出Console.WriteLine输出的内容会显示在浏览器的JavaScript控制台Console中这是最直接的日志输出方式。网络面板查看.wasm、DLL、API请求的加载情况诊断网络问题和加载性能。常见运行时错误MissingMethodException最常见。通常是因为链接器裁剪过度或者引用了某个在OpenSilver中不存在的API。检查堆栈跟踪找到缺失的方法然后在项目文件中使用TrimmerRootAssembly或DynamicDependency属性来告诉链接器保留它。XamlParseExceptionXAML解析错误。仔细检查错误信息指向的XAML文件和行号。常见原因是不支持的属性值、错误的命名空间引用、或不兼容的标记扩展。布局或渲染错乱检查浏览器控制台是否有CSS错误。OpenSilver最终会将XAML渲染为HTML和CSS。使用浏览器的元素检查器查看生成的DOM结构和CSS样式可能与WPF的渲染引擎有细微差别需要调整控件模板中的边距、对齐方式等。5.3 迁移后的测试策略迁移绝非一劳永逸全面的测试至关重要。单元测试Core类库中的业务逻辑、ViewModel、IValueConverter等应使用xUnit、NUnit等框架编写单元测试。由于Core是.NET Standard这些测试可以在任何平台上运行。UI自动化测试这是难点。传统的基于UI自动化的测试框架如TestComplete、Appium可能不直接支持OpenSilver渲染的HTML。可以考虑端到端测试使用Playwright或Selenium等Web自动化工具模拟用户操作来测试核心业务流程。组件级测试如果项目结构清晰可以尝试将CustomControl包装成Blazor组件然后使用bUnit等Blazor测试框架进行测试这需要更多探索。跨浏览器测试必须在Chrome、Firefox、Safari、Edge等主流浏览器上进行测试确保一致性和功能完整性。特别注意Safari对WebAssembly和某些JavaScript API的支持可能略有滞后。性能与内存测试使用浏览器性能分析工具如Chrome DevTools的Performance和Memory面板监控应用运行时的内存占用、垃圾回收情况以及UI响应速度。WebAssembly应用有内存限制需警惕内存泄漏。6. 总结与展望WPF开发者的Web新航线回顾整个“英雄联盟客户端”项目的OpenSilver迁移之旅它并非简单的“一键转换”而是一次精心策划的架构现代化改造。我们成功地将一个庞大的、基于CustomControl的WPF应用的核心资产几乎无损地转移到了Web平台。这个过程验证了OpenSilver作为WPF向Web迁移桥梁的可行性和强大能力。最大的收获是确认了基于CustomControl和MVVM的良好架构设计是跨平台迁移成功的基石。清晰的关注点分离View、ViewModel、Model、对平台无关接口如ICommand、INotifyPropertyChanged、IValueConverter的依赖以及避免使用死板的平台特定API这些在WPF时代就被倡导的最佳实践在迁移时显示了巨大的价值。OpenSilver并非万能钥匙。它最适合的是那些业务逻辑复杂、UI交互丰富、且希望最大限度复用现有C#/XAML代码的“厚客户端”应用。对于全新的、追求极致轻量与性能的Web应用可能Blazor或主流JS框架仍是更主流的选择。但对于我们这样拥有大量WPF遗产代码的团队OpenSilver提供了一条风险可控、成本可估的渐进式迁移路径。给后来者的建议如果你也在考虑这条路径不妨从一个相对独立的功能模块开始试点迁移。评估其第三方依赖、平台特定调用的情况。优先将业务逻辑和ViewModel下沉到.NET Standard类库。然后逐步处理UI层用VisualStateManager替代复杂的Trigger抽象文件、网络等IO操作。充分利用OpenSilver的模拟器进行快速迭代调试。这次迁移让我们珍视的WPF技术栈在Web时代重新焕发了活力。它不仅仅是将一个应用从桌面搬到了浏览器更是为我们打开了一扇门未来同一套核心业务逻辑和UI组件或许还能通过.NET MAUI走向移动端。技术浪潮奔涌不息而扎实的架构设计和清晰的代码边界是我们应对变化最可靠的锚点。