Migración de AngularJS a Angular con una aplicación híbrida

En enero de 2022, la última versión de AngularJS dejó de recibir soporte. No habrá más actualizaciones oficiales ni parches de seguridad para AngularJS en el futuro.

Sin embargo, muchas organizaciones grandes que invirtieron en AngularJS desde el principio todavía dependen de él. ¿Cómo podemos ayudar a estas organizaciones a gestionar la transición de AngularJS a Angular?

Primero, una causa común de confusión: AngularJS y Angular son dos cosas diferentes. Angular a veces también se llama Angular 2+ en resumen, en algún momento de la historia de AngularJS, se decidió rediseñar todo en un nuevo marco que mantuviera el mismo nombre, pero dejó de lado el "JS", conocido como Angular. No hay una ruta de actualización entre ellos. La única forma de pasar de uno a otro es migrar.

¿Por qué no (otro framework de UI)?

Dado que podrías preguntarte, ¿por qué migrar en absoluto? ¿Por qué no volver a implementar todo en Vue o Svelte u otro nuevo y emocionante marco? La razón principal es que Angular ofrece características para trabajar junto a AngularJS que facilitan mucho la migración de uno a otro. Específicamente, te permite ejecutar una "aplicación híbrida" en la que tanto Angular como AngularJS se ejecutan lado a lado durante la migración.

Esto te permite reemplazar una pequeña parte a la vez. Por otro lado, si utilizas un marco completamente diferente, es probable que no puedas usarlos de manera limpia en la misma página, por lo que es posible que debas migrar toda la aplicación de una sola vez.

Ahora, si tienes una aplicación muy pequeña y sencilla, puede que no sea una mala elección cambiar a otro marco. Pero tratar de reemplazar cualquier aplicación lo suficientemente compleja o grande en un cambio "todo o nada" nunca funcionará en realidad. Para cambiar de manera realista estas aplicaciones, debes "cambiar las ruedas mientras el vehículo está en movimiento".

Es decir, para evitar que la aplicación se rompa para sus usuarios, debes mantener la aplicación en un estado operativo en todo momento mientras reemplazas iterativamente numerosas partes de ella. La otra razón es que Angular utiliza conceptos y sintaxis similares a AngularJS, por lo que a tus desarrolladores de aplicaciones les resultará más fácil aprender que adoptar algo completamente nuevo.

Recursos

Desafortunadamente, aunque es muy posible y las características existen en Angular para admitirlas, no hay muchos recursos sobre cómo crear realmente una aplicación híbrida. El mejor recurso sobre este tema es "Upgrading Angular Applications" de Victor Savkin, que es miembro del equipo de Angular y ha estado muy activo en este tema. En mi proyecto, mantuvimos este libro a mano en todo momento como guía. El libro de Savkin entra en muchos detalles sobre el lado conceptual y incluye algunos ejemplos de código, pero también está la guía oficial de actualización de Angular, que es un poco ligera en cuanto al aspecto conceptual pero incluye muchos detalles técnicos.

En el mejor de los casos, he encontrado que la documentación es muy útil pero no lo suficientemente completa. En este punto, puedes optar por leer la documentación que he enlazado y ver a dónde te lleva, pero lo que sigue a partir de aquí serán mis lecciones aprendidas después de haber seguido el camino que estás a punto de tomar. Espero que te ayude.

Angular y AngularJS lado a lado

Cada aplicación de AngularJS o Angular se puede pensar como un árbol, así:tree-angularjs

En este caso, cada uno de esos nodos azules representa alguna unidad de código de AngularJS, ya sea un módulo o una directiva, entre otros. El nodo raíz debe ser un módulo, que es tu módulo de la aplicación que contiene toda la aplicación. Desde allí, podrías imaginar que la segunda fila son directivas que representan páginas completas, y la siguiente fila son directivas que componen pequeñas partes de esas páginas.

Lo que queremos lograr, en resumen, es esto:

Legend AngularJS Module, Directive, or Component or Angular component

Si pudiéramos hacer esto, podríamos reemplazar una pequeña directiva de todo este árbol con un componente de Angular. ¡Eso sería asombroso! Si continuara funcionando como antes, podríamos combinar y desplegar nuestros cambios sin que los usuarios noten ningún cambio. Esto nos permitiría seguir una integración continua real: hacer cambios pequeños que se fusionan y despliegan constantemente, siempre manteniéndonos al día con la rama principal. De esta manera, cada pequeña pieza se prueba y se mueve a producción poco a poco, lo que significa muchos despliegues pequeños y sin problemas en lugar de uno grande y aterrador si tuvieras que migrar muchos o todos los nodos de una sola vez. Además, realmente ayuda a reducir conflictos de control de versiones y mejora la cohesión del equipo en general.

