Système de gestion des assetseQuantic.UI fournit un système déclaratif de dépendances d'assets qui permet aux composants de déclarer les scripts et feuilles de styles dont ils ont besoin. Les assets sont collectés automatiquement pendant le SSR, dédupliqués, et injectés dans le <head> de la page.
Le système résout un problème courant : les composants qui dépendent de bibliothèques externes (Prism.js, Chart.js, etc.) ont besoin que leurs scripts et leur CSS soient chargés, mais ne devraient pas incorporer de balises <script> en ligne dans le HTML qu'ils rendent. À la place, ils déclarent leurs dépendances, et le framework s'occupe de l'injection.
Parcours de l'arbre de composants → Collecter IRequireAssets → Collecter IComponentAssetProvider<T> → Dédupliquer → Injecter dans <head>
Tous les types sont dans eQuantic.UI.Server.Assets.
L'interface de base de tous les types d'assets.
string Key { get; } // Clé unique pour la déduplication
string? Id { get; } // Id HTML facultatif, pour manipuler côté client
string Render(); // Se rend comme une balise HTML
Type
Sortie
Format de la clé
ScriptAsset
<script src="..." defer></script>
script:{Src}
InlineScriptAsset
<script>...</script>
inline-script:{hash}
StylesheetAsset
<link rel="stylesheet" href="...">
stylesheet:{Href}
InlineStyleAsset
<style>...</style>
inline-style:{hash}
Tous les types acceptent un paramètre Id facultatif pour manipuler le DOM côté client :
new StylesheetAsset("https://cdn.example.com/theme.css", Id: "theme-css")
// Rend : <link rel="stylesheet" href="https://cdn.example.com/theme.css" id="theme-css">
Le builder fluide passé aux composants pour déclarer leurs dépendances :
public class AssetBuilder
AssetBuilder AddScript(string src, bool defer = true, string? id = null);
AssetBuilder AddInlineScript(string content, string? id = null);
AssetBuilder AddStylesheet(string href, string? id = null);
AssetBuilder AddInlineStyle(string content, string? id = null);
Collecte et déduplique les assets. L'ordre de rendu est optimisé : le CSS d'abord, puis le JS (le bon ordre de blocage du rendu).
StylesheetAsset → InlineStyleAsset → ScriptAsset → InlineScriptAsset
La déduplication utilise TryAdd avec la Key de l'asset, donc le premier enregistrement gagne.
Motif 1 : IRequireAssets (auto-déclaratif)
Pour les composants qui possèdent leurs dépendances. Le composant lui-même déclare ce dont il a besoin.
public class CodeBlock : StatelessComponent, IRequireAssets
public void ConfigureAssets(AssetBuilder assets)
"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css",
assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js");
Quand l'utiliser :
•
Les composants du framework (CodeBlock, graphiques, etc.)
•
Les composants dont les dépendances sont intrinsèques
•
Quand qui utilise le composant n'a pas besoin de connaître les bibliothèques sous-jacentes
Motif 2 : IComponentAssetProvider\<T\> (fournisseur externe)
Pour associer des assets à des composants de l'extérieur, typiquement des composants tiers qui n'implémentent pas IRequireAssets.
public class ChartJsAssetProvider : IComponentAssetProvider<ChartCanvas>
public void ConfigureAssets(AssetBuilder assets)
assets.AddScript("https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js");
Enregistrement (choisissez-en un) :
// Option 1 : scan automatique (découverts seuls dans les assemblys scannés)
options.ScanAssembly(typeof(Program).Assembly);
// Option 2 : enregistrement explicite via UIOptions
options.WithAssetProvider<ChartJsAssetProvider>();
// Option 3 : enregistrement manuel dans la DI
services.AddSingleton<IComponentAssetProvider<ChartCanvas>, ChartJsAssetProvider>();
Quand l'utiliser :
•
Les composants tiers que vous ne pouvez pas modifier
•
Les surcharges au niveau de l'application des assets d'un composant du framework
•
Les composants venus de paquets externes
Quand les deux motifs existent pour le même type de composant :
1.
IRequireAssets s'exécute d'abord (la valeur par défaut du composant)
2.
IComponentAssetProvider<T> s'exécute ensuite (le fournisseur externe)
La déduplication se fait par Key avec une sémantique de premier arrivé gagnant. Si vous avez besoin que le fournisseur surcharge l'asset par défaut d'un composant, utilisez une Key différente (par exemple une autre URL).
Pendant le rendu côté serveur, ServerRenderingService parcourt l'arbre de composants :
├── Crée l'AssetCollection
├── CollectAssets(rootComponent, assets, services, visited)
│ ├── Vérifie IRequireAssets → ConfigureAssets()
│ ├── Vérifie la DI pour IComponentAssetProvider<T> → ConfigureAssets()
│ ├── Récurse dans component.Children
│ └── Pour un StatelessComponent → Build() → récurse sur le résultat
└── Renvoie un ServerRenderResult avec les Assets
Les assets sont ensuite fusionnés dans HtmlShellOptions.HeadTags avant de servir la page.
Comportements clés :
•
Déduplication par type : chaque type de composant est traité une fois (via HashSet<Type>)
•
Déduplication par asset : chaque clé d'asset est enregistrée une fois (via Dictionary.TryAdd)
•
Zéro surcoût : les pages sans composant demandeur d'assets ne reçoivent aucune balise de plus
•
Dégradation élégante : si Build() échoue (par exemple à cause d'une DI manquante), le composant est sauté
Enregistrement automatique
Les implémentations d'IComponentAssetProvider<T> sont découvertes seules pendant AddUI() :
builder.Services.AddUI(options =>
options.ScanAssembly(typeof(Program).Assembly); // Trouve les fournisseurs ici
Le scan cherche toutes les classes non abstraites qui implémentent IComponentAssetProvider<T> dans les assemblys scannés et les enregistre en singletons via TryAddSingleton.
Les enregistrements explicites via WithAssetProvider<T>() ont la priorité sur ceux scannés automatiquement (ils sont enregistrés d'abord).
Exemple du monde réel : CodeBlock
Le composant CodeBlock démontre le motif complet :
public class CodeBlock : StatelessComponent, IRequireAssets
public void ConfigureAssets(AssetBuilder assets)
// Feuille de styles avec un id, pour changer de thème dynamiquement
"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css",
// Les scripts centraux de Prism.js
assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js");
assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/plugins/autoloader/prism-autoloader.min.js");
// Fonctions utilitaires + bascule automatique entre thème clair et sombre
"function copyToClipboard(id){...}" +
"function toggleCodeBlock(id){...}" +
"function updateTheme(){...}" + // Bascule entre prism.css et prism-tomorrow.css
"new MutationObserver(function(){updateTheme()})" +
".observe(document.documentElement,{attributes:true,attributeFilter:['class']});" +
Qui développe utilise simplement new CodeBlock(code, "csharp"), et les scripts de Prism.js, le CSS, la bascule de thème et les fonctions utilitaires sont tous gérés automatiquement.
Depuis 0.2.0-preview.1
L'icône de l'application, dessinée en C# (IAppIcon)
L'icône du lanceur est un composant comme n'importe quel autre, dans le même vocabulaire, elle ne peut donc pas dériver de la marque avec laquelle l'application dessine déjà :
public sealed class AppIcon : IAppIcon
public VisualNode Build(ComponentContext context) =>
Box(new BoxStyle { Background = Brand, CornerRadius = new CornerRadii(0) },
Text("eQ", TypeRole.Display, Ink).Centered());
Deux limites, toutes deux venues du médium plutôt que du framework :
•
Remplissez tout le carré, en opaque. Une icône transparente est composée par-dessus ce que le lanceur montre à ce moment-là, ce qui n'est jamais ce contre quoi vous avez conçu.
•
Pas d'état, pas d'interaction. Elle est construite une fois ; un gestionnaire de pression sur une icône est un gestionnaire que personne ne peut atteindre. Tenez-vous aux formes, aux dégradés et au plus à une ou deux lettres, parce qu'à la taille à laquelle un écran d'accueil la montre vraiment, tout ce qui est plus fin est une tache.
Le build la rastérise à chaque taille que veut chaque plateforme, et lie celles du web dans le head sans que l'application le dise.