在 ASP.NET Core Blazor WebAssembly 中延迟加载程序集

注意

此版本不是本文的最新版本。 对于当前版本,请参阅此文的 .NET 8 版本

重要

此信息与预发布产品相关,相应产品在商业发布之前可能会进行重大修改。 Microsoft 对此处提供的信息不提供任何明示或暗示的保证。

对于当前版本,请参阅此文的 .NET 8 版本

通过等到需要开发人员创建的应用程序集时才加载该程序集,从而提高 Blazor WebAssembly 应用启动性能,这种方式称为“延迟加载”

本文的初始部分介绍了应用配置。 如需有效的演示,请参阅本文末尾的完整示例部分。

本文仅适用于 Blazor WebAssembly 应用。 由于服务器呈现的应用不会将程序集下载到客户端,因此程序集的延迟加载对服务器端应用并没有好处。

延迟加载不应用于核心运行时程序集,这些程序集在发布时可能会被裁剪,而在应用加载时,可能在客户端上不可用。

程序集文件的文件扩展名占位符 ({FILE EXTENSION})

程序集文件使用文件扩展名为 .wasm.NET 程序集的 Webcil 打包格式

在整个文章中,{FILE EXTENSION} 占位符表示“wasm”。

程序集文件基于文件扩展名为 .dll 的动态链接库 (DLL)。

在整个文章中,{FILE EXTENSION} 占位符表示“dll”。

项目文件配置

使用 BlazorWebAssemblyLazyLoad 项标记应用的项目文件 (.csproj) 中用于延迟加载的程序集。 使用带文件扩展名的程序集名称。 Blazor 框架会阻止程序集在应用启动时加载。

<ItemGroup>
  <BlazorWebAssemblyLazyLoad Include="{ASSEMBLY NAME}.{FILE EXTENSION}" />
</ItemGroup>

{ASSEMBLY NAME} 占位符是程序集的名称,{FILE EXTENSION} 占位符是文件扩展名。 文件扩展名是必需的。

每个程序集都包含一个 BlazorWebAssemblyLazyLoad 项。 如果程序集具有依赖项,则每个依赖项都包含一个 BlazorWebAssemblyLazyLoad 条目。

Router 组件配置

Blazor 框架会自动为客户端 Blazor WebAssembly 应用 LazyAssemblyLoader 中的延迟加载程序集注册单一服务。 LazyAssemblyLoader.LoadAssembliesAsync 方法:

  • 使用 JS 互操作通过网络调用提取程序集。
  • 在浏览器中将程序集加载到正在 WebAssembly 上执行的运行时。

注意

托管的Blazor WebAssembly解决方案的指南包含在托管的 Blazor WebAssembly 解决方案中的延迟加载程序集部分中。

Blazor 的 Router 组件指定 Blazor 搜索可路由组件的程序集,并负责为用户导航的路由呈现组件。 Router 组件的 OnNavigateAsync 方法 与延迟加载结合使用,以便为用户请求的终结点加载正确的程序集。

逻辑在 OnNavigateAsync 内实现,以确定要使用 LazyAssemblyLoader 加载的程序集。 逻辑构建方式的选项包括:

  • OnNavigateAsync 方法内部的条件检查。
  • 一个将路由映射到程序集名称的查找表,可以注入到组件中,也可以在 @code 块内实现。

如下示例中:

  • 指定了 Microsoft.AspNetCore.Components.WebAssembly.Services 的命名空间。
  • 注入了 LazyAssemblyLoader 服务 (AssemblyLoader)。
  • {PATH} 占位符是程序集列表应加载到的路径。 该示例对加载一组程序集的单个路径使用条件检查。
  • {LIST OF ASSEMBLIES} 占位符是以逗号分隔的程序集文件名字符串列表,包括它们的文件扩展名(例如 "Assembly1.{FILE EXTENSION}", "Assembly2.{FILE EXTENSION}")。

App.razor

@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="typeof(App).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject LazyAssemblyLoader AssemblyLoader
@inject ILogger<App> Logger

<Router AppAssembly="typeof(Program).Assembly" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}

注意

