Fermer

août 24, 2022

Meilleures pratiques Blazor : chargement des données

Meilleures pratiques Blazor : chargement des données


Blazor facilite le chargement des données dans vos composants, mais il y a quelques éléments clés à surveiller.

Blazor facilite la récupération des données et les utilise pour rendre vos composants.

Dans la plupart des cas, vous pouvez placer votre API (ou des appels de service directs, si vous utilisez Blazor Server) dans le OnIntialized (ou équivalent asynchrone), stockez les données résultantes dans un champ et laissez Blazor les restituer à partir de là.

@page "/Products"
@inject ITopSellersQuery topSellersQuery  
  
<ul>  
    @foreach (var product in TopSellingProducts)  
    {  
        <li>@product.Name</li>  
    }
</ul> 
@code {  
  
    private IList<ProductDetails> TopSellingProducts;  
  
    protected override Task OnInitializedAsync()  
    {  
        TopSellingProducts = topSellersQuery.List();    
        return base.OnInitializedAsync();  
    }  
      
}

Mais à mesure que votre application se développe et que vous ajoutez de plus en plus de composants, des questions et des défis clés commencent à émerger.

  • Quels composants doivent récupérer les données (et lesquels doivent accepter les données d’ailleurs) ?
  • Comment gérez-vous de gros volumes de données tout en gardant votre interface utilisateur réactive ?
  • Et si vous avez besoin de prendre en charge à la fois Blazor WASM et Blazor Server ?

Voici trois conseils pour assurer le bon fonctionnement de votre application Blazor (et la facilité de développement, d’extension et de maintenance pour vous).

Récupérer les données une fois et pousser vers d’autres composants

Au fur et à mesure que vous ajoutez des composants à votre application, vous pouvez commencer à vous demander s’ils doivent récupérer leurs propres données.

Par exemple, imaginez un composant qui récupère une liste de produits et les affiche sous forme de « cartes » dans l’interface utilisateur.

Exemple d'une page montrant divers produits avec une carte pour chacun, y compris une image d'espace réservé, le prix et une courte description

Une option serait de récupérer la liste des produits au niveau « supérieur » et de transmettre les détails du produit à un composant de carte.

Le composant ProductList récupère les données d'une API puis les transmet à un composant ProductCard

Alternativement, vous pouvez laisser chaque fiche produit récupérer ses propres détails.

Alors quelle est la bonne ? Eh bien, comme toujours, le contexte compte.

Chaque fois que vous effectuez un appel pour récupérer des données, vous introduisez un effet secondaire sur un composant. En plus du rôle principal du composant (pour gérer la logique de l’interface utilisateur, rendre l’interface utilisateur et réagir aux interactions de l’utilisateur), il doit maintenant passer un appel ailleurs (pour récupérer des données).

Introduisez trop d’effets secondaires à différents « niveaux » dans votre application, et vous courez le risque qu’il soit plus difficile de prédire le comportement d’un composant donné.

Les effets secondaires sont, par nature, imprévisibles. De nouvelles données peuvent soudainement arriver, ce qui entraîne un comportement différent de votre composant.

Dans ce cas, où nous affichons un certain nombre de produits dans une grille, il est logique de faire un appel pour récupérer les données dans le niveau supérieur ProductList composant, puis parcourez chaque produit et rendez un ProductCard pour chacun.

ProductList.razor

<ul>  
    @foreach (var product in TopSellingProducts)  
    {  
        <li>            
            <ProductCard Details="product"/>  
        </li>    
    }
</ul>

Nous pouvons garder le ProductCard composant lui-même agréable et simple.

ProductCard.razor

<div>  
    @Details.Name  
</div>  
<div>  
    @Details.Description  
</div>  
  
@code {  
    [Parameter]  
    public ProductDetails Details { get; set; }  
}

Avec cette approche, il est assez simple d’ajouter des éléments tels que le filtrage, le tri et la pagination, car nous pouvons réexécuter la requête entière dans ProductListrécupérez les données mises à jour et les fiches produits prendront soin d’elles-mêmes.

Nous pouvons également prendre soin de récupérer uniquement les champs dont nous avons besoin (en fonction de ce que nous affichons dans ce ProductCard composant), évitant ainsi de surcharger les données du backend/de l’API.

Mais qu’en est-il d’une « page » de détails sur le produit ?

Dans ce cas, il est presque certainement judicieux d’introduire un ProductDetails composant qui récupère ses propres données, pour plusieurs raisons.

  • Nous allons vouloir récupérer beaucoup plus de données pour une page de détails que ce que nous montrons dans la liste (mais seulement pour un produit spécifique à la fois).
  • Nous souhaiterons probablement que les utilisateurs puissent accéder directement aux détails du produit (et/ou partager un lien vers la « page » des détails).

