Partilhar via


Carregamento preguiçoso de assemblies no ASP.NET Core Blazor WebAssembly

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão .NET 9 deste artigo.

Advertência

Esta versão do ASP.NET Core não é mais suportada. Para obter mais informações, consulte a Política de suporte do .NET e .NET Core. Para a versão atual, consulte a versão .NET 9 deste artigo.

Importante

Estas informações referem-se a um produto de pré-lançamento que pode ser substancialmente modificado antes de ser lançado comercialmente. A Microsoft não oferece garantias, expressas ou implícitas, em relação às informações fornecidas aqui.

Para a versão atual, consulte a versão .NET 9 deste artigo.

Blazor WebAssembly O desempenho de arranque da aplicação pode ser melhorado adiando o carregamento dos assemblies criados pelo desenvolvedor até serem necessários, o que se chama de carregamento lento.

As seções iniciais deste artigo abrangem a configuração do aplicativo. Para uma demonstração de trabalho, consulte a seção Exemplo completo no final deste artigo.

Este artigo aplica-se apenas a aplicações Blazor WebAssembly. O carregamento preguiçoso de assembly não beneficia as aplicações do lado do servidor, porque as aplicações renderizadas pelo servidor não transferem assemblies para o cliente.

O carregamento lento não deve ser usado para assemblies de tempo de execução principais, que podem ser removidos na publicação e indisponíveis no cliente quando a aplicação é carregada.

Espaço reservado para extensão de arquivo ({FILE EXTENSION}) para arquivos de montagem

Os arquivos de montagem usam o formato de empacotamento Webcil para assemblies .NET com uma extensão de arquivo .wasm.

Ao longo do artigo, o marcador de posição {FILE EXTENSION} representa "wasm".

Os arquivos Assembly são baseados em bibliotecas Dynamic-Link (DLLs) com uma extensão de arquivo .dll.

Ao longo do artigo, o marcador de posição {FILE EXTENSION} representa "dll".

Configuração do arquivo de projeto

Marque assemblies para carregamento lento no ficheiro de projeto da aplicação (.csproj) usando o item BlazorWebAssemblyLazyLoad. Use o nome do assembly com extensão de arquivo. A estrutura Blazor impede que o assembly seja carregado na inicialização do aplicativo.

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

O marcador {ASSEMBLY NAME} é o nome do assembly e o marcador {FILE EXTENSION} é a extensão do ficheiro. A extensão do arquivo é necessária.

Inclua um item BlazorWebAssemblyLazyLoad para cada montagem. Se um assembly tiver dependências, inclua uma entrada BlazorWebAssemblyLazyLoad para cada dependência.

Router configuração do componente

A estrutura Blazor regista automaticamente um serviço singleton para carregamento preguiçoso de assemblies em aplicações Blazor WebAssembly do lado do cliente, LazyAssemblyLoader. O método LazyAssemblyLoader.LoadAssembliesAsync:

  • Utiliza JS interoperabilidade para buscar assemblies por meio de uma chamada de rede.
  • Carrega assemblies no tempo de execução em execução no WebAssembly no navegador.

Observação

Orientação para soluções hospedadas é abordada na seção Lazy load assemblies de em uma solução hospedada .

Blazorcomponente Router designa os conjuntos que Blazor procura componentes roteáveis e também é responsável por renderizar o componente para a rota onde o usuário navega. O método OnNavigateAsync do componente Router é usado em conjunto com o carregamento lento para carregar os assemblies corretos para pontos de extremidade solicitados por um usuário.

A lógica é implementada dentro OnNavigateAsync para determinar os assemblies a serem carregados com LazyAssemblyLoader. As opções de como estruturar a lógica incluem:

  • Verificações condicionais dentro do método OnNavigateAsync.
  • Uma tabela de pesquisa que mapeia rotas para nomes de assembly, injetados no componente ou implementados no código do componente.

No exemplo a seguir:

  • O namespace para Microsoft.AspNetCore.Components.WebAssembly.Services é especificado.
  • O serviço de LazyAssemblyLoader é injetado (AssemblyLoader).
  • O marcador {PATH} é o caminho onde a lista de assemblagens deve ser carregada. O exemplo usa uma verificação condicional para um único caminho que carrega um único conjunto de assemblies.
  • O espaço reservado {LIST OF ASSEMBLIES} é uma lista separada por vírgulas de nomes de ficheiros de assemblagem, incluindo as suas extensões de ficheiro (por exemplo, "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);
           }
    }
}

Observação

