LE 21/10/2025 — 4 MINUTES DE LECTURE

Appeler du javascript dans un composant Blazor Static SSR

Blazor est désormais capable de faire du rendu statique côté serveur. Une nouveauté bienvenue qui apporte aussi son lot de défis, notamment avec l'enhanced navigation

Appeler du javascript dans un composant Blazor Static SSR

La version Blazor en .NET 8 a été une petite révolution pour le framework UI, mixant les modes de rendu historiques de la version serveur (SSR interactif avec SignalR) et de la version client (CSR avec WebAssembly) dans un seul et même projet : Blazor Web App.

La partie serveur gagne en complexité subtilité avec un nouveau mode de rendu : le SSR statique. Le framework est désormais capable, avec ce mode, de faire du rendu côté serveur de la façon la plus traditionnelle possible : une requête envoyée, qui retourne une page HTML. Il devient ainsi l’équivalent de MVC ou de Razor Pages, permettant une approche différente de son fonctionnement initial en mode SPA. On le classe désormais dans la même catégorie que les méta-frameworks JS comme Next.js ou Nuxt.

Chaque mode de rendu possède ses propres avantages et inconvénients selon le cas d’utilisation. Et chacun apporte son lot de défis à surmonter. C’est particulièrement vrai pour l’interopérabilité avec JavaScript. 

Une des choses que j’apprécie le plus avec Blazor, c’est de pouvoir créer une application complète sans une seule ligne de JS. Non pas que j’y sois allergique, mais c’est souvent plus simple de travailler avec un seul langage de bout en bout.

Parfois, certaines limitations du framework s’imposent à nous, rendant l’utilisation de JavaScript préférable. Je pense, par exemple, à certaines manipulations du DOM ou à l’intégration de bibliothèques JS sans équivalent dans l’écosystème .NET (ou simplement à votre bibliothèque JavaScript préférée).

Un des moyens de mettre en place l’interopérabilité Blazor/JS, c’est l’interface IJSRuntime, qui fournit des méthodes comme Invoke permettant d’appeler des fonctions JS directement depuis le code C#. Cette méthode fonctionne à partir du moment où votre composant dispose d’une interactivité — qu’elle soit WASM ou WebSocket. Sur le nouveau mode de rendu SSR statique, c’est une autre paire de manches...

 

L'enhanced navigation en fauteur de trouble

Dans le cas du SSR statique, vous ne pourrez pas compter sur l’interface IJSRuntime.
Au passage, vous ne pourrez pas non plus compter sur certaines méthodes du cycle de vie de Blazor, comme OnAfterRender (et ce serait bien que Visual Studio l’indique). 

Pour exécuter du JavaScript dans un composant particulier en SSR statique, il faut prendre en compte la configuration initiale du projet et comprendre l’impact de l’enhanced navigation.

L’enhanced navigation (ou navigation améliorée) s’active lorsque vous naviguez entre des pages rendues de manière statique, sans aucune interactivité. Comment ça fonctionne ? Plutôt que de recharger toute la page (layout, CSS…), Blazor va récupérer les modifications du contenu principal via un endpoint interne, puis n’appliquera au DOM que ce qui est nécessaire (il n’y aura donc pas de rechargement complet).

Le gros avantage de cette méthode, c’est de donner un feeling SPA : le changement de page est rapide et fluide, sans avoir besoin d’activer un pont WebSocket ou une quelconque interactivité. L’inconvénient, c’est qu’un script lié à un composant ne se réexécutera pas à chaque navigation. Gênant, quand on doit travailler avec du javascript.

Il est possible de désactiver l’enhanced navigation en ajoutant l’attribut enhance-navigation="false" sur le composant <Router />. C’est la garantie d’obtenir un rechargement complet de la page. Dans ce cas, vous pouvez vous permettre d’appeler du JS via une simple balise <script />.

À noter que la navigation améliorée ne s’active que si votre projet est de type interactivity server. La présence du script blazor.web.js dans App.razor est un bon indicateur pour savoir si vous êtes dans ce cas de figure.

Mais il existe aussi un moyen de conserver le comportement de navigation améliorée (moyennant un peu d’huile de coude). D’après la documentation Microsoft, il faut passer par la création d'un composant PageScript.razor dédié au sein d’une Razor Class Library, qui aura pour rôle de charger du javascript dans ce contexte.

 

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

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

PageScript.razor

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);
}

BlazorPageScript.lib.module.js (a placer dans wwwroot de la RCL)

 

Il est maintenant possible d’ajouter aux composants Razor de votre application des scripts JS scoped, qui utiliseront des méthodes JavaScript correspondant au cycle de vie de l’enhanced navigation (OnLoad, OnUpdate, OnDispose).

export function onLoad() {
    alert("un script exécuté avec l'enhanced navigation");
}

Weather.razor.js

 

Avec cette méthode, on conserve un rendu statique, une navigation améliorée ET un interaction Javascript. N’hésitez pas à consulter mon projet de démo pour en savoir plus sur la mise en place.

Si vous souhaitez une solution moins manuelle, Mackinnon Buck, ingénieur chez Microsoft, propose un excellent package du nom de BlazorJSComponents, qui couvre ce genre de scénario (en allant beaucoup plus loin sur le support des modes de rendu).