Aller au contenu

Navigation API: la navigation côté client la plus moderne

La Navigation API transforme les Single Page Applications, améliorant performance et centralisation. Disponible dès Chrome 102. #NavigationAPI #WebDev

Navigation API

Introduction

Les SPAs, Single Page Application, sont caractérisées par une fonctionnalité essentielle: la possibilité de pouvoir changer son contenu dynamiquement en fonction des intéractions de l'utilisateur au lieu de charger des pages entières générées par un serveur.

Cette fonctionnalité a été rendue possible principalement par l'History API et par des workarounds réalisés avec la partie hash (/#hash) de l'url d'un site.

Celle-ci a été développée bien avant que les SPAs soient la norme applicative du Web, et aujourd'hui une approche totalement nouvelle a dû être créée pour répondre aux nouvelles exigences des applications.

Ainsi est née la Navigation API. Cette API a totalement été inventée, son but n'est pas simplement de corriger certains aspects de l'History API (ex: Scroll Restauration) mais de répondre à de nouvelles demandes de performance et de centralisation.

Cet article a pour objectif de vous présenter cette nouvelle API.

Attention cette API est disponible uniquement à partir de la version chrome 102 et reste encore en beta test.

A quoi ressemble cette nouvelle API ?

navigation.addEventListener('navigate', navigationEvent => {
  navigationEvent.transitionWhile(async() => {
    // Make your own login here
  });
});

Beaucoup d'informations en si peu de code.

navigation est un objet global qui vous permet d'écouter l'évènement navigate d'une application.

Cet évènement est totalement centralisé. Il sera automatiquement déclenché par des actions provoquées par l'utilisateur (comme soumettre un formulaire, cliquer sur un lien, utiliser le système de backward/forward du navigateur) ou de manière programmatique (par le code de votre site).
La plupart du temps, cet évènement vous permet de modifier le comportement par défaut de votre navigateur.
Dans le cas d'une SPA, il s'agit simplement de rester sur la page courante et de remplacer son contenu.

Deux fonctions vous permettent d'intercepter la navigation du navigateur:

  • transitionWhile
  • preventDefault

La fonction transitionWhile prend en paramètre une promesse généralement générée par une fonction asynchrone. En appelant la fonction transitionWhile le navigateur comprend que votre code va configurer le prochain état de votre site.

La fonction preventDefault permet d'annuler la navigation dans son intégralité.

Ces deux méthodes sont autorisées dans la plupart des cas. Cependant quelques petites restrictions existent.

Il est impossible de gérer la navigation via la méthode transitionWhile si votre navigation n'est pas intra-site. C'est à dire que vous naviguez vers un autre domaine que le site actuel.

Il est impossible d'annuler une navigation via la méthode preventDefault si l'utilisateur utilise les boutons de backward/forward du navigateur.

Pourquoi inventer une nouvelle API ?

L'évènement naviguer permet de centraliser la navigation d'une application. Cette centralisation était extrêmement compliquée avec les précédentes APIs, et il fallait principalement "tricker" pour réaliser ce comportement.
Ci-dessous la réalisation du routing d'une SPA avec l'History API:

function constructNextPage(event) {
  event.preventDefault();
  window.history.pushState(null, '', event.target.href);
  constructDOM();
}

const links = [...document.querySelectorAll('a[href]')];
links.forEach(link => link.addEventListener('click', constructNextPage));

Cette solution fonctionne plutôt bien. Cependant ici on ne centralise uniquement les navigations sur les liens. Il existe bien d'autres façons de naviguer et il deviendra compliqué de toutes les centraliser sans que le code base en soit grandement détérioré.

La Navigation API a pour objectif de répondre à cette problématique.

Que se passe t'il lors d'une transition ?

Quand la fonction transitionWhile est exécutée à partir du "listener" navigate, le navigateur est informé qu'il doit préparer la page pour un nouvel état ou une modification d'état. Le temps de réalisation de cette navigation est variable et est défini par la promesse passée en paramètre de cette fonction.

Il y a de nombreux avantages à désigner cette API de cette manière:

  • l'accessibilité
  • catcher les évèvements de succès et d'échec de navigation
  • catcher l'évènement d'annulation d'une navigation

Lorsque la promesse est résolue, que ce soit en succès ou en error, un évènement est déclenché.

Si la promesse est résolue avec succès, l'évènement navigatesuccess est émis. Cet évènement permet de réaliser la logique de fin de navigation.

navigation.addEventListener('navigatesuccess', event => {
  showLoading = false;
});

A l'inverse, si la promesse est résolue avec une erreur, l'évènement navigateerror est émis. Cet évènement permet de réaliser la logique d'erreur lorsque qu'une navigation échoue.

