翻译|使用教程|编辑:吉炜炜|2024-11-08 11:48:28.480|阅读 245 次
概述:在本文中,我们将探讨如何使用 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@ldacury.cn
文章转载自:慧都网