ng-upgrade

¿Pero es posible? Sí, gracias a ng-upgrade. ng-upgrade es una biblioteca que Angular proporciona para agregar una capa de compatibilidad entre Angular y AngularJS. Te permite actualizar directivas de AngularJS a Angular y degradar componentes de Angular a directivas de AngularJS, entre otros servicios.

Lo hace envolviendo un objeto de una API en un objeto de envoltura que traduce entre su API y la otra. Por ejemplo, para actualizar un objeto de AngularJS, envuelve un objeto alrededor de él que traduce sus salidas en una API que Angular comprende y traduce sus entradas de Angular a un formato que AngularJS comprende. En términos de nuestro diagrama, se ve así:

ngupgrade

Usando esto, podemos integrar nuestros nuevos componentes de Angular con la antigua aplicación de AngularJS, así:

tree-hybrid-2

La forma en que hacemos esto en el código es muy simple. A continuación, se muestra un ejemplo. Ten en cuenta que angular.module es una función regular de AngularJS (necesitarás los tipos para hacer referencia a ellos desde TypeScript) y solo el valor interno que estamos colocando allí es diferente, porque estamos proporcionando un componente de Angular degradado usando la función downgradeComponent:


angular.module('myModule', [])
  .directive('myDirective', downgradeComponent({
    component: MyComponent
  }))
 

¡Esto es realmente genial! A partir de aquí, puedes mover un nodo a la vez, desde abajo hacia arriba, hasta que toda la aplicación se migre, y mantener la aplicación funcionando y desplegable todo el tiempo. Pero no es una solución completa. De hecho, presenta problemas considerables. Imagina que sigues este enfoque y migras todos los nodos finales de este árbol, que pueden tener docenas, cientos o incluso miles de nodos en amplitud, ¿qué pasa entonces? Terminarás con un árbol que se ve un poco así:

tree-hybrid-3

En este escenario, has estado trabajando en esta migración durante al menos meses, tal vez años. Sin embargo, no tienes páginas de Angular completamente nuevas porque todo depende del módulo raíz de AngularJS. Y si introduces nuevas funciones, tendrán que incluir este envoltorio de AngularJS, incluso si las escribes en Angular. Además, ahora tienes un gran trabajo por delante: eliminar el módulo raíz de AngularJS y reemplazarlo por uno de Angular. Este es un cambio enorme: cada nodo degradado ahora debe tener su degradación eliminada, al igual que cualquier servicio de AngularJS que se haya actualizado.

Esto no solo es un cambio importante para hacer de una sola vez, sino que también es arriesgado: afecta a cada página en toda la aplicación, y cualquiera de ellas podría romperse en algún caso raro e imprevisto. Por lo tanto, la cantidad de trabajo de prueba es mucho mayor que la cantidad de trabajo de desarrollo.

Todo el tiempo estuviste creando una bomba de tiempo. ¿Cómo podemos evitar este problema de la "bomba de tiempo"? Savkin introduce lo que llama la Estrategia Shell para ayudar en este sentido.

La Estrategia Shell

En resumen, la estrategia shell consiste en: Envolver el módulo raíz de AngularJS en un módulo de Angular (usando ng-upgrade para que el módulo de AngularJS sea interoperable con el nuevo módulo de Angular), lo que hace que toda la aplicación de AngularJS pertenezca a una aplicación de Angular.

tree-shell-strategy

En la práctica, esto se hace eliminando tus llamadas originales .bootstrap o ng-bootstrap para AngularJS y, en su lugar, inicializar AngularJS desde el inicio de Angular, utilizando la función upgrade.bootstrap de ng-upgrade que toma los mismos parámetros que el original:


@NgModule({
  …
})

class AppModule implements DoBootstrap {
  private readonly upgrade: UpgradeModule

  constructor(upgrade: UpgradeModule) {
    this.upgrade = upgrade
  }

  ngDoBootstrap(appRef: ApplicationRef) {
    appRef.bootstrap(AppComponent)
    this.upgrade.bootstrap(document.getElementById('root-template-element') as Element, ['my-dependencies-here'])
  }

}
 

Esto tiene una ventaja inmediata y obvia: ahora puedes agregar rutas y páginas completamente nuevas en Angular sin ningún AngularJS, de esta manera:

tree-shell-strategy-2

