Du AngularJS plein la Vue

Cohabitation de deux apps front-end

Nico Prat
Nico Prat
Apr 27, 2020 · 6 min read

TL;DR. Grâce à une simple iframe, deux versions de notre app peuvent coexister, en ajustant les styles et en synchronisant les URL entre parent et enfant.

Démo de la solution, exemple minimaliste

Pourquoi ?

Dans la vie d’un projet front-end de long terme, il arrive généralement le moment fatidique où il est nécessaire de mettre à jour ou de changer le framework utilisé. Plusieurs options s’offrent à nous, plus ou moins radicales :

Tout jeter à la poubelle : satisfaisant dans le cas d’une app récente ou peu complexe, comme un MVP, mais rarement réalisable pour des apps d’un minimum d’envergure.

Remplacement progressif : on peut transiter brique par brique, mais il faut que les technologies puissent coexister, ce qui n’est pas forcément le cas — notamment concernant les routeurs ou encore les différents outils de build tels que Webpack. De plus il subsiste toujours le risque que l’ancienne version ne soit jamais définitivement remplacée et continue de “polluer” le projet pour des années.

Il existe évidemment une infinité de variations entre ces deux extrêmes. Dans notre cas, nous avions comme problématique de :

  • faire cohabiter notre v1 (Angular) et notre v2 (Vue)
  • continuer à apporter de la valeur à nos clients régulièrement (nouvelles fonctionnalités, correction de bugs, …)
  • créer une nouvelle version autonome sur des bases saines, sans conserver la dette technique accumulée au cours des dernières années
  • offrir une expérience cohérente, au moins visuellement
  • servir l’ancienne app complète ou embarquée selon les cas
  • garantir une compatibilité IE 11 (encore majoritaire chez nos clients)

Nous avons donc approché le problème d’une autre façon, grâce à une des briques fondamentales du web trop souvent moquée : l’iframe ! L’idée était d’embarquer l’ancienne version dans la nouvelle, puis de la maquiller suffisamment pour garder une app homogène, et enfin de la convertir page par page de façon transparente pour nos clients et indolore pour l’équipe. Ainsi on a pu :

  • éviter les conflits de styles
  • échanger des données entre les deux versions
  • être compatible tous navigateurs
  • conserver ou embarquer l’ancienne application selon les clients
  • faire évoluer la nouvelle version sans gêne

La solution se résume à 80% par un simple composant Vue (on parlera des fameux 20% restants par la suite) :

<template>
<iframe :src="iframeSrc"/>
</template>

Mais revenons en détail sur ces différents points…

Comment ?

CSS étant déjà particulièrement difficile à gérer à grande échelle, faire cohabiter ancien et nouveau code n’était pas une option. Une iframe nous permet de manière simple et parfaitement robuste d’éviter les conflits. On a seulement donné quelques coups de pinceaux, uniquement dans le cas de l’application embarquée ; pour savoir si l’app tourne dans une iframe, une simple condition suffit :

if (window.self !== window.top) {
document.querySelector('html').className += ' v2';
}

On a ensuite pu appliquer ces retouches de manière chirurgicale sans impacter la v1 qui devait pouvoir continuer sa vie chez certains clients :

html.v2 {
background: red;
}

De cette manière on a pu masquer les éléments structurants comme la navigation, le pied de page, … Et ajuster certains styles génériques pour que l’illusion soit suffisante. Je reviendrai toutefois en fin d’article sur quelques cas particuliers qui ont posé problème.

En reprenant les conventions de Vue, on peut considérer notre iframe comme un composant à part entière :

  • Une prop descendante via l’attribut src qui définit l’URL de l’iframe
  • Des évènements remontant grâce à postMessage() (MDN)

Simple et efficace, cette communication nous permet de ne pas nous soucier de l’implémentation du routeur de l’app enfant et de garantir la sécurité des messages transmis grâce au paramètre targetOrigin (MDN).

Maintenant que nos deux applications cohabitent, le but est de pouvoir naviguer de l’une à l’autre sans friction pour l’utilisateur. Deux bonnes nouvelles viennent largement nous faciliter la tâche :

  • Vue conserve le même élément HTML iframe et ne modifie que son attribut src, donc ne charge l’app enfant qu’une seule fois
  • Le routeur Angular (ui-router) gère naturellement ces changements d’URL et s’actualise de lui-même (j’imagine que c’est le cas pour tous les routeurs de frameworks front-end)