O exemplo anterior não mostra o conteúdo da marcação Razor do componente Router (...). Para obter uma demonstração com código completo, consulte a seção Exemplo Completo deste artigo.

Observação

Com o lançamento do ASP.NET Core 5.0.1 e para quaisquer versões adicionais do 5.x, o componente Router inclui o parâmetro PreferExactMatches definido como @true. Para obter mais informações, consulte migrar do ASP.NET Core 3.1 para o 5.0.

Montagens que incluem componentes roteáveis

Quando a lista de assemblies inclui componentes roteáveis, a lista de assemblies de um determinado caminho é passada para a coleção AdditionalAssemblies do componente Router.

No exemplo a seguir:

  • A Lista de <Assembly> no lazyLoadedAssemblies transmite a lista de montagem para AdditionalAssemblies. A estrutura pesquisa rotas nos assemblies e atualiza a coleção de rotas se novas rotas forem encontradas. Para acessar o tipo de Assembly, o namespace para System.Reflection está incluído na parte superior do arquivo App.razor.
  • O espaço reservado {PATH} é o caminho onde a lista de conjuntos deve ser carregada. O exemplo usa uma verificação condicional para um único caminho que carrega um único conjunto de assemblies.
  • O placeholder {LIST OF ASSEMBLIES} é a lista separada por vírgulas de nomes de ficheiros assembly, incluindo as suas extensões (por exemplo, "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);
           }
    }
}

Observação

O exemplo anterior não mostra o conteúdo da marcação Razor do componente Router (...). Para obter uma demonstração com código completo, consulte a seção Exemplo Completo deste artigo.

Observação

Com o lançamento do ASP.NET Core 5.0.1 e para quaisquer versões adicionais do 5.x, o componente Router inclui o parâmetro PreferExactMatches definido como @true. Para obter mais informações, consulte migrar do ASP.NET Core 3.1 para o 5.0.

Para obter mais informações, consulte ASP.NET Core Blazor roteamento e navegação.

Interação do utilizador com o conteúdo <Navigating>

Ao carregar assemblies, o que pode levar vários segundos, o componente Router pode indicar ao usuário que uma transição de página está ocorrendo com a propriedade Navigating do roteador.

Para obter mais informações, consulte ASP.NET Core Blazor roteamento e navegação.

Lidar com cancelamentos em OnNavigateAsync

O objeto NavigationContext passado para a função de retorno OnNavigateAsync contém um CancellationToken que é definido quando ocorre um novo evento de navegação. O retorno de chamada OnNavigateAsync deve ser lançado quando o token de cancelamento estiver definido para evitar continuar a executar o retorno de chamada OnNavigateAsync em uma navegação desatualizada.

Para obter mais informações, consulte ASP.NET Core Blazor roteamento e navegação.

OnNavigateAsync eventos e arquivos de assemblagem renomeados

O carregador de recursos depende dos nomes de assembly definidos no arquivo blazor.boot.json. Se assemblies forem renomeados, os nomes de assembly usados em um retorno de chamada OnNavigateAsync e os nomes de assembly no arquivo blazor.boot.json estarão fora de sincronia.

Para corrigir esta situação:

  • Verifique se o aplicativo está sendo executado no ambiente Production ao determinar quais nomes de assembly usar.
  • Armazene os nomes de assembly renomeados num ficheiro separado e leia esse ficheiro para determinar qual nome de assembly usar com o serviço LazyAssemblyLoader e a chamada de retorno OnNavigateAsync.

Carregar assemblies de forma preguiçosa numa solução Blazor WebAssembly hospedada

A implementação de carregamento lento da estrutura suporta carregamento lento com prérenderização numa solução de Blazor WebAssemblyhospedada. Durante a pré-renderização, todos os conjuntos, incluindo aqueles marcados para carregamento lento, são considerados carregados. Registre manualmente o serviço de LazyAssemblyLoader no projeto Server.

Na parte superior do arquivo de Program.cs do projeto Server, adicione o namespace para Microsoft.AspNetCore.Components.WebAssembly.Services:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

Em Program.cs do projeto Server, registe o serviço:

builder.Services.AddScoped<LazyAssemblyLoader>();

Na parte superior do arquivo de Startup.cs do projeto Server, adicione o namespace para Microsoft.AspNetCore.Components.WebAssembly.Services:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

Em Startup.ConfigureServices (Startup.cs) do projeto Server, registe o serviço:

services.AddScoped<LazyAssemblyLoader>();

Exemplo completo