De esta forma, puedes agregar nuevas funciones completas en Angular sin ningún AngularJS. Pero si lleváramos esta estrategia de arriba hacia abajo a su conclusión lógica, nos obligaría a aplicar el envoltorio ng-upgrade a todos los nodos de AngularJS de nivel superior, lo que no es factible. Hay un par de estrategias diferentes que Savkin describe en su libro, pero optamos por una estrategia de abajo hacia arriba junto con la estrategia de la carcasa. De esta manera, podemos migrar incrementalmente una parte del árbol a la vez hasta que una rama completa se pueda trasladar a Angular puro:

tree-shell-strategy-3

Seguimos un patrón de "abajo hacia arriba", es decir, migrar primero un nodo final del árbol, porque es el lugar más sencillo para comenzar. Si migras un nodo en medio de tu árbol, debes usar actualizaciones y degradaciones para gestionar las relaciones que ese nodo tiene por encima y por debajo, pero con el nodo final solo necesitas actualizar ese nodo y luego puede comunicarse con sus padres.

De esta manera, puedes hacer crecer la aplicación Angular de manera incremental mientras reduces la aplicación AngularJS. ¡Y terminamos con toda una rama en Angular puro! En lugar de terminar con muchas páginas parcialmente convertidas e incompletas, has completado algunas partes sencillas y autosuficientes sin romper el sitio hasta que se haya convertido completamente una sección completa. Este es un enfoque realmente convincente para un interesado en el proyecto.

Sugiero que hagas un seguimiento de cuántos controladores de AngularJS hay y cuántos componentes de Angular y los gráficos con el tiempo, esto te proporciona una métrica clara que puedes informar que demuestra el ritmo y el progreso continuo de la migración.

Enrutamiento

Recuerda que tanto en AngularJS como en Angular, puedes enrutar tus aplicaciones proporcionando un elemento en tu plantilla raíz que se reemplaza por el contenido de la página a la que se enruta.

En cuanto a cómo se ve esto en la plantilla HTML raíz de una aplicación híbrida, querrás tener las raíces de tus aplicaciones de Angular y AngularJS una al lado de la otra de esta manera:

<section>
<div ng-view></div>
<app-root></app-root>
</section>
 

Las páginas a las que tu aplicación de AngularJS enruta se mostrarán debajo de tu elemento ng-view y las páginas a las que enruta tu aplicación Angular se mostrarán debajo de tu elemento app-root (o reemplaza app-root con el selector que especifiques en tu componente de la aplicación, generalmente llamado app.component.ts).

Pero podrías pensar, ¿seguro que esto muestra ambas aplicaciones en la misma página? Bueno, sí, lo hace. Y eso abre un nuevo dilema que debes considerar.

Puedes asegurarte de que solo se muestre una página teniendo que mostrar la otra aplicación en una página en blanco. De esta manera:

routing

Pero existen algunos posibles errores: ¿Qué pasa si ambas aplicaciones no controlan una ruta y, por lo tanto, no muestran nada? ¿Qué pasa si ambas creen que controlan la ruta y, por lo tanto, ambas aplicaciones muestran una página al mismo tiempo?

El primer problema se resuelve fácilmente: asegúrate de que solo una de las aplicaciones tenga una página de error 404. Luego, si el usuario se dirige a una página que ninguna de ellas controla, una de ellas mostrará una página de error 404 y la otra nada. Solo asegúrate de que una aplicación sea la responsable de las páginas de error 404.

En cuanto al segundo problema, es un poco más difícil. Si migras una página, existe el riesgo de que la página antigua no se elimine correctamente y ambas aplicaciones puedan mostrar algo al mismo tiempo, especialmente si hay alguna lógica adicional no trivial en tu enrutamiento. En la mayoría de las aplicaciones de tamaño moderado, probablemente sea suficiente confiar en que los desarrolladores detecten estos errores manualmente. En nuestro caso, que era bastante grande y complejo, ideamos una solución más completa: especificar todas tus rutas de Angular en un archivo JSON que la nueva aplicación expone y leerlo (una vez al inicio) desde la aplicación de AngularJS. Luego, modificamos el enrutador de AngularJS para arrojar un error si se le indica que agregue una ruta que sabemos está controlada por AngularJS. Usando este método, también pudimos agregar banderas de funciones a rutas individuales.

Sincronización de rutas