C’est beau le web, non ? Il ne nous reste qu’à créer une correspondance des URL entre les deux versions. On a opté pour une façon simple et flexible, à savoir la propriété meta des routes définies pour vue-router (docs). Par exemple :

{
path: '/new',
component: IframeAngular,
meta: {
iframe: '/old'
}
}

Du côté du composant, pour mettre à jour l’attribut src de l’iframe, il suffit de réagir à cette propriété meta, par exemple de cette façon :

computed: {
iframeSrc() {
return `${baseUrl}/#!${this.$route.meta.iframe}`
}
}

Dans l’autre sens, on écoute le message envoyé par l’app enfant pour remplacer la route de l’app parente (code simplifié). On utilise la méthode .replace() plutôt que .push() car le navigateur a déjà enregistré la navigation provenant de l’iframe (on aurait besoin de faire deux retours en arrière sinon) :

mounted() {
window.addEventListener("message", this.syncRoute);
},
destroyed() {
window.removeEventListener("message", this.syncRoute);
},
methods: {
syncRoute({ data }) {
const match = this.$router.options.routes.find(route => route.meta.iframe === data.iframeUrl);
this.$router.replace(match);
}
}

Dans notre cas on avait aussi besoin de synchroniser les paramètres des URL mais je ne le traiterai pas ici, par soucis de simplicité.

Une iframe a pour inconvénient de ne pas avoir de dimensions dynamiques, mais en utilisant ResizeObserver et à nouveau les évènements via postMessage(), on peut réagir au changement de dimensions dans l’app enfant, et ainsi informer le parent de redéfinir la hauteur de l’iframe (penser à inclure le polyfill pour ResizeObserver si nécessaire) pour éviter d’afficher des barres de défilement :

const resizeObserver = new ResizeObserver(() => {
parent.window.postMessage({ iframeHeight: document.body.offsetHeight }, "*"); // targetOrigin à spécifier
});
resizeObserver.observe(document.body);

On peut alors écouter ce message et mettre à jour une propriété data de notre composant Vue pour appliquer le style de manière dynamique :

<template>
<iframe :src="iframeSrc" :style="{ height: `${iframeHeight}px` }"/>
</template>

Cas limites (les fameux 20%)

Le dialogue à travers l’iframe via l’API postMessage() nécessite de gérer les CORS. Dans notre cas ce n’est pas un problème puisque les domaines sont les mêmes, seules les URL sont préfixées. Dans le cas contraire, il faut que l’app enfant spécifie le domaine vers lequel elle envoie son message (ou "*", mais c’est mal). Plus d’infos sur le MDN.

Certainement ce qui nous a posé le plus de problèmes et nécessité les solutions les plus… bricolées : la propriété position: fixed n’est pas supportée au sein d’une iframe et se comporte comme position: absolute. On a donc dû trouver des compromis pour nos modales, notifications, et autres élément d’interface du même genre. On a pris le parti de les afficher au milieu de l’écran grâce à de savants calculs, mais c’est clairement la partie dont on est le moins fiers !

Conclusion

En attaquant le problème sous cet angle, on ne s’attendait pas à ce que les fonctionnalités de base soient si simples à mettre en place. Malgré les 20% qui nous font encore parfois découvrir des bugs tirés par les cheveux, on a pu mettre en place une solution qui contente l’équipe produit & design, les commerciaux, et surtout l’équipe technique en quelques jours. La contrainte du support IE 11 était clairement un risque, mais cette bonne vieille iframe a fait des miracles !

nicooprat

Myself

Medium is an open platform where 170 million readers come to find insightful and dynamic thinking. Here, expert and undiscovered voices alike dive into the heart of any topic and bring new ideas to the surface. Learn more

Follow the writers, publications, and topics that matter to you, and you’ll see them on your homepage and in your inbox. Explore

If you have a story to tell, knowledge to share, or a perspective to offer — welcome home. It’s easy and free to post your thinking on any topic. Write on Medium

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store