En Module Federation de Webpack facilita la carga de código compilado por separado como micro frontends. Incluso nos permite compartir bibliotecas entre ellos. Esto evita que la misma biblioteca tenga que ser cargada varias veces.
Recuerda que puedes visitar mi blog
Sin embargo, puede haber situaciones en las que varios micro frontends y el shell necesiten diferentes versiones de una biblioteca compartida. Además, estas versiones pueden no ser compatibles entre sí.
Recuerda que si tu quieres aprender más acerca de Microfrontend puedes adquirir el workshop de angular 16 y microfrontend
Para hacer frente a estos casos, Module Federation ofrece varias opciones. En este artículo, presento estas opciones desde diferentes escenarios.
Traducción en español del artículo original de Manfred Steyer "Getting Out of Version-Mismatch-Hell with Module Federation" actualizado el 06.09.2020
Muchas gracias a Tobias Koppers, fundador de webpack, por responder a varias preguntas sobre este tema y por corregir este artículo.
Nuestro Escenario
Para demostrar cómo Module Federation se ocupa de las diferentes versiones de las bibliotecas compartidas, utilizo una sencilla aplicación shell conocida en esta serie de artículos. Esta shell es capaz de cargar micro fronteds en su área de trabajo:
El micro frontend está enmarcado con la línea discontinua roja.
Para compartir las bibliotecas, tanto el shell como el micro frontend utilizan el siguiente ajuste en sus configuraciones de webpack:
new ModuleFederationPlugin({
[...],
shared: ["rxjs", "useless-lib"]
})
Si eres nuevo en Modulo Federation, puedes mas informacion en articulos anteriores.
El paquete useless-lib
es un paquete ficticio que he publicado para este ejemplo. Está disponible en las versiones 1.0.0, 1.0.1, 1.1.0, 2.0.0, 2.0.1 y 2.1.0. En el futuro, es posible que añada otras. Estas versiones nos permiten simular diferentes tipos de desajustes de versión.
Para indicar la versión instalada, useless-lib
exporta una constante de versión. Como puedes ver en la imagen de arriba, el shell y el micro frontend muestran esta constante. En la escenario mostrado, ambos utilizan la misma versión (1.0.0), y por tanto pueden compartirla. Por lo tanto, useless-lib
sólo se carga una vez.
Sin embargo, en las siguientes secciones, examinaremos lo que ocurre si hay cambios de versión entre la useless-lib
utilizada en el shell y la utilizada en el micro frontend. Esto también me permite explicar diferentes conceptos que Module Federation implementa para lidiar con tales situaciones.
Usando Versionado Semántico Por Defecto
Para nuestra primera variación, supongamos que nuestro package.json
apunta a las siguientes versiones:
Shell: useless-lib@^1.0.0
MFE1: useless-lib@^1.0.1
Esto nos lleva al siguiente resultado:
Module Federation decide ir con la versión 1.0.1 ya que es la versión más alta compatible con ambas aplicaciones según el versionado semántico (^1.0.0 significa, que también podemos ir con una versión menor y de parche superior).
Usando FallBack Módulos en Versiones Incompatibles
Ahora, supongamos que hemos ajustado nuestras dependencias en package.json de esta manera
Shell: useless-lib@~1.0.0
MFE1: useless-lib@1.1.0
Ambas versiones no son compatibles entre sí (~1.0.0 significa que sólo es aceptable una versión mayor del parche pero no una versión menor).
Esto nos lleva al siguiente resultado:
Esto muestra que Module Federation utiliza diferentes versiones para ambas aplicaciones. En nuestro caso, cada aplicación recurre a su propia versión, que también se denomina módulo fallback.
Diferencias en Module Federation Dinamico
Curiosamente, el comportamiento es un poco diferente cuando cargamos los micro frontends incluyendo sus puntos de entrada remotos sólo bajo demanda usando Dynamic Module Federation. La razón es que los remotos dinámicos no se conocen al inicio del programa, y por lo tanto Module Federation no puede tener en cuenta sus versiones durante su fase de inicialización.
Para explicar esta diferencia, supongamos que el shell está cargando el micro frontend dinámicamente y que tenemos las siguientes versiones:
Shell: useless-lib@^1.0.0
MFE1: useless-lib@^1.0.1
Mientras que en el caso de usar Module Federation de forma clásica (estática), ambas aplicaciones estarían de acuerdo en utilizar la versión 1.0.1 durante la fase de inicialización, aquí en el caso de la federación de módulos dinámica, el shell ni siquiera conoce el micro frontend en esta fase. Por lo tanto, sólo puede elegir su propia versión:
Si hubiera otras remotes estáticas (por ejemplo, micro frontales), el shell también podría elegir una de sus versiones según el versionado semántico, como se muestra arriba.
Desafortunadamente, cuando se carga el micro frontend dinámico, la federación de módulos no encuentra una versión ya cargada compatible con 1.0.1. Por lo tanto, el micro frontend vuelve a su propia versión 1.0.1.
Por el contrario, supongamos que el shell tiene la versión más compatible:
Shell: useless-lib@^1.1.0
MFE1: useless-lib@^1.0.1
En este caso, el micro frontend decidiría utilizar la ya cargada:
Para decirlo en pocas palabras, en general, es una buena idea asegurarse de que su shell proporcione las versiones más compatibles cuando cargue remotas dinámicas lo más tarde posible.
Sin embargo, como se discutió en el artículo de Module Federation Dinámicos, es posible cargar dinámicamente sólo el punto de entrada remoto al inicio del programa y cargar el micro frontend más tarde bajo demanda. Al dividir esto en dos procesos de carga, el comportamiento es exactamente el mismo que con la Federación de Módulos estática ("clásica"). La razón es que en este caso los metadatos de la entrada remota están disponibles lo suficientemente pronto como para ser considerados durante la negociación de las versiones.
Usar Singletons
Recurrir a otra versión no siempre es la mejor solución: El uso de más de una versión puede conducir a efectos imprevisibles cuando hablamos de bibliotecas que mantienen el estado. Esto parece ser siempre el caso de framework principal como Angular, React o Vue.
Para tales escenarios, Module Federation nos permite definir las bibliotecas como singletons para que se cargen una vez.
Si sólo hay versiones compatibles, Module Federation se decidirá por la más alta como se muestra en los ejemplos anteriores. Sin embargo, si hay diferencias entre las versiones, los singletons evitan que Module Federation se decante por otra versión de la biblioteca.
Para ello, consideremos la siguiente diferencia de versiones:
// Shell
shared: {
"rxjs": {},
"useless-lib": {
singleton: true,
}
},
Aquí utilizamos una configuración avanzada para definir los singletons. En lugar de un simple array, vamos con un objeto donde cada clave representa un paquete.
Si se utiliza una biblioteca como singleton, es muy probable que se establezca la propiedad singleton en cada configuración. Por lo tanto, también estoy ajustando la configuración de la Federación de Módulos del microfrontend en consecuencia:
// MFE1
shared: {
"rxjs": {},
"useless-lib": {
singleton: true
}
}
Para evitar la carga de varias versiones del paquete singleton, Module Federation decide cargar sólo la biblioteca más alta disponible de la que tiene conocimiento durante la fase de inicialización. En nuestro caso es la versión 2.0.0:
Sin embargo, como la versión 2.0.0 no es compatible con la versión 1.1.0 según el versionado semántico, recibimos una advertencia. Si tenemos suerte, la aplicación federada funciona aunque tengamos este desajuste. Sin embargo, si la versión 2.0.0 ha introducido breaking changes con los que nos encontramos, nuestra aplicación podría fallar.
En este último caso, podría ser beneficioso fallar rápidamente al detectar el desajuste lanzando un ejemplo. Para hacer que la Federación de Módulos se comporte de esta manera, establecemos strictVersion a true:
// MFE1
shared: {
"rxjs": {},
"useless-lib": {
singleton: true,
strictVersion: true
}
}
El resultado es el siguiente en tiempo de ejecución:
Rango de Versiones
Puede haber casos en los que sepa que una versión mayor es compatible con versiones anteriores aunque no sea necesario con respecto al versionado semántico. En estos casos, puede hacer que Module Federation acepte un rango de versiones definido.
Para explorar esta opción, supongamos una vez más el siguiente desajuste de versiones:
Shell: useless-lib@^2.0.0
MFE1: useless-lib@^1.1.0
Ahora, podemos utilizar la opción requiredVersion
para el useless-lib cuando configuremos el microfrontend:
// MFE1
shared: {
"rxjs": {},
"useless-lib": {
singleton: true,
strictVersion: true,
requiredVersion: ">=1.1.0 <3.0.0"
}
}
De acuerdo con esto, también aceptamos todo lo que tenga 2 como versión principal. Por lo tanto, podemos utilizar la versión 2.0.0 proporcionada por el shell para el micro frontend:
Conclusión
La Federación de Módulos ofrece varias opciones para tratar con diferentes versiones en las librerias. La mayoría de las veces, no es necesario hacer nada, ya que utiliza el versionado semántico para decidir la versión más compatible. Si una remota necesita una versión incompatible, vuelve a ella por defecto.
En los casos en los que necesite evitar la carga de varias versiones del mismo paquete, puede definir un paquete compartido como un singleton. En este caso, se utiliza la versión más alta conocida durante la fase de inicialización, aunque no sea compatible con todas las versiones necesarias. Si quiere evitar esto, puede hacer que Module Federation lance una excepción utilizando la opción strictVersion
.
También puede facilitar los requisitos de una versión específica definiendo un rango de versiones mediante requestedVersion. Incluso puede definir varios ámbitos para escenarios avanzados donde cada uno de ellos puede obtener su propia versión.
Hasta la próxima.