A demonstração nesta seção:

  • Cria um conjunto de controles de robô (GrantImaharaRobotControls.{FILE EXTENSION}) como uma biblioteca de classes Razor (RCL) que inclui um componente Robot (Robot.razor com um modelo de rota de /robot).
  • Preguiçosamente carrega o assembly da RCL para renderizar seu componente Robot quando a URL /robot é solicitada pelo usuário.

Crie um aplicativo Blazor WebAssembly autônomo para demonstrar o carregamento lento de um assembly da biblioteca de classes Razor. Nomeie o projeto LazyLoadTest.

Adicione um projeto de biblioteca de classes ASP.NET Core à solução:

  • Visual Studio: Clique com o botão direito do mouse no arquivo de solução no Gerenciador de Soluções e selecione Adicionar>Novo projeto. Na caixa de diálogo de novos tipos de projeto, selecione Razor Biblioteca de Classes. Nomeie o projeto GrantImaharaRobotControls. Não marque a caixa de seleção páginas e exibições de suporte.
  • Visual Studio Code/.NET CLI: execute dotnet new razorclasslib -o GrantImaharaRobotControls a partir de um prompt de comando. A opção -o|--output cria uma pasta e nomeia o projeto GrantImaharaRobotControls.

Crie uma classe HandGesture na RCL com um método ThumbUp que, hipoteticamente, faz com que um robô execute um gesto de polegar para cima. O método aceita um argumento para o eixo, Left ou Right, como um enum. O método retorna true no sucesso.

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 }
}

Adicione o seguinte componente à raiz do projeto RCL. O componente permite que o usuário envie uma solicitação de gesto de polegar para cima com a mão esquerda ou direita.

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; }
    }
}

No projeto LazyLoadTest, crie uma referência de projeto para o GrantImaharaRobotControls RCL:

  • Visual Studio: Clique com o botão direito do mouse no projeto LazyLoadTest e selecione Adicionar>Referência de Projeto para adicionar uma referência de projeto ao RCL GrantImaharaRobotControls.
  • Visual Studio Code/.NET CLI: execute dotnet add reference {PATH} em um shell de comando da pasta do projeto. O marcador {PATH} é o caminho para o projeto RCL.

Especifique o assembly da RCL para carregamento lento no arquivo de projeto do aplicativo LazyLoadTest (.csproj):

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

O componente Router a seguir demonstra o carregamento da assemblagem GrantImaharaRobotControls.{FILE EXTENSION} quando o utilizador navega para /robot. Substitua o componente App padrão do aplicativo pelo componente App a seguir.

Durante as transições de página, uma mensagem com estilo é exibida para o usuário com o elemento <Navigating>. Para obter mais informações, consulte a secção Interação do usuário com o conteúdo <Navigating> na secção.

O conjunto é atribuído a AdditionalAssemblies, o que faz com que o roteador procure componentes roteáveis no conjunto, onde encontra o componente Robot. A rota do componente Robot é adicionada à coleção de rotas do aplicativo. Para obter mais informações, consulte o artigo ASP.NET Core Blazor roteamento e navegação e a seção Assembléias que incluem componentes roteáveis deste artigo.

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 bool grantImaharaRobotControlsAssemblyLoaded;

    private async Task OnNavigateAsync(NavigationContext args)
    {
        try
        {
            if ((args.Path == "robot") && !grantImaharaRobotControlsAssemblyLoaded)
            {
                var assemblies = await AssemblyLoader.LoadAssembliesAsync(
                    new[] { "GrantImaharaRobotControls.{FILE EXTENSION}" });
                lazyLoadedAssemblies.AddRange(assemblies);
                grantImaharaRobotControlsAssemblyLoaded = true;
            }
        }
        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 bool grantImaharaRobotControlsAssemblyLoaded;

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

Crie e execute o aplicativo.

Quando o componente Robot da RCL é solicitado em /robot, o conjunto GrantImaharaRobotControls.{FILE EXTENSION} é carregado e o componente Robot é renderizado. Pode inspecionar o carregamento de assemblies no separador Network das ferramentas para programadores do navegador.

Diagnosticar problemas

  • Se ocorrer renderização inesperada, como renderizar um componente de uma navegação anterior, confirme se o código é lançado se o token de cancelamento estiver definido.
  • Se os assemblies configurados para carregamento lento carregarem inesperadamente no início do aplicativo, verifique se o assembly está marcado para carregamento lento no arquivo de projeto.

Observação

Existe um problema conhecido ao carregar tipos de um assembly de carregamento lento. Para obter mais informações, consulte Blazor WebAssembly lazy loading assemblies not working when using @ref attribute in the component (dotnet/aspnetcore #29342).

Recursos adicionais