前面的示例没有显示 Router 组件的 Razor 标记 (...) 的内容。 有关完整代码的演示,请参阅本文的完整示例部分。

注意

随着 ASP.NET Core 5.0.1 的发布及任何附加 5.x 版本的推出,Router 组件包含 PreferExactMatches 参数(设置为 @true)。 有关详细信息,请参阅从 ASP.NET Core 3.1 迁移到 5.0

包含可路由组件的程序集

当程序集列表包含可路由组件时,指定路径的程序集列表将传递到 Router 组件的 AdditionalAssemblies 集合。

如下示例中:

  • lazyLoadedAssemblies 中的 List<Assembly> 将程序集列表传递给 AdditionalAssemblies。 框架会搜索程序集中的路由,并在发现新路由时更新路由集合。 要访问 Assembly 类型,请将 System.Reflection 的命名空间包含在 App.razor 文件的顶部。
  • {PATH} 占位符是程序集列表应加载到的路径。 该示例对加载一组程序集的单个路径使用条件检查。
  • {LIST OF ASSEMBLIES} 占位符是以逗号分隔的程序集文件名字符串列表,包括它们的文件扩展名(例如 "Assembly1.{FILE EXTENSION}", "Assembly2.{FILE EXTENSION}")。

App.razor

@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(App).Assembly" 
    AdditionalAssemblies="lazyLoadedAssemblies" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
                   lazyLoadedAssemblies.AddRange(assemblies);
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(Program).Assembly" 
    AdditionalAssemblies="lazyLoadedAssemblies" 
    OnNavigateAsync="OnNavigateAsync">
    ...
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
           {
               if (args.Path == "{PATH}")
               {
                   var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                       new[] { {LIST OF ASSEMBLIES} });
                   lazyLoadedAssemblies.AddRange(assemblies);
               }
           }
           catch (Exception ex)
           {
               Logger.LogError("Error: {Message}", ex.Message);
           }
    }
}

注意

前面的示例没有显示 Router 组件的 Razor 标记 (...) 的内容。 有关完整代码的演示,请参阅本文的完整示例部分。

注意

随着 ASP.NET Core 5.0.1 的发布及任何附加 5.x 版本的推出,Router 组件包含 PreferExactMatches 参数(设置为 @true)。 有关详细信息,请参阅从 ASP.NET Core 3.1 迁移到 5.0

有关详细信息,请参阅 ASP.NET Core Blazor 路由和导航

用户与 <Navigating> 内容的交互

加载程序集可能需要几秒钟的时间,Router 组件可以通过路由器的 Navigating 属性向用户指示正在发生页面转换。

有关详细信息,请参阅 ASP.NET Core Blazor 路由和导航

处理 OnNavigateAsync 中的取消

传递到 OnNavigateAsync 回调的 NavigationContext 对象包含的 CancellationToken 在发生新导航事件时进行设置。 设置取消标记时,OnNavigateAsync 回调必须引发,以避免在过时的导航中继续运行 OnNavigateAsync 回调。

有关详细信息,请参阅 ASP.NET Core Blazor 路由和导航

OnNavigateAsync 事件和重命名的程序集文件

资源加载程序依赖于在 blazor.boot.json 文件中定义的程序集名称。 如果程序集已重命名,则 OnNavigateAsync 回叫中使用的程序集名称和 blazor.boot.json 文件中的程序集名称将不同步。

若要更正此问题,请执行以下操作:

  • 确定要使用的程序集名称时,检查应用是否在 Production 环境中运行。
  • 将重命名的程序集名称存储在单独的文件中并从该文件中读取,以确定要与 LazyAssemblyLoader 服务和 OnNavigateAsync 回叫一起使用的程序集名称。

托管的 Blazor WebAssembly 解决方案中的延迟加载程序集

框架的延迟加载实现支持在托管的 Blazor WebAssembly解决方案中使用预呈现进行延迟加载。 在预呈现期间,所有的程序集(包括那些被标记为延迟加载的程序集)都被假定为已加载。 在 Server 项目中手动注册 LazyAssemblyLoader 服务。

Server 项目的 Program.cs 文件顶部,添加 Microsoft.AspNetCore.Components.WebAssembly.Services 的命名空间:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