El enrutamiento fue quizás el problema más incómodo y complicado de la aplicación híbrida en la que mi equipo y yo trabajamos. Hubo algunos errores particulares que causaron muchos problemas: bucles recursivos en los que AngularJS provoca un evento de enrutamiento en Angular, que a su vez provoca un evento en Angular, que a su vez provoca un evento en AngularJS, ad infinitum. A menudo esto ocurría porque cada uno de ellos tenía una opinión diferente sobre el nombre correcto de la ruta, lo que en su mayoría se reduce a diferencias en el comportamiento en ciertos casos extremos con respecto a cómo Angular y AngularJS analizan las URL. Cuando se trata de esto, tuvimos que escribir nuestro propio UrlSerializer para configurar cómo Angular serializa y deserializa las URL. Si lee las direcciones de AngularJS correctamente e imprime las que AngularJS puede leer, funcionará bien.

El otro problema era lo opuesto: a veces, cuando AngularJS desencadenaba un evento de enrutamiento, Angular no lo reconocía. Esto resultaría en estados incorrectos en los que ambas aplicaciones renderan una página al mismo tiempo porque Angular no ha recibido un evento de enrutamiento y no sabe que debe mostrar nada, pero AngularJS es consciente del evento y, por lo tanto, está mostrando algo.

