ASP.NET Core Blazor JavaScript com renderização estática do lado do servidor (SSR estática)

Observação

Esta não é a versão mais recente deste artigo. Para a versão atual, consulte a versão do .NET 10 deste artigo.

Este artigo explica como carregar JavaScript (JS) em um Blazor Web App com SSR estática (renderização estática no servidor) e navegação aprimorada.

Alguns aplicativos dependem de JS para executar tarefas de inicialização específicas para cada página. Ao usar o recurso de navegação aprimorada de Blazor, que permite ao usuário evitar recarregar a página inteira, o JS específico da página pode não ser executado novamente como esperado sempre que ocorrer uma navegação de página aprimorada.

Para evitar esse problema, não recomendamos confiar em elementos <script> específicos da página colocados fora do arquivo de layout aplicado ao componente. Em vez disso, os scripts devem registrar um afterWebStartedJS inicializador para executar a lógica de inicialização e usar um ouvinte de eventos para escutar atualizações de página causadas pela navegação aprimorada.

Eventos

Nos exemplos de ouvinte de eventos a seguir, o {CALLBACK} espaço reservado é a função de retorno de chamada.

  • O início de navegação aprimorado (enhancednavigationstart) aciona um retorno de chamada antes que o processo de navegação aprimorado ocorra:

    blazor.addEventListener("enhancednavigationstart", {CALLBACK});
    
  • O final de navegação aprimorada (enhancednavigationend) dispara um retorno de chamada depois que ocorre uma navegação aprimorada:

    blazor.addEventListener("enhancednavigationend", {CALLBACK});
    
  • O carregamento da página de navegação aprimorada (enhancedload) aciona um retorno de chamada sempre que a página é atualizada devido a uma navegação aprimorada, incluindo atualizações de streaming:

    blazor.addEventListener("enhancedload", {CALLBACK});
    

Para evitar esse problema, não recomendamos confiar em elementos <script> específicos da página colocados fora do arquivo de layout aplicado ao componente. Em vez disso, os scripts devem registrar um afterWebStartedJS inicializador para executar a lógica de inicialização e usar um ouvinte de eventos para escutar atualizações de página causadas pela navegação aprimorada:

blazor.addEventListener("enhancedload", {CALLBACK});

No exemplo anterior, o espaço reservado {CALLBACK} é a função retorno de chamada.

Exemplo de script de carregamento de página aprimorado

O exemplo a seguir demonstra uma maneira de configurar o código JS para ser executado quando uma página renderizada estaticamente com navegação aprimorada for inicialmente carregada ou atualizada.

O exemplo de componente PageWithScript a seguir é um componente no aplicativo que exige que os scripts sejam executados com SSR estático e navegação aprimorada. O exemplo de componente a seguir inclui um componente PageScript de uma RCL (biblioteca de classes) Razor que é adicionado à solução mais adiante neste artigo.

Components/Pages/PageWithScript.razor:

@page "/page-with-script"
@using BlazorPageScript

<PageTitle>Enhanced Load Script Example</PageTitle>

<PageScript Src="./Components/Pages/PageWithScript.razor.js" />

Welcome to my page.

No Blazor Web App, adicione o seguinte arquivo JS associado:

  • onLoad é chamado quando o script é adicionado à página.
  • onUpdate é chamado quando o script ainda existe na página após uma atualização aprimorada.
  • onDispose é chamado quando o script é removido da página após uma atualização aprimorada.

Components/Pages/PageWithScript.razor.js:

export function onLoad() {
  console.log('Loaded');
}

export function onUpdate() {
  console.log('Updated');
}

export function onDispose() {
  console.log('Disposed');
}

Em uma Biblioteca de classes Razor (RLC) (o exemplo de RCL é nomeado BlazorPageScript), adicione o seguinte módulo, que é um inicializador JS.

wwwroot/BlazorPageScript.lib.module.js:

const pageScriptInfoBySrc = new Map();

function registerPageScriptElement(src) {
  if (!src) {
    throw new Error('Must provide a non-empty value for the "src" attribute.');
  }

  let pageScriptInfo = pageScriptInfoBySrc.get(src);

  if (pageScriptInfo) {
    pageScriptInfo.referenceCount++;
  } else {
    pageScriptInfo = { referenceCount: 1, module: null };
    pageScriptInfoBySrc.set(src, pageScriptInfo);
    initializePageScriptModule(src, pageScriptInfo);
  }
}

