eQuantic.UIeQuantic.UI
Docs
Playground
GitHub
Docspt-BR
Sistema de gestão de assets
Edit this page
4 min read
🌐 Esta página em: English · Português
O eQuantic.UI oferece um sistema declarativo de dependências de assets que permite aos componentes declararem os scripts e folhas de estilo de que precisam. Os assets são coletados automaticamente durante o SSR, deduplicados e injetados no <head> da página.
Visão geral
O sistema resolve um problema comum: componentes que dependem de bibliotecas externas (Prism.js, Chart.js, etc.) precisam que os scripts/CSS deles sejam carregados, mas não deveriam embutir tags <script> inline no HTML renderizado. Em vez disso, eles declaram dependências, e o framework cuida da injeção.
1
Percurso da árvore de componentes → Coleta IRequireAssets → Coleta IComponentAssetProvider<T> → Deduplica → Injeta no <head>
Tipos principais
Todos os tipos estão em eQuantic.UI.Core.Assets.
IAsset
Interface base para todos os tipos de asset.
1
2
3
4
5
6
public interface IAsset
{
string Key { get; } // Chave única para deduplicação
string? Id { get; } // Id HTML opcional para manipulação no cliente
string Render(); // Renderiza como tag HTML
}
Implementações de asset
Tipo
Saída
Formato da chave
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}
Todos os tipos aceitam um parâmetro Id opcional para manipulação do DOM no cliente:
1
2
new StylesheetAsset("https://cdn.example.com/theme.css", Id: "theme-css")
// Renderiza: <link rel="stylesheet" href="https://cdn.example.com/theme.css" id="theme-css">
AssetBuilder
Builder fluente passado aos componentes para declarar dependências:
1
2
3
4
5
6
7
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);
}
AssetCollection
Coleta e deduplica os assets. A ordem de renderização é otimizada: CSS primeiro, depois JS (a ordem correta de bloqueio de render).
1
StylesheetAsset → InlineStyleAsset → ScriptAsset → InlineScriptAsset
A deduplicação usa TryAdd com a Key do asset, então o primeiro registro vence.
Dois padrões
Padrão 1: IRequireAssets (autodeclarante)
Para componentes que são donos das próprias dependências. O componente declara ele mesmo o que precisa.
1
2
3
4
5
6
7
8
9
10
public class CodeBlock : StatelessComponent, IRequireAssets
{
public void ConfigureAssets(AssetBuilder assets)
{
assets.AddStylesheet(
"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css",
id: "prism-theme");
assets.AddScript("https://cdn.jsdelivr.net/npm/prismjs@1.29.0/prism.min.js");
}
}
Quando usar:
Componentes do framework (CodeBlock, gráficos, etc.)
Componentes cujas dependências são intrínsecas
O desenvolvedor que usa o componente não precisa saber das bibliotecas por baixo
Padrão 2: IComponentAssetProvider\<T\> (provedor externo)
Para associar assets a componentes externamente, tipicamente componentes de terceiros que não implementam IRequireAssets.
1
2
3
4
5
6
7
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");
}
}
Registro (escolha um):
1
2
3
4
5
6
7
8
// Opção 1: varredura automática (descoberto sozinho nos assemblies varridos)
options.ScanAssembly(typeof(Program).Assembly);
// Opção 2: registro explícito via UIOptions
options.WithAssetProvider<ChartJsAssetProvider>();
// Opção 3: registro manual na DI
services.AddSingleton<IComponentAssetProvider<ChartCanvas>, ChartJsAssetProvider>();
Quando usar:
Componentes de terceiros que você não pode modificar
Sobreposições no nível do app para assets de componentes do framework
Componentes vindos de pacotes externos
Prioridade
Quando os dois padrões existem para o mesmo tipo de componente:
1.
O IRequireAssets executa primeiro (padrão do componente)
2.
O IComponentAssetProvider<T> executa depois (provedor externo)
A deduplicação é por Key, com semântica de o primeiro vence. Se você precisa que o provedor sobreponha o asset padrão de um componente, use uma Key diferente (por exemplo, outra URL).
Como funciona (pipeline de SSR)
Durante a renderização no servidor, o ServerRenderingService percorre a árvore de componentes:
1
2
3
4
5
6
7
8
9
RenderPageAsync()
├── Cria a AssetCollection
├── CollectAssets(rootComponent, assets, services, visited)
│ ├── Verifica IRequireAssets → ConfigureAssets()
│ ├── Verifica a DI por IComponentAssetProvider<T> → ConfigureAssets()
│ ├── Recursa em component.Children
│ └── Para StatelessComponent → Build() → recursa no resultado
├── Renderiza o HTML
└── Devolve o ServerRenderResult com os Assets
Os assets são então mesclados em HtmlShellOptions.HeadTags antes de servir a página.
Comportamentos-chave:
Deduplicação por tipo: cada tipo de componente é processado uma vez (via HashSet<Type>)
Deduplicação por asset: cada chave de asset é registrada uma vez (via Dictionary.TryAdd)
Custo zero: páginas sem componentes que exigem assets não ganham tag extra nenhuma
Degradação graciosa: se o Build() falhar (por exemplo, DI faltando), o componente é pulado
Registro automático
As implementações de IComponentAssetProvider<T> são descobertas automaticamente durante o AddUI():
1
2
3
4
builder.Services.AddUI(options =>
{
options.ScanAssembly(typeof(Program).Assembly); // Acha os provedores aqui sozinho
});
A varredura procura todas as classes não abstratas que implementam IComponentAssetProvider<T> nos assemblies varridos e as registra como singletons via TryAddSingleton.
Registros explícitos via WithAssetProvider<T>() têm prioridade sobre os varridos automaticamente (são registrados primeiro).
Exemplo do mundo real: CodeBlock
O componente CodeBlock demonstra o padrão completo:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class CodeBlock : StatelessComponent, IRequireAssets
{
public void ConfigureAssets(AssetBuilder assets)
{
// Folha de estilo com id para troca dinâmica de tema
assets.AddStylesheet(
"https://cdn.jsdelivr.net/npm/prismjs@1.29.0/themes/prism-tomorrow.min.css",
id: "prism-theme");
// Scripts principais do 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");
// Funções utilitárias + troca automática de tema claro/escuro
assets.AddInlineScript(
"function copyToClipboard(id){...}" +
"function toggleCodeBlock(id){...}" +
"(function(){" +
"function updateTheme(){...}" + // Alterna entre prism.css e prism-tomorrow.css
"updateTheme();" +
"new MutationObserver(function(){updateTheme()})" +
".observe(document.documentElement,{attributes:true,attributeFilter:['class']});" +
"})();"
);
}
}
O desenvolvedor só usa new CodeBlock(code, "csharp"), e os scripts do Prism.js, o CSS, a troca de tema e as funções utilitárias são todos tratados automaticamente.
Desde 0.2.0-preview.1
O ícone do app, desenhado em C# (IAppIcon)
O ícone do lançador é um componente como qualquer outro, no mesmo vocabulário, então ele não consegue divergir da marca com que o app já desenha:
1
2
3
4
5
6
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());
}
Duas cercas, ambas vindas do meio e não do framework:
Preencha o quadrado inteiro, opaco. Um ícone transparente é composto contra o que quer que o lançador esteja mostrando, que nunca é aquilo contra o que você desenhou.
Sem estado, sem interação. Ele é construído uma vez; um handler de toque num ícone é um handler que ninguém alcança. Fique em formas, gradientes e no máximo uma letra ou duas, porque no tamanho em que uma tela inicial de fato mostra, qualquer coisa mais fina vira borrão.
O build o rasteriza em cada tamanho que cada plataforma quer, e liga os do web no head sem o app precisar dizer nada.