翻译|使用教程|编辑:吉伟伟|2024-11-08 11:48:28.480|阅读 16 次
概述:在本文中,我们将探讨如何使用 Avalonia UI 和 DotNetBrowser 作为 Web View 来创建 Blazor 混合应用程序。
# 慧都年终大促·界面/图表报表/文档/IDE等千款热门软控件火热促销中 >>
Blazor 是一个 .NET 前端框架,用于仅使用 .NET 技术构建 Web 应用程序。2021 年,Blazor 扩展到桌面端,推出了 Blazor Hybrid(混合),使开发者可以在桌面平台上使用已有的技能。
Blazor 混合应用程序是传统的桌面应用程序,它们在一个 Web View 控件中托管实际的 Blazor Web 应用程序。虽然这些应用程序使用 .NET MAUI 作为桌面端技术,但如果不符合需求,也可以使用其他框架。
MAUI 的局限性在于它缺乏对 Linux 的支持,并且在 Windows 和 macOS 上使用不同的 Browser Engine。Microsoft Edge 和 Safari 在实现 Web 标准、执行 JavaScript 以及页面渲染方面存在差异。这些差异在高级应用程序中可能会导致 bug 并需要额外的测试。
如果 MAUI 不符合您的要求,可以考虑选择 Avalonia UI,它是一个跨平台的 UI 库,其生态系统中包含多个基于 Chromium 的 Web View。
在本文中,我们将探讨如何使用 Avalonia UI 和 DotNetBrowser (下载试用)作为 Web View 来创建 Blazor 混合应用程序。
使用模板快速入门
要使用 DotNetBrowser 和 Avalonia UI 创建一个基本的 Blazor 混合应用程序,请使用我们的模板:
dotnet new install DotNetBrowser.Templates
从模板创建一个 Blazor 混合应用程序,并将您的许可证密钥作为参数传递:
dotnet new dotnetbrowser.blazor.avalonia.app -o Blazor.AvaloniaUi -li <your_license_key>
然后运行应用程序:
dotnet run --project Blazor.AvaloniaUi
实现
在混合环境中,Blazor 应用程序在其桌面壳程序的进程中运行。这个壳程序或窗口管理整个应用程序的生命周期,显示 Web View,并启动 Blazor 应用程序。我们将使用 Avalonia UI 创建这个窗口。
Blazor 应用程序的后端是 .NET 代码,前端是托管在 Web View 中的 Web 内容。 Web View 中的 Browser Engine 和 .NET 运行时之间没有直接连接。因此,为了前后端通信,Blazor 必须知道如何在它们之间交换数据。由于我们引入了一个新的 Web View,我们必须教会 Blazor 如何使用 DotNetBrowser 进行数据交换。
接下来,我们将带您了解 Blazor 与 Avalonia 和 DotNetBrowser 集成的关键部分。有关完整解决方案,请查看上面的模板。
创建窗口
为了托管 Blazor 混合应用程序,我们需要创建一个常规的 Avalonia 窗口,并添加一个 Web View 组件。
MainWindow.axaml
<Window ... Closed="Window_Closed"> <browser:BlazorBrowserView x:Name="BrowserView" ... /> ... </browser:BlazorBrowserView> </Window>
MainWindow.axaml.cs
public partial class MainWindow : Window { public MainWindow() { InitializeComponent(); ... BrowserView.Initialize(); } private void Window_Closed(object sender, EventArgs e) { BrowserView.Shutdown(); } }
BlazorBrowserView 是我们为了封装 DotNetBrowser 而创建的一个 Avalonia 控件。稍后,我们将在这个控件中将其与 Blazor 集成。
BlazorBrowserView.axaml
<UserControl ...> ... <avaloniaUi:BrowserView x:Name="BrowserView" IsVisible="False" ... /> </UserControl>
BlazorBrowserView.axaml.cs
public partial class BlazorBrowserView : UserControl { private IEngine engine; private IBrowser browser; public BlazorBrowserView() { InitializeComponent(); } public async Task Initialize() { EngineOptions engineOptions = new EngineOptions.Builder { RenderingMode = RenderingMode.HardwareAccelerated }.Build(); engine = await EngineFactory.CreateAsync(engineOptions); browser = engine.CreateBrowser(); ... Dispatcher.UIThread.InvokeAsync(ShowView); } public void Shutdown() { engine?.Dispose(); } private void ShowView() { BrowserView.InitializeFrom(browser); BrowserView.IsVisible = true; browser?.Focus(); } }
配置 Blazor
在混合应用程序中,负责 Blazor 与环境集成的主要实体是 WebViewManager。这是一个抽象类,因此我们需要创建自己的实现,这里我们称之为 BrowserManager 并在 BlazorBrowserView 中实例化它。
BrowserManager.cs
class BrowserManager : WebViewManager { private static readonly string AppHostAddress = "0.0.0.0"; private static readonly string AppOrigin = $"//{AppHostAddress}/"; private static readonly Uri AppOriginUri = new(AppOrigin); private IBrowser Browser { get; } public BrowserManager(IBrowser browser, IServiceProvider provider, Dispatcher dispatcher, IFileProvider fileProvider, JSComponentConfigurationStore jsComponents, string hostPageRelativePath) : base(provider, dispatcher, AppOriginUri, fileProvider, jsComponents, hostPageRelativePath) { Browser = browser; } ... }
BlazorBrowserView.axaml.cs
public partial class BlazorBrowserView : UserControl { private IEngine engine; private IBrowser browser; private BrowserManager browserManager; ... public async Task Initialize() { EngineOptions engineOptions = new EngineOptions.Builder { RenderingMode = RenderingMode.HardwareAccelerated }.Build(); engine = await EngineFactory.CreateAsync(engineOptions); browser = engine.CreateBrowser(); ... browserManager = new BrowserManager(browser, ...); ... } ... }
一个 Blazor 应用程序需要一个或多个根组件。当 Web View 正在初始化时,我们将它们添加到 WebViewManager 中。
RootComponent.cs
public class RootComponent { public string ComponentType { get; set; } public IDictionary<string, object> Parameters { get; set; } public string Selector { get; set; } public Task AddToWebViewManagerAsync(BrowserManager browserManager) { ParameterView parameterView = Parameters == null ? ParameterView.Empty : ParameterView.FromDictionary(Parameters); return browserManager?.AddRootComponentAsync( Type.GetType(ComponentType)!, Selector, parameterView); } }
BlazorBrowserView.axaml.cs
public partial class BlazorBrowserView : UserControl { private IEngine engine; private IBrowser browser; private BrowserManager browserManager; public ObservableCollection<RootComponent> RootComponents { get; set; } = new(); ... public async Task Initialize() { ... engine = await EngineFactory.CreateAsync(engineOptions); browser = engine.CreateBrowser(); browserManager = new BrowserManager(browser, ...); foreach (RootComponent rootComponent in RootComponents) { await rootComponent.AddToWebViewManagerAsync(browserManager); } ... } ... }
MainWindow.axaml
<Window ... Closed="Window_Closed"> <browser:BlazorBrowserView x:Name="BrowserView" ... /> <browser:BlazorBrowserView.RootComponents> <browser:RootComponent Selector="..." ComponentType="..." /> </browser:BlazorBrowserView.RootComponents> </browser:BlazorBrowserView> </Window>
加载静态资源
在普通的 Web 应用程序中,Browser 通过向服务器发送 HTTP 请求来加载页面和静态资源。在 Blazor 混合应用程序中,虽然原理相似,但这里并没有传统的服务器。相反,WebViewManager 提供了一个名为 TryGetResponseContent 的方法,该方法接受一个 URL 并返回数据作为类似 HTTP 的响应。
我们通过拦截 DotNetBrowser 中的 HTTPS 流量将 HTTP 请求和响应传递到此方法并返回。
BlazorBrowserView.axaml.cs
public partial class BlazorBrowserView : UserControl { private IEngine engine; private IBrowser browser; private BrowserManager browserManager; ... public async Task Initialize() { EngineOptions engineOptions = new EngineOptions.Builder { RenderingMode = RenderingMode.HardwareAccelerated, Schemes = { { Scheme.Https, new Handler<InterceptRequestParameters, InterceptRequestResponse>(OnHandleRequest) } } }.Build(); engine = await EngineFactory.CreateAsync(engineOptions); browser = engine.CreateBrowser(); browserManager = new BrowserManager(browser, ...); ... } public InterceptRequestResponse OnHandleRequest( InterceptRequestParameters params) => browserManager?.OnHandleRequest(params); ... }
BrowserManager.cs
internal class BrowserManager : WebViewManager { private static readonly string AppHostAddress = "0.0.0.0"; private static readonly string AppOrigin = $"//{AppHostAddress}/"; private static readonly Uri AppOriginUri = new(AppOrigin); ... public InterceptRequestResponse OnHandleRequest(InterceptRequestParameters p) { if (!p.UrlRequest.Url.StartsWith(AppOrigin)) { // 如果请求不以 AppOrigin 开头,则允许它通过。 return InterceptRequestResponse.Proceed(); } ResourceType resourceType = p.UrlRequest.ResourceType; bool allowFallbackOnHostPage = resourceType is ResourceType.MainFrame or ResourceType.Favicon or ResourceType.SubResource; if (TryGetResponseContent(p.UrlRequest.Url, allowFallbackOnHostPage, out int statusCode, out string _, out Stream content, out IDictionary<string, string> headers)) { UrlRequestJob urlRequestJob = p.Network.CreateUrlRequestJob(p.UrlRequest, new UrlRequestJobOptions { HttpStatusCode = (HttpStatusCode)statusCode, Headers = headers .Select(pair => new HttpHeader(pair.Key, pair.Value)) .ToList() }); Task.Run(() => { using (MemoryStream memoryStream = new()) { content.CopyTo(memoryStream); urlRequestJob.Write(memoryStream.ToArray()); } urlRequestJob.Complete(); }); return InterceptRequestResponse.Intercept(urlRequestJob); } return InterceptRequestResponse.Proceed(); } }
导航
现在,当 Web View 可以导航到应用页面并加载静态资源时,我们可以加载索引页并教导 WebViewManager 如何执行导航操作。
BlazorBrowserView.axaml.cs
public partial class BlazorBrowserView : UserControl { private IEngine engine; private IBrowser browser; private BrowserManager browserManager; ... public async Task Initialize() { ... engine = await EngineFactory.CreateAsync(engineOptions); browser = engine.CreateBrowser(); browserManager = new BrowserManager(browser, ...); foreach (RootComponent rootComponent in RootComponents) { await rootComponent.AddToWebViewManagerAsync(browserManager); } browserManager.Navigate("/"); ... } ... }
BrowserManager.cs
internal class BrowserManager : WebViewManager { ... private IBrowser Browser { get; } ... protected override void NavigateCore(Uri absoluteUri) { Browser.Navigation.LoadUrl(absoluteUri.AbsoluteUri); } }
数据交换
与普通的 Web 应用程序不同,Blazor Hybrid 不使用 HTTP 进行数据交换。前端和后端通过字符串消息进行通信,使用的是特殊的 .NET-JavaScript 互操作机制。在 JavaScript 中,消息通过 window.external 对象发送和接收,而在 .NET 端,则通过 WebViewManager 进行。
我们使用 DotNetBrowser 的 .NET-JavaScript 桥接功能来创建 window.external 对象并传输消息。
BrowserManager.cs
internal class BrowserManager : WebViewManager { ... private IBrowser Browser { get; } private IJsFunction sendMessageToFrontEnd; public BrowserManager(IBrowser browser, IServiceProvider provider, Dispatcher dispatcher, IFileProvider fileProvider, JSComponentConfigurationStore jsComponents, string hostPageRelativePath) : base(provider, dispatcher, AppOriginUri, fileProvider, jsComponents, hostPageRelativePath) { Browser = browser; // 此处理程序在页面加载之后但在执行其自己的 JavaScript 之前调用。 Browser.InjectJsHandler = new Handler<InjectJsParameters>(OnInjectJs); } ... private void OnInjectJs(InjectJsParameters p) { if (!p.Frame.IsMain) { return; } dynamic window = p.Frame.ExecuteJavaScript("window").Result; window.external = p.Frame.ParseJsonString("{}"); // 当页面调用这些方法时,DotNetBrowser 会将调用代理到 .NET 方法。 window.external.sendMessage = (Action<dynamic>)OnMessageReceived; window.external.receiveMessage = (Action<dynamic>)SetupCallback; } private void OnMessageReceived(dynamic obj) { this.MessageReceived(new Uri(Browser.Url), obj.ToString()); } private void SetupCallback(dynamic callbackFunction) { sendMessageToFrontEnd = callbackFunction as IJsFunction; } protected override void SendMessage(string message) { sendMessageToFrontEnd?.Invoke(null, message); } }
结论
在本文中,我们讨论了 Blazor Hybrid,这是一种用于使用 Blazor 构建桌面应用程序的 .NET 技术。
Blazor Hybrid 使用 .NET MAUI 存在两个局限性:
我们建议使用 Avalonia UI + DotNetBrowser 作为替代方案。这种组合为 Windows、macOS 和 Linux 提供了全面支持,并确保在所有平台上都能保持一致的 Browser 环境。
本站文章除注明转载外,均为本站原创或翻译。欢迎任何形式的转载,但请务必注明出处、不得修改原文相关链接,如果存在内容上的异议请邮件反馈至chenjj@cahobeh.cn
文章转载自:慧都网