function unregisterPageScriptElement(src) {
  if (!src) {
    return;
  }

  const pageScriptInfo = pageScriptInfoBySrc.get(src);
  
  if (!pageScriptInfo) {
    return;
  }

  pageScriptInfo.referenceCount--;
}

async function initializePageScriptModule(src, pageScriptInfo) {
  if (src.startsWith("./")) {
    src = new URL(src.substr(2), document.baseURI).toString();
  }

  const module = await import(src);

  if (pageScriptInfo.referenceCount <= 0) {
    return;
  }

  pageScriptInfo.module = module;
  module.onLoad?.();
  module.onUpdate?.();
}

function onEnhancedLoad() {
  for (const [src, { module, referenceCount }] of pageScriptInfoBySrc) {
    if (referenceCount <= 0) {
      module?.onDispose?.();
      pageScriptInfoBySrc.delete(src);
    }
  }

  for (const { module } of pageScriptInfoBySrc.values()) {
    module?.onUpdate?.();
  }
}

export function afterWebStarted(blazor) {
  customElements.define('page-script', class extends HTMLElement {
    static observedAttributes = ['src'];

    attributeChangedCallback(name, oldValue, newValue) {
      if (name !== 'src') {
        return;
      }

      this.src = newValue;
      unregisterPageScriptElement(oldValue);
      registerPageScriptElement(newValue);
    }

    disconnectedCallback() {
      unregisterPageScriptElement(this.src);
    }
  });

  blazor.addEventListener('enhancedload', onEnhancedLoad);
}

Não adicione uma tag <script> ao componente raiz do aplicativo, normalmente o componente App, para BlazorPageScript.lib.module.js porque o módulo nesse caso é um inicializador JS (afterWebStarted). Inicializadores JS são detectados e carregados automaticamente pela estrutura Blazor.

Na RCL, adicione o componente PageScript a seguir.

PageScript.razor:

<page-script src="@Src"></page-script>

@code {
    [Parameter]
    [EditorRequired]
    public string Src { get; set; } = default!;
}

O componente PageScript funciona normalmente no nível superior de uma página.

Se você colocar o componente PageScript em um layout do aplicativo (por exemplo, MainLayout.razor), o que resultará em um PageScript compartilhado entre as páginas que usam o layout, o componente só executará o onLoad após um recarregamento total da página e o onUpdate quando ocorrer qualquer atualização de página aprimorada, incluindo navegação aprimorada.

Para reutilizar o mesmo módulo entre as páginas, mas ter os retornos de chamada onLoad e onDispose invocados em cada alteração de página, acrescente uma string de consulta ao final do script para que ele seja reconhecido como um módulo diferente. Um aplicativo pode adotar a convenção de usar o nome do componente como o valor da string de consulta. No exemplo a seguir, a string de consulta é "counter" porque essa referência de componente PageScript é colocada em um componente Counter. Essa é apenas uma sugestão, e você pode usar qualquer esquema de string de consulta que preferir.

<PageScript Src="./Components/Pages/PageWithScript.razor.js?counter" />

Para monitorar alterações em elementos específicos do DOM, use o padrão MutationObserver no JS no cliente. Para obter mais informações, confira Interoperabilidade ASP.NET Core Blazor JavaScript (interoperabilidade JS).

Implementação sem usar uma RCL

A abordagem descrita neste artigo pode ser implementada diretamente em um Blazor Web App sem usar uma RCL (biblioteca de classes Razor) movendo os ativos da RCL para o aplicativo. No entanto, o uso de uma RCL é conveniente porque a RCL pode ser empacotada em um pacote NuGet para consumo por aplicativos Blazor em uma organização.

Se você mover os ativos para um Blazor Web App, renomeie o módulo (BlazorPageScript.lib.module.js) para corresponder ao aplicativo de acordo com as regras de nomenclatura de arquivo para inicializadores JS. Se o arquivo não for nomeado corretamente, Blazor não poderá detectar e carregar o módulo e o evento afterWebStarted não será executado automaticamente quando o aplicativo for iniciado.