Pour ces raisons, la page de détails du produit se prête à être une « page de niveau supérieur », qui récupère ses propres données, plutôt qu’un sous-composant.

En règle générale, j’essaie de m’en tenir à la récupération des données au niveau de la « page », en transmettant les données à tous les composants rendus sur cette même « page ». Tout ce vers quoi vous naviguez en tant que page distincte récupère alors ses propres données.

Cela apporte quelques avantages :

  • Il est plus facile de trouver d’où proviennent les données (car vous êtes cohérent quant au moment et à l’endroit où vous chargez les données).
  • Vous pouvez créer des composants très petits et simples s’ils n’ont pas besoin de récupérer leurs propres données.
  • Les composants qui ne récupèrent pas de données sont plus faciles à raisonner et à comprendre.
  • Si vous avez besoin de filtrer, trier ou parcourir des données, vous pouvez gérer tout cela en un seul endroit (le composant de niveau supérieur).
  • Vous pouvez gérer et minimiser le nombre d’appels réseau.

Naturellement, ce n’est pas une règle absolue.

Par exemple, si vous avez quelque chose comme un tableau de bord, affichant de nombreuses informations disparates, il serait logique de laisser chaque widget de tableau de bord récupérer ses propres données.

Vous ne voudriez pas récupérer toutes les données de chaque widget de tableau de bord au niveau supérieur de la « page de tableau de bord », car les données de chaque widget sont en grande partie des données distinctes et non liées, donc les récupérer toutes ensemble n’apporte pas beaucoup d’avantages (et couple directement tous les widgets au tableau de bord lui-même).

Utilisez la virtualisation pour garder votre interface utilisateur dynamique si vous affichez beaucoup de données

Et si vous avez besoin de charger beaucoup de données et de les rendre dans votre application Blazor ? Nous parlons de milliers, de centaines de milliers ou même de millions de lignes.

Si vous essayez d’utiliser un foreach boucle avec des millions d’enregistrements, votre interface utilisateur Blazor tentera de tous les rendre et s’arrêtera brusquement.

Heureusement, le Virtualize Le composant peut ramener votre application au bord du gouffre et la rendre à nouveau accrocheuse !

ProductList.razor

<ul>  
    <Virtualize Items="@TopSellingProducts" Context="product">  
        <li>            
            <ProductCard Details="product"/>  
        </li>    
    </Virtualize>  
</ul>

Blazor affichera désormais uniquement le nombre d’éléments visibles à l’écran. Au fur et à mesure que vous faites défiler la page, il continuera à réutiliser ces mêmes éléments, en échangeant les données au fur et à mesure.

Votre application, exécutée dans le navigateur, reste réactive, malgré le grand nombre d’enregistrements que vous avez chargés en arrière-plan.

Avec cette approche, votre composant va toujours récupérer et charger tous les enregistrements en mémoire, mais il n’essaiera pas de les restituer tous en même temps.

Si vous souhaitez aller plus loin et charger paresseusement ces données (récupération uniquement des données réellement visibles à l’écran), vous pouvez via le ItemsProvider paramètre.

ProductList.razor

@page "/Products"  
@inject ITopSellersQuery topSellersQuery  
  
<ul>  
    <Virtualize ItemsProvider="LoadProductDetails" Context="product">  
        <li>            
            <ProductCard Details="product"/>  
        </li>    
    </Virtualize>  
</ul>

Désormais, au lieu de transmettre une liste d’éléments préremplis au composant, nous lui avons expliqué comment récupérer ses propres données.

private async ValueTask<ItemsProviderResult<ProductDetails>> LoadProductDetails(ItemsProviderRequest request)  
{  
    var totalCount = TopSellersQuery.Count();  
    var numProducts = Math.Min(request.Count, totalCount - request.StartIndex);  
    var results = topSellersQuery.List (request.StartIndex, numProducts);
    
   	return new ItemsProviderResult<ProductDetails>(results, totalCount);  
}

LoadProductDetails fait un peu de travail pour déterminer l’index du premier enregistrement que nous voulons charger.

Request.count fait référence au nombre d’enregistrements que le composant Virtualize doit rendre (qui varie en fonction de facteurs tels que la taille de l’écran et si la fenêtre du navigateur est maximisée ou non).

Nous devons dire Virtualize quel est le nombre total d’enregistrements (d’où l’appel à récupérer ce nombre en premier – nous pourrions également le faire une fois et le stocker dans un champ).

Nous effectuons quelques calculs rapides pour nous assurer que nous savons combien d’enregistrements récupérer :

