JavaScript isolado no Blazor: módulo por componente, sem função global
Coloque o script ao lado do componente, importe como módulo ES pelo IJSRuntime, passe ElementReference em vez de id e libere tudo no DisposeAsync.
A tela do painel tem um mapa e um gráfico. Cada componente veio com seu script, e os dois declaram function iniciar() no escopo global. O gráfico carrega por último, sobrescreve o window.iniciar e o mapa para de funcionar, mas só nessa tela. A resposta curta: no Blazor, coloque o JavaScript num arquivo ao lado do componente (Mapa.razor.js), exporte as funções como módulo ES, importe pelo IJSRuntime com "import" no primeiro render, passe o elemento por ElementReference e libere o módulo no DisposeAsync.
JavaScript solto na página é espaço compartilhado: tudo que vai para o window pode colidir com outra coisa. Um módulo ES tem escopo próprio, e só quem importou enxerga o que ele exporta. É o mesmo princípio do componente, aplicado ao script.
Por que o script global quebra componentes
O modelo antigo de interop do Blazor chamava funções pelo nome a partir do window: você incluía um <script> no layout e chamava JS.InvokeVoidAsync("iniciar"). Isso tem três problemas que crescem com o projeto. Nomes colidem entre componentes e bibliotecas. Todo script carrega em todas as páginas, mesmo onde não é usado. E nada amarra o script ao ciclo de vida do componente, então quando o componente sai da tela, os listeners e timers que o script criou continuam vivos.
O último é o mais traiçoeiro. Navegue para fora do painel e volte cinco vezes: se o script registra um listener de resize a cada entrada e nunca remove, agora são cinco listeners, quatro deles apontando para elementos que não existem mais. Juntar todos os scripts num arquivo grande, aliás, agrava o primeiro e o segundo problema, como descrito em juntar todos os scripts do kit no mesmo arquivo.
Como isolar o script em módulo
- Crie o arquivo JavaScript com o mesmo nome do componente e a extensão
.razor.js, na mesma pasta. O Blazor publica esse arquivo junto com os estáticos. - Exporte com
exportsó as funções que o C# precisa chamar. - No componente, importe o módulo no
OnAfterRenderAsync, apenas no primeiro render. - Passe o elemento com
@refeElementReference, nunca por id fixo. - Faça a função de início devolver um objeto com um método de limpeza.
- Implemente
IAsyncDisposablee chame a limpeza antes de liberar o módulo.
Exemplo completo: componente com módulo e limpeza
O exemplo é um componente que acompanha a largura do próprio area, o tipo de coisa que uma biblioteca de gráfico ou de mapa precisa para se redesenhar. O script fica em Components/Painel/Largura.razor.js:
export function iniciar(elemento, dotnet) {
const aoRedimensionar = () => {
dotnet.invokeMethodAsync('AtualizarLargura', elemento.clientWidth);
};
window.addEventListener('resize', aoRedimensionar);
aoRedimensionar();
return {
destruir() {
window.removeEventListener('resize', aoRedimensionar);
}
};
}
Nada vai para o window além do listener, e o próprio módulo devolve a forma de removê-lo. O componente em Components/Painel/Largura.razor:
@implements IAsyncDisposable
@inject IJSRuntime JS
<div @ref="area">
<p>Largura disponível: @largura px</p>
</div>
@code {
private ElementReference area;
private IJSObjectReference? modulo;
private IJSObjectReference? instancia;
private DotNetObjectReference<Largura>? referencia;
private int largura;
protected override async Task OnAfterRenderAsync(bool firstRender)
{
if (!firstRender) return;
modulo = await JS.InvokeAsync<IJSObjectReference>(
"import", "./Components/Painel/Largura.razor.js");
referencia = DotNetObjectReference.Create(this);
instancia = await modulo.InvokeAsync<IJSObjectReference>(
"iniciar", area, referencia);
}
[JSInvokable]
public void AtualizarLargura(int valor)
{
largura = valor;
StateHasChanged();
}
public async ValueTask DisposeAsync()
{
try
{
if (instancia is not null)
{
await instancia.InvokeVoidAsync("destruir");
await instancia.DisposeAsync();
}
if (modulo is not null)
await modulo.DisposeAsync();
}
catch (JSDisconnectedException)
{
}
referencia?.Dispose();
}
}
Alguns detalhes que fazem diferença. O caminho do import é relativo à raiz do site e espelha a pasta do componente no projeto; num Razor Class Library, o caminho começa com ./_content/NomeDaBiblioteca/. A importação acontece no OnAfterRenderAsync porque durante a pré-renderização no servidor ainda não existe navegador para executar JavaScript. E a JSDisconnectedException é esperada no Blazor Server quando o circuito já caiu, por exemplo ao fechar a aba: nesse caso não há JavaScript do outro lado para limpar.
Por que ElementReference e não id
Se o componente usa document.getElementById('grafico') e aparece duas vezes na mesma página, as duas instâncias pegam o mesmo elemento. O @ref entrega ao JavaScript o elemento exato daquela instância, sem depender de id único. Isso também evita colisão com outro componente que tenha escolhido o mesmo id.
O que fica no C# e o que vai para o JavaScript
O JavaScript cuida do que o navegador faz melhor: foco, medida de elemento, biblioteca visual, APIs do navegador como área de transferência. Regra de negócio, validação que importa e decisão de permissão ficam no C#, e o que protege dado fica no servidor. "É mais fácil desenhar em JS" não é motivo para levar o cálculo do desconto para lá.
Se o módulo falhar ao carregar, por um erro de caminho ou de rede, o componente deve dizer isso em vez de mostrar um botão mudo. Envolva o import em try/catch de JSException e exiba uma mensagem. Para interações que precisam funcionar com teclado, como reordenar listas, veja arrastar no Blazor com teclado.
Erros comuns
- Declarar funções no
window: dois componentes com o mesmo nome de função quebram um ao outro, e só em algumas telas. - Importar no
OnInitializedAsync: com pré-renderização ativa, a chamada de interop falha porque ainda não há navegador. - Não implementar
DisposeAsync: listeners e timers continuam vivos depois que o componente sai, e se acumulam a cada navegação. - Esquecer de liberar o
DotNetObjectReference: o componente fica preso na memória enquanto o JavaScript mantiver a referência. - Usar id fixo para achar o elemento: com duas instâncias do componente, ambas mexem no mesmo elemento.
- Mover regra de negócio para o script: a regra fica duplicada, difícil de testar e editável por qualquer um no DevTools.
Checklist
- Cada script de componente está num
.razor.jsao lado do componente. - O script não cria nada global além de listeners que ele mesmo remove.
- A importação acontece no primeiro
OnAfterRenderAsync. - Elementos chegam ao JavaScript por
ElementReference. - O componente implementa
IAsyncDisposablee chama a limpeza do script. DotNetObjectReferenceé liberado no descarte.- Falha ao carregar o módulo aparece como mensagem na tela.
Abra a tela do seu sistema que tem mais componentes com JavaScript e navegue para fora e de volta cinco vezes. Os listeners se multiplicaram, e algum script ainda declara função no window?
Referência: https://www.macoratti.net/21/01/blz_isolajs1.htm