Ensamblados de carga diferida en Blazor WebAssembly de ASP.NET Core

Nota

Esta no es la versión más reciente de este artículo. Para la versión actual, consulte la versión .NET 8 de este artículo.

Importante

Esta información hace referencia a un producto en versión preliminar, el cual puede sufrir importantes modificaciones antes de que se publique la versión comercial. Microsoft no proporciona ninguna garantía, expresa o implícita, con respecto a la información proporcionada aquí.

Para la versión actual, consulte la versión .NET 8 de este artículo.

El rendimiento de inicio de la aplicación Blazor WebAssembly puede mejorar si espera a que se requieran los ensamblados de la aplicación creados por el desarrollador antes de cargarlos, lo que se denomina carga diferida.

Las secciones iniciales de este artículo cubren la configuración de la aplicación. Para ver una demostración en funcionamiento, vea la sección Ejemplo completo al final de este artículo.

Este artículo solo se aplica a aplicaciones Blazor WebAssembly. La carga diferida de ensamblados no beneficia a las aplicaciones del lado servidor porque las aplicaciones representadas por el servidor no descargan ensamblados en el cliente.

La carga diferida no se debe usar para los ensamblados principales del entorno de ejecución, que podrían recortarse al publicarse y no estar disponibles en el cliente cuando se carga la aplicación.

Marcador de posición de extensión de archivo ({FILE EXTENSION}) para archivos de ensamblado

Los archivos de ensamblado usan el formato de empaquetado de Webcil para ensamblados .NET con una extensión de archivo .wasm.

A lo largo del artículo, el marcador de posición {FILE EXTENSION} representa "wasm".

Los archivos de ensamblado se basan en bibliotecas de vínculos dinámicos (DLL) con una extensión de archivo .dll.

A lo largo del artículo, el marcador de posición {FILE EXTENSION} representa "dll".

Configuración del archivo del proyecto

Marque los ensamblados para la carga diferida en el archivo del proyecto de la aplicación (.csproj) mediante el elemento BlazorWebAssemblyLazyLoad. Use el nombre del ensamblado con la extensión de archivo. El marco de Blazor evita que el ensamblado se cargue en el inicio de la aplicación.

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

El marcador de posición {ASSEMBLY NAME} es el nombre del ensamblado y el marcador de posición {FILE EXTENSION} es la extensión de archivo. Se requiere la extensión de archivo .

Incluya un elemento de BlazorWebAssemblyLazyLoad para cada ensamblado. Si un ensamblado tiene dependencias, incluya una entrada de BlazorWebAssemblyLazyLoad para cada dependencia.

Configuración del componente Router

El marco de Blazor registra de forma automática un servicio singleton para los ensamblados de carga diferida en las aplicaciones Blazor WebAssembly del lado cliente, LazyAssemblyLoader. El método LazyAssemblyLoader.LoadAssembliesAsync realiza las acciones siguientes:

  • Usa la interoperabilidad de JS para capturar ensamblados mediante una llamada de red.
  • Carga los ensamblados en el runtime que se ejecuta en WebAssembly en el explorador.

Nota:

Guía para solucionesBlazor WebAssemblyhospedadas en la sección Ensamblados de carga diferida en una solución Blazor WebAssembly hospedada por .

El componente Router de Blazor designa los ensamblados que busca Blazor para componentes enrutables, y también es responsable de representar el componente para el enrutador en el que navega el usuario. El método OnNavigateAsync del componente Router se usa junto a la carga diferida para cargar los ensamblados correctos para los punto de conexión que solicita un usuario.

La lógica se implementa dentro de OnNavigateAsync para determinar los ensamblados que se cargarán con LazyAssemblyLoader. Entre las opciones para estructurar la lógica se incluyen:

  • Comprobaciones condicionales dentro del método OnNavigateAsync.
  • Tabla de búsqueda que asigna rutas a los nombres de ensamblado, insertados en el componente o implementados en el bloque @code.

En el ejemplo siguiente:

  • Se especifica el espacio de nombres para Microsoft.AspNetCore.Components.WebAssembly.Services.
  • Se inserta el servicio LazyAssemblyLoader (AssemblyLoader).
  • El marcador de posición {PATH} es la ruta en la que se debería cargar la lista de ensamblados. En el ejemplo se usa una comprobación condicional para una sola ruta que carga un único conjunto de ensamblados.
  • El marcador de posición {LIST OF ASSEMBLIES} es la lista separada por comas de cadenas de nombres de archivos de ensamblado, incluidas sus extensiones (por ejemplo, "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);
           }
    }
}

Nota:

