Migration frontend sans interruption
Réécrire le frontend. 170 sites l’utilisent toujours. Zéro interruption.
Une migration en direct de CRA vers Next.js avec une enveloppe V2, un pont iframe, une authentification partagée, des routes synchronisées et un déploiement progressif.
5 min de lecturePar Abdullah Raheel
Le produit ne pouvait pas s’arrêter
CRA V1 desservait encore 170 sites et environ 765 utilisateurs simultanés. Nous ne pouvions pas geler les fonctionnalités, construire un remplacement à part et migrer tous les clients en une fois. La migration devait s’effectuer dans le produit en production.
Nous avons utilisé le modèle Strangler Fig : placer le nouveau système autour de l’ancien, transférer une responsabilité de production à la fois, puis supprimer la couche de compatibilité lorsque plus rien n’en dépend.
Utiliser le modèle Strangler Fig, pas un second lancement
Next.js V2 est devenu l’enveloppe de l’application. Il possédait la barre de navigation, la barre supérieure, l’authentification et l’interface globale. Les routes non encore migrées continuaient à s’exécuter dans une seule iframe CRA. Les clients voyaient donc un seul produit alors que deux environnements frontend étaient actifs.
L’iframe chargeait toujours une URL de base stable du tableau de bord. Elle restait montée lors des changements de route du parent. La remonter aurait réinitialisé l’état historique et répété l’authentification. V2 envoyait la route initiale uniquement depuis le callback onLoad de l’iframe, après l’installation du listener de messages par V1, puis n’envoyait les changements suivants que lorsque la route mappée avait réellement changé.
iframe.contentWindow?.postMessage(
{
action: 'NAVIGATE',
route: iframeRoute,
location_id: locationId,
migratedLocationIds: [...MIGRATED_LOCATION_IDS]
},
dashboardUrl
)Faire fonctionner deux frontends comme un seul
Nous avons défini explicitement les deux sens des messages. V2 envoyait la navigation, les jetons SSO, les données utilisateur et les données de site. V1 renvoyait les changements de route, les demandes de données utilisateur, les demandes de renouvellement de jeton et les actions Click2Call.
Chaque destinataire rejetait les origines inattendues et chaque expéditeur ciblait l’origine configurée de l’application au lieu d’utiliser un caractère générique. Les contrôles d’origine constituaient la première frontière, pas une validation complète de la charge utile. Les messages contenant un jeton exigeaient aussi des types de messages connus et des champs validés.
if (event.origin !== dashboardUrl) return
if (event.data?.source !== 'dispo-iframe') return
if (event.data?.type !== 'ROUTE_CHANGE') return
const route = mapV1RouteToV2(event.data.route)
if (route) router.replace(route)Une connexion devait authentifier les deux applications
L’utilisateur s’authentifiait une seule fois dans V2. Au chargement, V1 demandait le contexte utilisateur actuel. V2 renvoyait le jeton SSO avec les données utilisateur et de site. V1 stockait le jeton pour son client d’API existant et en demandait un autre au moyen du même pont après son expiration.
Un hook d’authentification proactif a été ajouté pendant le diagnostic d’une condition de concurrence au démarrage de l’iframe. Il tentait d’envoyer l’authentification avant la demande de V1, mais ajoutait un autre chemin de synchronisation et se déclenchait trop tôt. La modification suivante a supprimé ce hook et conservé une authentification réactive par requête-réponse, ce qui a laissé un seul responsable clair du moment d’envoi du jeton.
Maintenir la cohérence de la barre d’adresse et de l’historique du navigateur
V2 mappait ses routes vers des chemins V1 tels que /dashboard, /buyer-genie, /deals et /my-buyers. Le mappage inverse traitait les routes exactes et les préfixes imbriqués. Lorsqu’une navigation commençait dans V1, V2 utilisait router.replace afin que la barre d’adresse et la navigation active changent sans ajouter une deuxième entrée d’historique pour une seule action utilisateur.
L’actualisation, les liens profonds et les actions Précédent et Suivant du navigateur restaient ainsi cohérents. Une action Précédent modifiait la route parente, le parent envoyait la route enfant mappée et l’iframe déjà montée naviguait sans recharger l’enveloppe.
Déplacer clients et fonctionnalités par segments contrôlés
MIGRATED_LOCATION_IDS constituait le commutateur de déploiement progressif. Nous avons commencé par les sites internes et de préproduction, puis étendu la migration à huit sites clients. Tous les sites absents de la liste restaient sur le chemin V1 existant.
- Natifs dans V2 : Navbar, Topbar, Settings, Investor Intake, SSO et Twilio Dialer.
- Encore dans V1 au point intermédiaire consigné : Home, Genius Mode, Deals et Buyers.
- Unité de migration : une fonctionnalité et des sites clients sélectionnés, pas un lancement distinct.
Le remplacement est resté invisible pendant le déploiement
La couche de compatibilité a finalement été supprimée au lieu de devenir une architecture permanente. La bascule a supprimé le système d’iframe et les anciens clients d’API générés, déplacé les clients d’API encore utilisés sous V2, ajouté le parcours de connexion natif, puis nettoyé les routes, tests, composants partagés du composeur, protections d’authentification et structure des composants.
Pendant un temps, nous avons maintenu deux routeurs, deux modèles de session, des événements entre fenêtres, des mappages de routes et des conditions de concurrence au démarrage. Ce coût n’était acceptable que parce que chaque route migrée nous rapprochait de la suppression du pont.