La solución a esto es simple: ng-upgrade proporciona una herramienta llamada setupLocationSync que asegura que todos los eventos de enrutamiento de AngularJS se igualen en Angular. Todo lo que tienes que hacer es llamar a la función durante la inicialización de Angular. Para la mayoría de las personas, esto será suficiente, pero si estás utilizando direcciones de fragmento (es decir, tus URL tienen un # en ellas antes de la dirección de la página de front-end, como my.website.com/index.jsp#/about-us), entonces tendrás otra complicación. Tu suerte puede variar, pero descubrimos que setUpLocationSync no funciona para este tipo de dirección de forma predeterminada. Afortunadamente, es una función bastante simple, así que la copiamos de la fuente, escribimos algunas pruebas a su alrededor y producimos una versión personalizada que funciona con las URL de fragmento.

Actualización y degradación de servicios

Un problema con el que es probable que te encuentres es compartir servicios entre ambas aplicaciones. Tienes páginas en AngularJS que deseas migrar a Angular, pero utilizan servicios que son compartidos por otras páginas de AngularJS. En este caso, tienes tres enfoques posibles:

  1. Reimplementa el servicio en Angular, manteniendo dos copias del mismo servicio.
  2. Reimplementa el servicio en Angular, elimina la versión de AngularJS y utiliza un envoltorio de degradación para que la versión de Angular sea utilizable en el código de AngularJS.
  3. Agrega un envoltorio de actualización al servicio de AngularJS que lo hace disponible en Angular. Después de migrar todas las páginas que lo usan, el servicio en sí se puede migrar y el envoltorio se elimina.

Todas estas opciones son viables y depende de los detalles del servicio en particular cuál es la mejor elección. Honestamente, aunque puede que no sea tu primera opción, la opción 1 no es mala en absoluto. Sí, estás creando duplicación, pero siempre y cuando todos estén de acuerdo en que estamos trabajando exclusivamente en la versión de Angular a partir de ahora, no es un gran problema. Sin embargo, no funciona si tu servicio tiene un estado; no intentes hacer algo tonto para sincronizarlos, realmente no vale la pena, simplemente elige la opción 2 o 3. La opción 1 tampoco es una buena elección cuando un servicio se usa en muchos lugares y es probable que se modifique. Si es probable que lo cambies y es poco probable que puedas completar su migración antes de eso, simplemente sigue las opciones 2 o 3.

Para elegir entre las opciones 2 o 3, debes decidir si deseas adelantar o atrasar el esfuerzo: ¿migrar el servicio y agregar muchos envoltorios degradados a muchas páginas de AngularJS en este momento que puedes eliminar más tarde? ¿O ahorrarte eso agregando un envoltorio de actualización de AngularJS cada vez que migras un componente o una página, pero con el conocimiento de que eventualmente tendrás que realizar esa migración y eliminar todos los envoltorios? Yo tendería hacia atrasar el esfuerzo: simplifica tu trabajo inmediato, sigue la implementación más simple que se te ocurra y pon una tarea para seguir migrando ese servicio en tu lista de tareas.

En realidad, hacer que un servicio de Angular esté disponible en AngularJS es, al igual que degradar un componente, muy sencillo:

angular.module('myModule', [])
    .factory('myService', downgradeInjectable(myService))
 

La actualización de un servicio de AngularJS es un poco más complicada porque debes agregar tipos para el servicio relevante en TypeScript, pero aún es bastante fácil. Agregas el servicio al conjunto de proveedores de uno de tus módulos, en la anotación @NgModule. De esta manera:

providers: [
  …
  {
    deps: ['$injector'],
    provide: 'my-service-name',
    useFactory: (injector: Injector) => {
      return injector.get(serviceName)
    }
  }
]
 

El Injector es capaz de buscar el servicio de AngularJS y, al pasarlo a los proveedores a través de useFactory, lo pones disponible para su uso general en tu aplicación de Angular. Sin embargo, mi recomendación sería no utilizar este servicio directamente en ningún componente. En su lugar, crea un nuevo servicio de Angular que envuelva a este, proporcione una API similar y delegue el comportamiento real al servicio de AngularJS.

De esta manera, cuando llegue el momento de migrar ese servicio, tendrás un trabajo mucho más sencillo para desenredar la versión de AngularJS, ya que solo se usa en un lugar, no en muchos, y simplemente puedes implementar la misma lógica en tu servicio de Angular utilizando la API que definiste. Puedes inyectar el servicio subyacente que especificaste en tu nuevo servicio envoltorio en el constructor de esta manera:

public constructor(
    @Inject("my-service-name") private myInnerService: MyInnerService
) { }
 

Nota que el argumento que pasas a @Inject debe ser exactamente el mismo que la cadena (conocida como token) que pasas al argumento de provide al definir el proveedor anteriormente.

Entrenamiento

Los desarrolladores que estaban acostumbrados a AngularJS necesitarán un poco de formación para ponerse al día. Ahora tendrán que escribir en TypeScript, que es un lenguaje bastante diferente, y Angular en sí tiene diferencias significativas también. Debes liderar estos esfuerzos para asegurarte de que los desarrolladores tengan todo el apoyo que necesitan.

Cursos

Para TypeScript, te recomiendo "Introducción a TypeScript" de TotalTypescript. Es una serie de tutoriales muy prácticos en los que creas una aplicación de hoja de cálculo simple. Será una buena práctica para que los desarrolladores experimenten con TypeScript y lo dominen lo antes posible.

Para Angular, el curso oficial "Tour of Heroes" es un excelente punto de partida. Es un tutorial paso a paso que guía a los desarrolladores a través de la creación de una aplicación de ejemplo en Angular. También, puedes encontrar muchos otros recursos en línea, como tutoriales, videos y cursos en plataformas de aprendizaje en línea como Udemy, Coursera y Pluralsight.

Soporte continuo

Una vez que hayas migrado tu aplicación de AngularJS a Angular, es importante continuar brindando soporte y mantenimiento. Asegúrate de que tu equipo esté al tanto de las actualizaciones de Angular y realice un seguimiento de las mejores prácticas de desarrollo para mantener tu aplicación en buen estado y segura.

Pruebas y depuración

Las pruebas son esenciales durante todo el proceso de migración. Asegúrate de que tus pruebas existentes en AngularJS sigan siendo válidas y desarrolla nuevas pruebas para las partes de Angular. Las pruebas automatizadas te ayudarán a identificar y solucionar problemas de manera más eficiente a medida que migres componentes y funciones. Además, utiliza herramientas de depuración, como las herramientas de desarrollador de los navegadores y las herramientas de depuración de Angular, para resolver problemas y encontrar errores en tu aplicación.

Optimización del rendimiento

A medida que migres a Angular, es una oportunidad para optimizar el rendimiento de tu aplicación. Asegúrate de seguir las mejores prácticas de Angular para mejorar la eficiencia y la velocidad de tu aplicación. Esto puede incluir la implementación de técnicas de carga diferida, optimización de detección de cambios y eliminación de código muerto.

Migración exitosa

La migración de AngularJS a Angular con una aplicación híbrida es un proceso desafiante, pero con la planificación adecuada y la estrategia correcta, puedes lograr una transición exitosa. Aprovecha las herramientas y recursos proporcionados por Angular y ng-upgrade para facilitar la migración paso a paso. Asegúrate de brindar capacitación y soporte continuo a tu equipo y de mantener tu aplicación actualizada y optimizada para un rendimiento óptimo.

Recuerda que la migración es un proceso continuo y evolutivo. A medida que evoluciona Angular y tu aplicación, es importante estar al tanto de las nuevas características y mejores prácticas para mantener tu código actualizado y eficiente. Con paciencia y un enfoque paso a paso, puedes migrar con éxito de AngularJS a Angular y aprovechar todas las ventajas que ofrece el último marco de desarrollo web. ¡Buena suerte en tu proceso de migración!

New call-to-action