Server 项目的 Program.cs 中,注册服务:

builder.Services.AddScoped<LazyAssemblyLoader>();

Server 项目的 Startup.cs 文件顶部,添加 Microsoft.AspNetCore.Components.WebAssembly.Services 的命名空间:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

Server 项目的 Startup.ConfigureServices (Startup.cs) 中,注册服务:

services.AddScoped<LazyAssemblyLoader>();

完整示例

本部分中的演示:

  • 创建机器人控制程序集 (GrantImaharaRobotControls.{FILE EXTENSION}) 作为包含 Robot 组件的 Razor 类库 (RCL)Robot.razor,路由模板为 /robot)。
  • 当用户请求 /robot URL 时,延迟加载 RCL 的程序集以呈现其 Robot 组件。

创建独立 Blazor WebAssembly 应用以演示 Razor 类库程序集的延迟加载。 将项目命名为 LazyLoadTest

向解决方案添加 ASP.NET Core 类库项目:

  • Visual Studio:右键单击解决方案资源管理器中的解决方案文件,然后选择“添加”>“新项目”。 从新项目类型的对话框中,选择“Razor 类库”。 将项目命名为 GrantImaharaRobotControls不要选中“支持页面和视图”复选框。
  • Visual Studio Code/.NET CLI:从命令提示符执行 dotnet new razorclasslib -o GrantImaharaRobotControls-o|--output 选项将创建文件夹并为项目 GrantImaharaRobotControls 命名。

本部分后面介绍的示例组件使用 Blazor 表单。 在 RCL 项目中,将 Microsoft.AspNetCore.Components.Forms 包添加到项目。

注意

有关将包添加到 .NET 应用的指南,请参阅包使用工作流(NuGet 文档)中“安装和管理包”下的文章。 在 NuGet.org 中确认正确的包版本。

使用 ThumbUp 方法在 RCL 中创建一个 HandGesture 类,该类可通过假设方式让机器人竖起大拇指。 该方法接受轴的参数 LeftRight,作为 enum。 该方法在成功时返回 true

HandGesture.cs

using Microsoft.Extensions.Logging;

namespace GrantImaharaRobotControls;

public static class HandGesture
{
    public static bool ThumbUp(Axis axis, ILogger logger)
    {
        logger.LogInformation("Thumb up gesture. Axis: {Axis}", axis);

        // Code to make robot perform gesture

        return true;
    }
}

public enum Axis { Left, Right }
using Microsoft.Extensions.Logging;

namespace GrantImaharaRobotControls
{
    public static class HandGesture
    {
        public static bool ThumbUp(Axis axis, ILogger logger)
        {
            logger.LogInformation("Thumb up gesture. Axis: {Axis}", axis);

            // Code to make robot perform gesture

            return true;
        }
    }

    public enum Axis { Left, Right }
}

将以下组件添加到 RCL 项目的根目录。 通过该组件,用户可以提交竖起左手大拇指或右手大拇指的手势请求。

Robot.razor

@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm FormName="RobotForm" Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in Enum.GetValues<Axis>())
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

    <button type="submit">Submit</button>
</EditForm>

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new() { AxisSelection = Axis.Left };
    private string? message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}
@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in Enum.GetValues<Axis>())
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

    <button type="submit">Submit</button>
</EditForm>

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new() { AxisSelection = Axis.Left };
    private string? message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}
@page "/robot"
@using Microsoft.AspNetCore.Components.Forms
@using Microsoft.Extensions.Logging
@inject ILogger<Robot> Logger

<h1>Robot</h1>

<EditForm Model="robotModel" OnValidSubmit="HandleValidSubmit">
    <InputRadioGroup @bind-Value="robotModel.AxisSelection">
        @foreach (var entry in (Axis[])Enum
            .GetValues(typeof(Axis)))
        {
            <InputRadio Value="entry" />
            <text>&nbsp;</text>@entry<br>
        }
    </InputRadioGroup>

    <button type="submit">Submit</button>
</EditForm>

<p>
    @message
</p>