navigation.addEventListener('navigateerror', event => {
  showLoading = false;
  alert('Ohhh no an error occured');
});

L'évènement navigateerror reçoit en paramètre un évènement de type ErrorEvent qui permet de gérer l'erreur avec granularité.

Le fait de passer une promesse à la fonction transitionWhile rend la navigation asynchrone, ce qui laisse par exemple la possibilité de réaliser une seconde navigation lorsque la première est en cours.
Dans ce genre de situation, il est important d'annuler le traitement asynchrone de la précédente navigation.

L'évènement passé en paramètre de l'évènement navigate comporte une propriété signal de type AbortSignal. Pour résumer, cette propriété est un objet qui permet de stopper certains traitements asysnchrones comme les requêtes HTTP.

navigation.addEventListener('navigate', navigationEvent => {
  navigationEvent.transitionWhile(async() => {
    const todos = await (await fetch(url, {
      signal: navigateEvent.signal})).json();
    // make some work with todos
});

Pour plus d'informations sur "l'Abortable fetch" c'est par ici

Historique, state et navigation programmatique

Historique

Récupérer l'entrée courante de son historique de navigation est très simple.

const { key } = navigation.currentEntry

currentEntry est un objet possédant la propriété key. Cette propriété est très importante. La valeur de cette propriété de type string est unique et réprésente l'entrée courante de l'historique de navigation et son "slot". Cette clé ne change pas même si l'URL ou l'état change puisque le slot reste inchangé.

Au contraire, si l'utilisateur utilise le bouton précédent du navigateur et ouvre une nouvelle fois la page, la clé change et un nouveau slot est créé.

Cette propriété est importante pour les développeurs, elle permet de faire naviguer l'utilisateur facilement d'un état à un autre

const { key } = navigation.entry;
const homeButton = document.querySelector("button[name=home]");

homeButton.onclick = () => navigation.transitionTo(key)

await navigation.navigate('/todos').finished

Ainsi, si l'utisateur se trouve dans l'état todos, et décide de cliquer sur le bouton home, celui-ci sera correctement redirigé.

Récupérer l'historique de navigation se fait à l'aide de la propriété currentEntries de l'objet navigation.

State

La Navigation API offre aux développeurs une notion d'état pour une entrée courante. Cet état est stocké de manière persistente sur l'entrée courante de la navigation mais n'est pas directement exposé à l'utilisateur. Cette fonctionnalité est similaire à la fonction history.state de l'History API mais reste grandement améliorée par rapport à celle-ci.

Pour récupérer l'état d'une entrée courante, l'object navigation.currentEntry expose une fonction getState. Cette fonction renvoie une copie de votre état, ainsi les mutations apportées à votre état n'impactent pas l'état persisté sur l'entrée courante.

Pour modifier l'état persisté, une fonction updateCurrentEntry est mise à disposition à partir de l'objet global navigation

navigation.updateCurrentEntry({ state: { count: 0 } });
const { getState } = navigation.currentEntry;
const count = getState().count;
count++; // count = 1

console.log(getState()); // { count: 0 }

navigation.updateCurrentEntry({ state: { count } });
console.log(getState()); // { count: 1 }

Naviguer de manière programmatique signifie naviguer par une méthode appelée par le code.

Pour naviguer de manière programmatique, l'objet global navigation expose une fonction navigate prenant en premier paramètre une URL de navigation et en second paramètre un objet de configuration.

L'objet de configuration possède trois propriétés:

  • state: état de votre nouvelle entrée de navigation
  • history: pouvant être setté à 'replace' pour remplacer l'enrée courante dans l'historique
  • info: objet pouvant être récupéré grâce à navigateEvent.info

La fonction navigate remplace et améliore les méthodes location.assign(), pushState ou encore replaceState()

navigation.navigate('/todos', { state: { count: 0 } });

Cette fonction retourne également un objet qui contient deux promesses:

  • committed: URL de navigation a changé et une nouvelle entrée dans l'historique de navigation est disponible
  • finished: toutes les promesses de transitionWhile ont été résolues (succès ou erreur).

Il existe bien d'autres méthodes pour naviguer:

  • traverseTo
  • back
  • forward
  • reload

et ces méthodes retournent toutes le même objet.

await navigation.navigate('/todos', { state: { count: 0 } }).finished

Conclusion

Cette API est relativement récente, et est disponible à partir de la version Chrome 102, sans activer un flag.

Elle permet de centraliser notre navigation de manière efficace et offre à travers les évènements et les promesses la possibilité à l'utilisateur de customiser sa navigation de manière granulaire.

Malgré sa récente sortie, Cette API promet déjà de grandes choses, alors n'hésitez pas à la tester et faire remonter vos feedbacks.

Dernier