En el ejemplo anterior no se muestra el contenido del marcado de Razor del componente Router (...). Para ver una demostración con código completo, vea la sección Ejemplo completo de este artículo.

Nota

Con la publicación de ASP.NET Core 5.0.1 y para las versiones 5.x adicionales, el componente Router incluye el parámetro PreferExactMatches establecido en @true. Para más información, vea Migración de ASP.NET Core 3.1 a 5.0.

Ensamblados que incluyen componentes enrutables

Cuando la lista de ensamblados incluye componentes enrutables, la lista de ensamblados de una ruta determinada se pasa a la colección AdditionalAssemblies del componente de Router.

En el ejemplo siguiente:

  • La lista<Assembly> en lazyLoadedAssemblies pasa la lista de ensamblados a AdditionalAssemblies. El marco busca las rutas en los ensamblados y actualiza la colección si se encuentran nuevas rutas. Para acceder al tipo Assembly, el espacio de nombres para System.Reflection se incluye en la parte superior del archivo App.razor.
  • El marcador de posición {PATH} es la ruta en la que se debería cargar la lista de ensamblados. En el ejemplo se usa una comprobación condicional para una sola ruta que carga un único conjunto de ensamblados.
  • El marcador de posición {LIST OF ASSEMBLIES} es la lista separada por comas de cadenas de nombres de archivos de ensamblado, incluidas sus extensiones (por ejemplo, "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);
           }
    }
}

Nota:

En el ejemplo anterior no se muestra el contenido del marcado de Razor del componente Router (...). Para ver una demostración con código completo, vea la sección Ejemplo completo de este artículo.

Nota

Con la publicación de ASP.NET Core 5.0.1 y para las versiones 5.x adicionales, el componente Router incluye el parámetro PreferExactMatches establecido en @true. Para más información, vea Migración de ASP.NET Core 3.1 a 5.0.

Para más información, vea Enrutamiento y navegación de Blazor de ASP.NET Core.

Interacción del usuario con el contenido de <Navigating>

Mientras se cargan los ensamblados, que puede tardar varios segundos, el componente Router puede indicar al usuario que se está produciendo una transición de página con la propiedad Navigating del enrutador.

Para más información, vea Enrutamiento y navegación de Blazor de ASP.NET Core.

Control de las cancelaciones en OnNavigateAsync

El objeto NavigationContext pasado a la devolución de llamada de OnNavigateAsync contiene un elemento CancellationToken que se establece cuando se produce un nuevo evento de navegación. La devolución de llamada de OnNavigateAsync debe iniciarse cuando se establece el token de cancelación para evitar que continúe la ejecución de la devolución de llamada de OnNavigateAsync en una navegación no actualizada.

Para más información, vea Enrutamiento y navegación de Blazor de ASP.NET Core.

Eventos y archivos de ensamblado con nombre cambiado OnNavigateAsync

El cargador de recursos se basa en los nombres de ensamblado que se definen en el archivo blazor.boot.json. Si se cambia el nombre de los ensamblados, los nombres de ensamblado utilizados en la devolución de llamada de OnNavigateAsync y los nombres de ensamblado del archivo blazor.boot.json no estarán sincronizados.

Para rectificar esto, haga lo siguiente:

  • Compruebe si la aplicación se ejecuta en el entorno de producción (Production) al determinar los nombres de ensamblado que se van a usar.
  • Almacene los nombres de ensamblado con el nombre cambiado en un archivo independiente y lea desde ese archivo para determinar el nombre de ensamblado que se va a usar en el servicio LazyAssemblyLoader y la devolución de llamada OnNavigateAsync.

Ensamblados de carga diferida en una solución hospedada de Blazor WebAssembly

La implementación de carga diferida del marco admite la carga diferida con representación previa en una soluciónBlazor WebAssembly hospedada. Durante la representación previa, se supone que se cargan todos los ensamblados, incluidos los marcados para la carga diferida. Registre de forma manual el servicio LazyAssemblyLoader en el proyecto Server .

En la parte superior del archivo Program.cs del proyecto Server , agregue el espacio de nombres para Microsoft.AspNetCore.Components.WebAssembly.Services:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

En Program.cs del proyecto Server , registre el servicio:

builder.Services.AddScoped<LazyAssemblyLoader>();

En la parte superior del archivo Startup.cs del proyecto Server , agregue el espacio de nombres para Microsoft.AspNetCore.Components.WebAssembly.Services:

using Microsoft.AspNetCore.Components.WebAssembly.Services;

En Startup.ConfigureServices (Startup.cs) del proyecto Server, registre el servicio:

services.AddScoped<LazyAssemblyLoader>();

Ejemplo completo