Math.Min(request.Count, totalCount - request.StartIndex); 

Par exemple, si Virtualize demandé 80 enregistrements (parce que c’est le nombre qu’il pouvait rendre), mais nous n’avions que 50 enregistrements au total, cela garantirait que nous n’avons tenté d’en charger que 50 (car c’est tout ce que nous avons !).

Après cela, il s’agit de dire à notre requête par quel enregistrement commencer et combien charger.

var results = topSellersQuery.List (request.StartIndex, numProducts);

Lorsque nous renvoyons ces résultats, Virtualize fait le reste et restitue les données renvoyées.

Besoin de prendre en charge le serveur et WASM ? Envisagez d’utiliser une interface

Enfin, si vous envisagez de prendre en charge Server et WASM, vous ne voudrez peut-être pas passer des appels via HTTP dans tous les cas. Il semble redondant de faire un appel API lorsque vous exécutez sous Blazor Server et que vous avez déjà accès à la base de données elle-même.

Mais pour WASM, vous auriez besoin de cet appel HTTP (car WASM s’exécute dans le navigateur, qui n’a pas d’accès direct aux données).

La solution la plus simple consiste à utiliser une interface, puis à désactiver l’implémentation pour les différents modèles d’hébergement.

public interface ITopSellersQuery  
{  
    IEnumerable<ProductDetails> List(int startIndex, int numProducts);  
}

Vous pouvez utiliser l’interface dans votre composant :

@page "/Products"  
@inject ITopSellersQuery topSellersQuery

Placez ce composant dans une bibliothèque de classes partagée et les deux clients Server/WASM peuvent le restituer.

À partir de là, vous voudriez deux implémentations de ITopSellersQuery—une version WASM :

class TopSellersQueryWASM : ITopSellersQuery  
{  
    private readonly HttpClient _httpClient;  
  
    public TopSellersQueryWASM(HttpClient httpClient)  
    {  
        _httpClient = httpClient;  
    }  
  
    public async Task<IEnumerable<ProductDetails>> List(int startIndex, int numProducts)  
    {  
        var requestUri = 
            $"api/products?skip={startIndex}&take={numProducts}";  
        
        return await _httpClient
            .GetFromJsonAsync<IEnumerable<ProductDetails>>(requestUri);  
    }  
}

Et une version Serveur :

public class class TopSellersQuery : ITopSellersQuery {
    public IEnumerable<ProductDetails> List(int startIndex, int numProducts)  
    {  
        return Enumerable.Range(startIndex, numProducts)
                         .Select(x => new ProductDetails  
        {  
            Id = x,  
            Description = "Test Product Description",  
            Name = "A Product",  
            Price = 200m  
        });  
    }
}

Ici, nous utilisons des données codées en dur, mais cela pourrait également être un appel à une base de données (en utilisant EF Core, Dapper ou tout autre ORM de votre choix).

Si vous enregistrez la version WASM dans votre projet WASM et la version Server dans votre projet Blazor Server, vous devriez être prêt à partir.

Vous pouvez ignorer les cycles réseau supplémentaires lorsque vous exécutez Blazor Server et accéder directement aux données, mais utilisez toujours le même composant partagé pour Blazor WASM (cette fois en appelant le même code via HTTP).

Pour que la version WASM fonctionne, vous devez exposer un point de terminaison (dans la partie serveur de votre application) qui appelle cette même requête.

Voici un exemple utilisant des API minimales :

app.MapGet("api/products", async (  
    HttpContext context,   
    ITopSellersQuery query,  
    [FromQuery] int skip,   
    [FromQuery] int take) => await query.List(skip, take));

Avec cela, vous pouvez réutiliser vos composants sur Blazor Server et WASM, en étant sûr que votre application empruntera la voie la plus efficace pour charger les données.

En résumé

Blazor dispose d’un mécanisme simple et évolutif pour charger et rendre les données dans un composant.

La plupart du temps, cela « fonctionne simplement », mais souvenez-vous de ces trois conseils si vous avez besoin de récupérer beaucoup de données, ou si vous voulez simplement pouvoir revenir à votre application dans quelques mois et déterminer facilement d’où proviennent ces données :

  • Visez la cohérence dans la façon dont (et où) vous récupérez vos données.
  • Utilisation Virtualize si vous avez besoin de gérer de nombreux enregistrements.
  • Utilisez des interfaces pour réduire les cycles réseau lorsque vous utilisez Blazor Server mais que vous souhaitez toujours prendre en charge Blazor WASM.

Curieux a propos de Blazor pour .NET MAUI? Lisez les réflexions de Jon sur quoi, comment et quand l’utiliser.




Source link