Сборки с отложенной загрузкой в ASP.NET Core Blazor WebAssembly

Примечание.

Это не последняя версия этой статьи. Сведения о текущем выпуске см. в ASP.NET версии Core 8.0 этой статьи.

Blazor WebAssembly Производительность запуска приложения можно улучшить, ожидая загрузки сборок, созданных разработчиком, до тех пор, пока сборки не требуются, что называется отложенной загрузкой.

В первых разделах этой статьи рассматривается конфигурация приложений. Рабочую демонстрацию см. в разделе Полный пример в конце этой статьи.

Этот раздел относится только к приложениям Blazor WebAssembly. Отложенная загрузка сборок не дает преимущества серверным приложениям, так как отрисованные сервером приложения не загружают сборки в клиент.

Отложенная загрузка не должна использоваться для основных сборок среды выполнения, которые могут быть обрезаны при публикации и недоступности на клиенте при загрузке приложения.

Заполнитель расширения файла ({FILE EXTENSION}) для файлов сборки

Файлы сборок используют формат упаковки Webcil для сборок .NET с расширением .wasm файла.

На протяжении всей статьи {FILE EXTENSION} заполнитель представляет "wasm".

Файлы сборок основаны на библиотеках динамических ссылок (DLL) с расширением .dll файла.

На протяжении всей статьи {FILE EXTENSION} заполнитель представляет "dll".

Конфигурация файла проекта

Пометьте сборки для отложенной загрузки в файле проекта приложения (.csproj) с помощью элемента BlazorWebAssemblyLazyLoad. Используйте имя сборки с расширением файла. Платформа 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 WebAssemblyBlazor WebAssembly решения.

Компонент BlazorRouter определяет сборки, в которых Blazor ищет маршрутизируемые компоненты. Кроме того, он отвечает за отрисовку компонента для маршрута, по которому переходит пользователь. Метод OnNavigateAsync компонента Router используется совместно с отложенной загрузкой для загрузки подходящих сборок для конечных точек, запрашиваемых пользователем.

Логика реализуется внутри 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);
           }
    }
}

Примечание.

В предыдущем примере не показано содержимое разметки Razor компонента Router (...). Полный код см. в разделе Полный пример этой статьи.

Примечание.

В выпуске ASP.NET Core 5.0.1 и дальнейших выпусках 5.x компонент Router содержит параметр PreferExactMatches со значением @true. Дополнительные сведения см. в статье Миграция с ASP.NET Core 3.1 на 5.0.

Сборки, включающие маршрутизируемые компоненты

Если список сборок включает маршрутизируемые компоненты, этот список сборок для данного пути передается в коллекцию AdditionalAssemblies компонента Router.

В следующем примере :

  • List<Assembly> в lazyLoadedAssemblies передает список сборок в 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);
           }
    }
}

Примечание.

В предыдущем примере не показано содержимое разметки Razor компонента Router (...). Полный код см. в разделе Полный пример этой статьи.

Примечание.

В выпуске 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

Объект NavigationContext, переданный в обратный вызов OnNavigateAsync, содержит CancellationToken, который задается при возникновении нового события навигации. Обратный вызов OnNavigateAsync необходим, если этот токен отмены установлен так, чтобы не выполнять обратный вызов OnNavigateAsync для устаревшей навигации.

Дополнительные сведения см. в статье Маршрутизация ASP.NET Core Blazor и навигация.

События OnNavigateAsync и переименованные файлы сборки

Загрузчик ресурсов использует имена сборок, определенные в файле blazor.boot.json. Если сборки переименованы, то имена сборок, используемые в обратном вызове OnNavigateAsync, и имена сборок в файле blazor.boot.json не синхронизированы.

Чтобы исправить это, сделайте следующее.

  • Проверьте, выполняется ли приложение в среде Production при определении используемых имен сборок.
  • Сохраните имена переименованных сборок в отдельном файле и выполните чтение из этого файла, чтобы определить, какое имя сборки использовать со службой LazyAssemblyLoader и обратным вызовом OnNavigateAsync.

Отложенная загрузка сборок в размещенном решении Blazor WebAssembly

Реализация отложенной загрузки на платформе поддерживает отложенную загрузку с предварительной отрисовкой в размещенном Blazor WebAssemblyрешении . Во время предварительной отрисовки предполагается, что будут загружены все сборки, включая те, которые отмечены для отложенной загрузки. Вручную зарегистрируйте службу LazyAssemblyLoader в проекте Server.

В верхней части файла Program.cs проекта Server добавьте пространство имен для Microsoft.AspNetCore.Components.WebAssembly.Services:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

В методе Program.cs проекта Server зарегистрируйте службу:

builder.Services.AddScoped<LazyAssemblyLoader>();

В верхней части файла Startup.cs проекта Server добавьте пространство имен для Microsoft.AspNetCore.Components.WebAssembly.Services:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

В Startup.ConfigureServices (Startup.cs) Server проекта зарегистрируйте службу:

services.AddScoped<LazyAssemblyLoader>();

Полный пример

Демонстрация в этом разделе:

  • Создает сборку элементов управления робота (GrantImaharaRobotControls.{FILE EXTENSION}) в качестве библиотеки классов Razor (RCL), которая включает компонент Robot (Robot.razor с шаблоном маршрута /robot).
  • Выполняет отложенную загрузку сборки RCL для отрисовки ее компонента Robot, когда пользователь запрашивает URL-адрес /robot.

Создайте автономное Blazor WebAssembly приложение, чтобы продемонстрировать отложенную загрузку сборки Razor библиотеки классов. Присвойте проекту имя LazyLoadTest.

Добавьте в решение проект библиотеки классов ASP.NET Core:

  • Visual Studio: щелкните правой кнопкой мыши файл решения в Обозреватель решений и выберите "Добавить>новый проект". В диалоговом окне новых типов проектов выберите Razor библиотеку классов. Присвойте проекту имя GrantImaharaRobotControls. Не выбирайте страницы поддержки и просматривайте проверка box.
  • Visual Studio Code/.NET CLI: выполните dotnet new razorclasslib -o GrantImaharaRobotControls из командной строки. Параметр -o|--output создает папку и присваивает имя проекту GrantImaharaRobotControls.

В примере компонента, приведенном далее в этом разделе, используется форма Blazor. В проекте RCL добавьте в проект пакет Microsoft.AspNetCore.Components.Forms.

Примечание.

Рекомендации по добавлению пакетов в приложения .NET см. в разделе Способы установки пакетов NuGet в статье Рабочий процесс использования пакета (документация по NuGet). Проверьте правильность версий пакета на сайте NuGet.org.

Создайте класс HandGesture в RCL с помощью метода ThumbUp, который гипотетически заставляет робота поднять большой палец вверх. Метод принимает аргумент для оси, Left или Right, в качестве 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.

Укажите сборку RCL для отложенной загрузки в файле проекта приложения LazyLoadTest (.csproj):

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

Следующий компонент Router демонстрирует загрузку сборки GrantImaharaRobotControls.{FILE EXTENSION}, когда пользователь переходит к /robot. Замените компонент приложения по умолчанию 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).

Дополнительные ресурсы