La demostración de esta sección:

  • Crea un ensamblado de controles robot (GrantImaharaRobotControls.{FILE EXTENSION}) como una biblioteca de clases de Razor (RCL) que incluye un componente Robot (Robot.razor con una plantilla de ruta de /robot).
  • Carga de forma diferida el ensamblado de la RCL para representar su componente Robot cuando el usuario solicita la dirección URL /robot.

Cree una aplicación Blazor WebAssembly independiente para demostrar la carga diferida del ensamblado de una biblioteca de clases Razor. Dé un nombre al proyecto LazyLoadTest.

Agregue un proyecto de biblioteca de clases ASP.NET Core a la solución:

  • Haga clic con el botón derecho en el archivo de la solución Explorador de soluciones y, a continuación, seleccione Agregar>Nuevo proyecto. En el cuadro de diálogo de los nuevos tipos de proyecto, seleccione Biblioteca de clases Razor. Dé un nombre al proyecto GrantImaharaRobotControls. No active la casilla Páginas y vistas de soporte técnico.
  • Visual Studio Code/CLI de .NET: ejecute dotnet new razorclasslib -o GrantImaharaRobotControls desde símbolo del sistema. La opción -o|--output crea una carpeta y asigna un nombre al proyecto GrantImaharaRobotControls.

El componente de ejemplo que se presenta más adelante en esta sección usa un formulario de Blazor. En el proyecto de RCL, agregue el paquete Microsoft.AspNetCore.Components.Forms al proyecto.

Nota:

Para obtener instrucciones sobre cómo agregar paquetes a aplicaciones .NET, consulte los artículos de Instalación y administración de paquetes en Flujo de trabajo de consumo de paquetes (NuGet documentación). Confirme las versiones correctas del paquete en NuGet.org.

Cree una clase HandGesture en la RCL con un método ThumbUp que hipotéticamente haga que un robot realice un gesto de levantar el pulgar. El método acepta un argumento para el eje, Left o Right, como enum. El método devuelve true si se ejecuta correctamente.

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

Agregue el componente siguiente a la raíz del proyecto de la RCL. El componente permite al usuario enviar una solicitud de gesto de levantar el pulgar de la mano izquierda o derecha.

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

En el proyecto LazyLoadTest, cree una referencia de proyecto para la biblioteca RCL GrantImaharaRobotControls:

  • Visual Studio: haga clic con el botón derecho en el proyecto LazyLoadTest y seleccione Agregar>Referencia de proyecto para agregar una referencia de proyecto para la biblioteca RCL GrantImaharaRobotControls.
  • Visual Studio Code/CLI de .NET: ejecute dotnet add reference {PATH} en un shell de comandos desde la carpeta del proyecto. El marcador de posición {PATH} es la ruta al proyecto de la RCL.

Especifique el ensamblado de la RCL para la carga diferida en el archivo de proyecto de la aplicación de LazyLoadTest (.csproj):

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

El siguiente componente Router muestra cómo cargar el ensamblado GrantImaharaRobotControls.{FILE EXTENSION} cuando el usuario navega a /robot. Reemplace el componente predeterminado de la aplicación App por el siguiente componente App.

Durante las transiciones de página, se muestra al usuario un mensaje con estilo con el elemento <Navigating>. Para obtener más información, vea la sección Interacción del usuario con el contenido de <Navigating>.

El ensamblado se asigna a AdditionalAssemblies, lo que hace que el enrutador busque componentes enrutables en el ensamblado, donde encuentra el componente Robot. La ruta del componente Robot se agrega a la colección de rutas de la aplicación. Para más información, vea el artículo Enrutamiento y navegación de Blazor de ASP.NET Core y la sección Ensamblados que incluyen componentes enrutables de este artículo.

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

Compile y ejecute la aplicación.

Cuando el componente Robot de la RCL se solicita en /robot, el ensamblado GrantImaharaRobotControls.{FILE EXTENSION} se carga y el componente Robot se representa. Puede inspeccionar la carga del ensamblado en la pestaña Red de las herramientas de desarrollo del explorador.

Solución de problemas

  • Si se produce una representación inesperada, como una representación de un componente de una navegación anterior, confirme que el código se ejecuta si se establece el token de cancelación.
  • Si los ensamblados configurados para la carga diferida se cargan inesperadamente al iniciar la aplicación, compruebe que el ensamblado está marcado para la carga diferida en el archivo del proyecto.

Nota

Existe un problema conocido para cargar tipos desde un ensamblado cargado de forma diferida. Para obtener más información, vea Blazor WebAssembly lazy loading assemblies not working when using @ref attribute in the component (dotnet/aspnetcore #29342).

Recursos adicionales