App shell
Le terme app shell désigne une approche architecturale de développement d’applications web qui sépare l’infrastructure applicative et l’interface de base des données. L’app shell contient généralement le HTML, le CSS et le JavaScript minimaux nécessaires pour afficher l’interface de l’application.
Dans O3, l’app shell correspond au code du paquet esm-app-shell. Il gère tout ce qui se passe entre le moment où vous demandez une page et le moment juste avant que quelque chose soit rendu dans l’interface. Le premier point d’entrée de l’app shell est le fichier index.ejs. Ce fichier est le point d’entrée de l’application et se charge de rendre l’app shell. O3 est une application monopage construite autour d’un seul fichier HTML généré depuis le template index.ejs. Ce template est principalement du HTML statique avec quelques valeurs dynamiques interpolées au moment du build. Nous y passons notamment:
- La locale par défaut.
- Le titre de la page.
- Le favicon de l’application.
- Une balise
<link>qui référence l’import map. - Une balise
<script>pour l’import map. - Une balise
<link>qui précharge le registre des routes, c’est-à-dire les métadonnéesroutes.jsonagrégées depuis les modules frontend. - Une balise
<script>pour le registre des routes.
Le template contient aussi une référence au fichier JavaScript principal généré par le bundle Rspack de l’app shell (openmrs.js en développement et un fichier openmrs.[contenthash].js en production). Ce bundle crée une fonction appelée initializeSpa et l’ajoute à l’objet window, ce qui la rend disponible dans le scope global. Cette fonction est appelée depuis le template index.ejs et démarre l’application. Le template contient aussi:
- Des
<div>dans lesquels sont rendus les conteneurs de modales, notifications inline, notifications actionnables, snackbars et toasts. - Un indicateur de chargement affiché pendant le démarrage de l’app shell, avant que single-spa ne commence le routage.
- Un état d’erreur affiché quand l’application n’arrive pas à se charger.
initializeSpa
La fonction initializeSpa:
- Met en place des fonctions utilitaires, comme
copyTextpour les messages d’erreur. - Configure des chemins et variables globaux sur l’objet
window, par exempleopenmrsBase,spaBase,spaEnv,spaVersionetgetOpenmrsSpaBase. - Branche le chemin de base de la SPA en créant un élément
<base>dans leheaddu document. - Initialise le shared scope Module Federation utilisé par les modules frontend.
- Initialise le mécanisme de chargement des modules en invoquant la fonction
run.
run()
La fonction run orchestre toute la séquence d’initialisation. Elle est responsable de:
- Afficher l’indicateur de chargement. Il reste visible pendant le démarrage de l’app shell et disparaît après que
runShell()a démarré single-spa. Le code des modules frontend reste chargé paresseusement quand des pages, extensions, modales ou workspaces sont rendus. - Mettre en place les breakpoints d’interface pour les viewports
tablet,small-desktopetlarge-desktop. - Brancher les souscriptions de l’app shell pour les toasts, notifications inline, notifications actionnables, snackbars et modales.
- Appeler
setupApiModulepour initialiser le schéma de configuration. - Appeler
setupHistorypour mettre en place le routage côté client. - Appeler
registerCoreExtensions, un hook historique pour les extensions possédées par l’app shell. L’app shell actuel n’y enregistre pas les fils d’Ariane. - Appeler
setupCoreConfigpour initialiser la configuration core. - Appeler
setupApps, qui lit les route maps et enregistre les métadonnées des modules sans encore importer leur code. - Une fois les modules enregistrés, appeler
finishRegisteringAllAppspour finaliser le processus d’enregistrement. - Si le mode hors ligne est activé, mettre en place les classes CSS hors ligne et les handlers de connectivité avant de fournir la configuration. Après que
runShell()a démarré single-spa et que l’indicateur de chargement est fermé, enregistrer le service worker et activer le support hors ligne. - Charger la configuration depuis les
configUrlsfournis. - Appeler
runShell, qui démarre single-spa et initialise l’internationalisation. - Gérer les échecs d’initialisation avec
handleInitFailuresi quelque chose se passe mal. - Nettoyer les feature flags obsolètes.
setupApps()
La fonction setupApps charge toutes les routes depuis le registre de routes. Elle lit les routes depuis:
- Les balises
<script type="openmrs-routes">dans le HTML, sous forme de JSON inline ou depuis une URL. - Les surcharges de routes stockées dans
localStorage, utiles pour le développement et le débogage.
Après avoir chargé toutes les routes, elle invoque registerApp() sur chaque module et sa définition de routes. Les définitions de routes restent disponibles sur window.installedModules, ce qui permet aux outils d’inspecter les versions des modules et les métadonnées de dépendances backend. registerApp():
- Enregistre un schéma de configuration implicite pour l’application.
- Parcourt les extensions et les enregistre dans le registre des extensions.
- Parcourt les modales, workspaces, groupes de workspaces, fenêtres de workspace et feature flags, puis les enregistre.
- Ajoute les pages à un tableau global. Une fois tous les modules enregistrés,
finishRegisteringAllAppstrie les pages alphabétiquement par nom d’app, puis crée un élément<div>dans le DOM pour chaque page. Ce div est créé dans le conteneur indiqué par la propriétécontainerDomIdde la page, avecomrs-apps-containercomme valeur par défaut. Cela garantit que single-spa dispose d’un élément DOM prédéterminé dans lequel monter chaque page, ce qui est nécessaire au bon fonctionnement du routage.
Les pages et extensions sont implémentées comme objets single-spa. Ce sont essentiellement des objets JavaScript qui définissent trois fonctions de cycle de vie:
bootstrap- appelée une seule fois lorsque l’application est chargée pour la première fois. Elle charge les dépendances dont l’application a besoin.mount- appelée quand la page est chargée. Elle rend la page. En pratique, cela appelle généralementReactDOM.createRoot()et rend l’arbre React dans l’élément DOM qui correspond à la page.unmount- appelée quand la page est déchargée. Elle nettoie les ressources utilisées par la page.
Les pages et extensions sont chargées avec la fonction loadLifeCycles(), qui prend le nom de l’app et le nom du composant, puis retourne une Promise résolue en objet de lifecycle single-spa. Si vous regardez le point d’entrée d’un module frontend (src/index.ts), vous verrez des exports nommés qui invoquent soit getAsyncLifecycle, soit getSyncLifecycle. Ces fonctions permettent d’emballer des pages et extensions dans un format chargeable par single-spa. Le composant React est essentiellement enveloppé dans un openmrsComponentDecorator qui permet au framework de:
- Gérer les erreurs si le rendu échoue de façon catastrophique.
- Brancher la prise en charge de la configuration pour le composant.
- Brancher la prise en charge de
i18npour le composant. - Brancher un fallback Suspense pour le composant, avec
nullpar défaut. - Rendre le composant.
Quand vous définissez une page, sa définition peut avoir une propriété route ou routeRegex. Single-spa utilise l’emplacement référencé par route ou routeRegex pour déterminer quelles pages doivent être rendues à cet emplacement via la fonction getActivityFn. Dans O3, les pages sont essentiellement des applications single-spa avec une balise <div> prédéterminée dans laquelle elles sont rendues. Les extensions s’appuient sur le concept de parcel de single-spa. Elles ont exactement les mêmes lifecycles que les applications single-spa, mais elles n’ont pas de getActivityFn. Cela signifie que single-spa ne montera ou démontera jamais automatiquement un parcel. Il faut lui dire manuellement quand le monter ou le démonter. Le système d’extensions existe pour déterminer quand une extension doit être chargée, puis invoquer sa fonction mount, et quand elle doit être déchargée, puis invoquer sa fonction unmount. Chaque extension est montée via le composant Extension défini dans le framework. Ce composant rend l’extension en invoquant la fonction mount de single-spa, puis démonte le parcel quand le composant est démonté. Il définit un <div> avec une propriété data-extension-id dans lequel React rend le parcel. L’important à retenir est que, une fois les <div> créés pour les pages et extensions, React prend le relais et rend la page ou l’extension dans le DOM.
Chargement des modules
O3 charge les modules frontend avec des import maps et Module Federation. L’app shell inclut une ou plusieurs balises
script systemjs-importmap dans index.ejs, mais le runtime actuel lit ces balises lui-même avec le package de
chargement dynamique. SystemJS n’est plus l’élément qui importe le code des modules au runtime.
Pour chaque URL de module, l’app shell ajoute une balise <script>. Quand le script se charge, il expose un conteneur
Module Federation sur window, avec le nom du module transformé en identifiant JavaScript. L’app shell appelle ensuite
les fonctions init et get du conteneur pour charger l’export ./start du module.
Quand une page, une extension, une modale ou un workspace est sur le point d’être rendu, loadLifeCycles() importe le
module frontend propriétaire, exécute une seule fois startupApp() si le module l’exporte, marque le module comme chargé
dans le système de configuration, puis appelle l’export de lifecycle nommé dans routes.json.
Séquence d’initialisation
La séquence complète d’initialisation suit cet ordre:
- Rendu du template: le template
index.ejsest rendu avec les valeurs dynamiques injectées au moment du build. - initializeSpa: configure les variables et chemins globaux, puis appelle
run(). - run(): orchestre l’initialisation:
- Met en place l’infrastructure UI, dont breakpoints, notifications et modales.
- Initialise le module API et le schéma de configuration.
- Met en place l’historique de routage.
- Exécute le hook historique d’enregistrement des extensions core.
- Charge les route maps et enregistre les métadonnées des modules via
setupApps(). - Finalise l’enregistrement des modules.
- Met en place les classes CSS hors ligne et les handlers de connectivité si le mode hors ligne est activé.
- Charge les fichiers de configuration.
- Démarre le routage single-spa via
runShell(). - Gère toute erreur d’initialisation.
- Ferme l’indicateur de chargement initial.
- Enregistre le service worker et active le support hors ligne si le mode hors ligne est activé.
- Déclenche l’événement
started, qui lance des tâches de suivi comme le nettoyage des feature flags obsolètes.
- runShell(): finalise le démarrage:
- Met en place l’internationalisation (i18n).
- Enregistre le calendrier par défaut.
- Démarre single-spa avec
start(), ce qui lance le routage et le rendu des pages.
Support hors ligne
Quand le mode hors ligne est activé via l’option de configuration offline, l’app shell:
- Enregistre un service worker (
service-worker.js) pour le cache et les fonctionnalités hors ligne. - Active ou désactive la classe
omrs-offlinesur l’élément body selon la connectivité. - Enregistre des handlers hors ligne pour les changements de connectivité.
- Active la capacité hors ligne pour la synchronisation des données.
- Met en place le précaching des dépendances statiques pour l’utilisation hors ligne.
Gestion des erreurs
Si l’initialisation échoue à n’importe quelle étape, la fonction handleInitFailure:
- Capture l’erreur et affiche le template d’erreur depuis
index.ejs. - Affiche un message d’erreur lisible par l’utilisateur avec un bouton de rechargement.
- Journalise les détails de l’erreur dans la console.
- Fournit un bouton de copie pour le message d’erreur.
Surcharges de routes
Pour le développement et le débogage, vous pouvez surcharger les routes en les stockant dans localStorage avec des clés préfixées par openmrs-routes:. Les surcharges de routes ne sont actives que lorsque window.spaEnv === "development"; les builds de production les ignorent. L’app shell charge les surcharges présentes au moment du chargement de la page et les fusionne avec les routes du registre. La valeur stockée doit être du JSON, soit:
- Un objet de routes sérialisé en JSON pour ce module.
- Une URL sérialisée en JSON pointant vers un fichier
routes.jsonà récupérer.
Cela permet aux développeurs de tester différentes versions ou configurations de modules sans reconstruire toute l’application. Par exemple:
localStorage.setItem('openmrs-routes:@openmrs/esm-my-app', JSON.stringify({
pages: [/* ... */],
extensions: [/* ... */]
}));Pour une surcharge basée sur une URL, stockez l’URL avec JSON.stringify("https://example.org/routes.json"); une URL brute ne sera pas analysée comme une surcharge valide.