@code {
    private RobotModel robotModel = new RobotModel() { AxisSelection = Axis.Left };
    private string message;

    private void HandleValidSubmit()
    {
        Logger.LogInformation("HandleValidSubmit called");

        var result = HandGesture.ThumbUp(robotModel.AxisSelection, Logger);

        message = $"ThumbUp returned {result} at {DateTime.Now}.";
    }

    public class RobotModel
    {
        public Axis AxisSelection { get; set; }
    }
}

LazyLoadTest 项目中,为 GrantImaharaRobotControls RCL 创建项目引用:

  • Visual Studio:右键单击 LazyLoadTest 项目,然后选择“添加”>“项目引用”以添加 GrantImaharaRobotControls RCL 的项目引用。
  • Visual Studio Code/.NET CLI:在命令行界面中从项目文件夹执行 dotnet add reference {PATH}。 占位符 {PATH} 是 RCL 项目的路径。

LazyLoadTest 应用的项目文件 (.csproj) 中指定用于延迟加载的 RCL 程序集:

<ItemGroup>
    <BlazorWebAssemblyLazyLoad Include="GrantImaharaRobotControls.{FILE EXTENSION}" />
</ItemGroup>

以下 Router 组件演示了在用户导航到 /robot 时如何加载 GrantImaharaRobotControls.{FILE EXTENSION} 程序集。 将应用的默认 App 组件替换为以下 App 组件。

在页面转换期间,系统会向用户显示一条具有 <Navigating> 元素的有样式消息。 有关详细信息,请参阅用户与 <Navigating> 内容互动部分。

程序集会分配给 AdditionalAssemblies,这会导致路由器在程序集中搜索可路由组件,并在其中找到 Robot 组件。 Robot 组件的路由将添加到应用的路由集合中。 有关详细信息,请参阅 ASP.NET Core Blazor 路由和导航一文以及本文的包含可路由组件的程序集部分。

App.razor

@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(App).Assembly"
        AdditionalAssemblies="lazyLoadedAssemblies" 
        OnNavigateAsync="OnNavigateAsync">
    <Navigating>
        <div style="padding:20px;background-color:blue;color:white">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if (args.Path == "robot")
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" });
                lazyLoadedAssemblies.AddRange(assemblies);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}
@using System.Reflection
@using Microsoft.AspNetCore.Components.Routing
@using Microsoft.AspNetCore.Components.WebAssembly.Services
@using Microsoft.Extensions.Logging
@inject ILogger<App> Logger
@inject LazyAssemblyLoader AssemblyLoader

<Router AppAssembly="typeof(Program).Assembly"
        AdditionalAssemblies="lazyLoadedAssemblies" 
        OnNavigateAsync="OnNavigateAsync">
    <Navigating>
        <div style="padding:20px;background-color:blue;color:white">
            <p>Loading the requested page&hellip;</p>
        </div>
    </Navigating>
    <Found Context="routeData">
        <RouteView RouteData="routeData" DefaultLayout="typeof(MainLayout)" />
    </Found>
    <NotFound>
        <LayoutView Layout="typeof(MainLayout)">
            <p>Sorry, there's nothing at this address.</p>
        </LayoutView>
    </NotFound>
</Router>

@code {
    private List<Assembly> lazyLoadedAssemblies = new List<Assembly>();

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if (args.Path == "robot")
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" });
                lazyLoadedAssemblies.AddRange(assemblies);
            }
        }
        catch (Exception ex)
        {
            Logger.LogError("Error: {Message}", ex.Message);
        }
    }
}

构建并运行应用。

当在 /robot 处请求了来自 RCL 的 Robot 组件时,将加载 GrantImaharaRobotControls.{FILE EXTENSION} 程序集并呈现 Robot 组件。 可以在浏览器开发人员工具的“网络”选项卡中检查程序集加载。

疑难解答

  • 如果出现意外的呈现(例如呈现上一次导航中的组件),请确认在设置取消标记时是否引发代码。
  • 如果配置为延迟加载的程序集在应用启动时意外加载,请检查该程序集是否在项目文件中标记为延迟加载。

注意

从延迟加载的程序集中加载类型存在一个已知问题。 有关详细信息,请参阅 Blazor WebAssembly lazy loading assemblies not working when using @ref attribute in the component (dotnet/aspnetcore #29342)

其他资源