Desarrollo web y móvil
Migrar de AngularJS a Angular: estrategias, secuenciación y riesgos
Avanzado12 min de lecturaActualizado
Esta página se tradujo del inglés con ayuda de IA y se cotejó con el original. Leer la versión en inglés
AngularJS llegó al final de su vida útil en 2021, así que toda aplicación que lo sigue usando funciona sin parches sobre un framework que ya nadie mantiene. Esta guía compara con honestidad ambas arquitecturas, muestra cómo preparar una base de código legacy para que cualquier vía de migración salga más barata, sopesa el arranque híbrido frente a la entrega en paralelo y la reescritura, y hace corresponder cada concepto de AngularJS con su sustituto moderno.
Lo que aprenderás
- Justificar una migración en términos de riesgo, coste y mantenibilidad, no de moda
- Explicar las diferencias de arquitectura entre scopes con un bucle de digest y componentes con signals
- Preparar una base de código AngularJS para que la migración posterior sea más pequeña y segura
- Elegir entre arranque híbrido, entrega en paralelo y reescritura según tu situación
- Convertir controladores, directivas, filtros, servicios y rutas a sus equivalentes modernos
- Mantener la aplicación publicable y probada durante una transición de varios meses
Toda aplicación AngularJS que sigue en producción funciona sobre un framework que dejó de recibir parches el último día de 2021. Ese día no pasó nada espectacular —el código siguió funcionando—, pero el perfil de riesgo cambió para siempre. La próxima vulnerabilidad que se descubra en el framework, o en la larga cola de directivas abandonadas de las que dependen casi todas estas aplicaciones, no tendrá arreglo; los navegadores siguen evolucionando por debajo, y la cantera de desarrolladores dispuestos a trabajar con él se reduce cada año.
Ese es el argumento para migrar, y es un argumento de riesgo, no de estética. Importa porque las migraciones las aprueban personas que necesitan un motivo mejor que «el nuevo es más bonito». Lo que sigue es la parte práctica: en qué se diferencian de verdad las dos arquitecturas, qué hacer con la base de código legacy antes de empezar, cómo elegir una estrategia de entrega y cómo seguir sacando funcionalidades mientras dura la transición.
Dos frameworks con el mismo nombre y cimientos distintos
El nombre es desafortunado: AngularJS y Angular comparten marca y poco más. Entender dónde divergen los modelos te dice qué partes de una migración son mecánicas y cuáles exigen repensar el diseño.
| Aspecto | AngularJS | Angular |
|---|---|---|
| Unidad de interfaz | Controlador más plantilla, después .component() | Clase con un decorador, standalone por defecto |
| Contenedor de estado | $scope, con herencia prototípica | Campos de clase, normalmente signals |
| Mecanismo de actualización | Bucle de digest que reevalúa cada watcher hasta que se estabiliza | Detección de cambios selectiva, dirigida por inputs, eventos y signals |
| Flujo de datos | Binding bidireccional casi en todas partes | Unidireccional hacia abajo, eventos hacia arriba |
| Inyección de dependencias | Por nombre como cadena, con anotación necesaria para la minificación | Por tipo o token, resuelta mediante un inyector jerárquico |
| Lenguaje | JavaScript ES5, después ES6 | TypeScript, plantillas fuertemente tipadas |
| Modularidad | Registros angular.module y un espacio de nombres global | Módulos ES y el grafo de imports del compilador |
| Unidad de composición | Directivas con una función link | Componentes y directivas de atributo |
| Herramientas | Concatenación, Grunt o Gulp | CLI, compilación ahead-of-time, tree shaking |
El bucle de digest es lo que más moldea una base de código legacy. Como AngularJS no podía saber qué había cambiado, reevaluaba todos los watchers registrados una y otra vez hasta que los valores se estabilizaban, con un tope de diez pasadas. De ahí el conocido desplome de rendimiento a partir de unos pocos miles de bindings, los errores $apply already in progress cuando el código se ejecutaba sin que el framework lo supiera, y la costumbre de añadir $scope.$watch para pegar las piezas. Angular moderno le da la vuelta: sabe exactamente qué bindings dependen de qué estado, así que una actualización solo toca los componentes afectados. Cualquier lógica de tu aplicación que exista para compensar el bucle de digest se puede borrar sin más, pero antes tienes que reconocerla.
La segunda diferencia estructural es la herencia de scopes. Un scope hijo heredaba de su padre, de modo que una plantilla podía leer un valor definido tres niveles más arriba y —gracias a los prototipos de JavaScript— escribir un valor primitivo en el hijo ocultaba en silencio el del padre en lugar de actualizarlo. Esto dio lugar a toda una familia de bugs y a una regla popular («pon siempre un punto en tu ng-model»). En el nuevo modelo no hay equivalente: los datos llegan por inputs explícitos y nada es visible implícitamente desde un ancestro.
Evalúa antes de planificar
Empieza por un inventario, no por una prueba de concepto. Cuenta las pantallas y las definiciones de rutas. Enumera cada directiva, módulo y plugin de jQuery de terceros, y marca cuáles están abandonados. Mide cuánta lógica vive en los controladores frente a los servicios: la que ya está en servicios es en gran parte portable; la de los controladores, normalmente no. Localiza el estado compartido: autenticación, usuario actual, permisos, un carrito, una caché en memoria. El estado compartido es lo que determina si la aplicación antigua y la nueva pueden convivir en paralelo o tienen que entrelazarse.
Después, sé honesto con el destino. Una migración es una ocasión poco frecuente de arreglar una arquitectura, y también una ocasión poco frecuente de descarrilar un proyecto por intentar arreglarlo todo a la vez. Decide explícitamente el alcance: solo el framework, el framework más el design system, o el framework más un nuevo contrato con el backend. Cada añadido multiplica el riesgo. La mayoría de las migraciones que salen bien congelan el diseño visual y el contrato de la API, para que cualquier diferencia de comportamiento sea, sin ambigüedad, un defecto.
Preparar la base de código legacy
Cada hora invertida aquí reduce la conversión posterior, y ninguna se pierde aunque la migración se aplace.
Pasa de controladores a componentes. AngularJS 1.5 añadió .component(), que se parece deliberadamente al modelo moderno: un scope aislado, bindings de entrada unidireccionales con <, callbacks de salida con & y una clase controladora con controllerAs en lugar de $scope. Dar esta forma a una pantalla es el paso de preparación que más rinde, porque el componente resultante se traslada casi línea por línea a su sustituto.
angular.module('orders').component('orderRow', {
bindings: { order: '<', onRemove: '&' }, // one-way in, callback out
controllerAs: 'vm',
template: `
<tr>
<td>{{ vm.order.reference }}</td>
<td><button ng-click="vm.onRemove({ id: vm.order.id })">Remove</button></td>
</tr>`,
controller: function () { /* no $scope */ },
});
Elimina la dependencia de $scope. Sustituye $scope.$watch por llamadas explícitas a métodos, $scope.$emit y $scope.$broadcast por un servicio que guarde el estado, y cualquier dependencia de la herencia de scopes por un binding explícito. Los buses de eventos que cruzan scopes son lo más difícil de migrar, porque sus dependencias son invisibles.
Adopta módulos ES y un build. Sustituye las variables globales y el orden de concatenación por import/export, para que el grafo de dependencias sea explícito y las herramientas puedan analizarlo. En este punto también se vuelve posible introducir TypeScript en el código antiguo, archivo por archivo, con allowJs activado: los tipos que escribas ahora sobreviven a la migración.
Estandariza la estructura. Un componente, servicio o filtro por archivo; carpetas por funcionalidad y no por tipo; nombres coherentes; anotación de DI explícita en todas partes, para que nada dependa de que los nombres de parámetros sobrevivan a la minificación.
Retira jQuery y los plugins abandonados. Dos sistemas que se disputan el DOM de una misma página son una fuente recurrente de problemas en una migración, porque un re-render del framework descarta en silencio los cambios que hizo la otra librería. El enfoque por etapas de la guía de jQuery se aplica directamente.
Añade tests de caracterización. No puedes verificar una migración contra una especificación que nadie escribió. Los tests end-to-end sobre los recorridos críticos, escritos contra la aplicación actual antes de tocarla, son lo que convierte «se ve igual» en una prueba. Ejecútalos en cada commit en el pipeline descrito en integración continua, y consulta estrategia de automatización de pruebas para que esa suite siga siendo asumible.
Elegir una estrategia de entrega
Arranque híbrido
El paquete @angular/upgrade arranca ambos frameworks en una misma página. UpgradeModule inicia AngularJS desde dentro de una aplicación Angular; downgradeComponent envuelve un componente moderno para que una plantilla antigua pueda usarlo; downgradeInjectable y UpgradeModule.$injector.get permiten que los servicios crucen en ambos sentidos. Se migran primero los componentes hoja, luego sus padres y, por último, el enrutamiento.
Su atractivo es que una misma pantalla puede contener ambos, así que no hay una frontera rígida ni gestión de sesión duplicada. Los costes son reales. Los dos frameworks van en el bundle, de modo que la aplicación crece antes de adelgazar. Interactúan dos sistemas de detección de cambios, y las costuras —un digest antiguo disparado por un evento nuevo, o al revés— producen bugs de timing difíciles de diagnosticar. El enrutamiento tiene que pertenecer a uno de los dos lados, lo que suele implicar una fase intermedia incómoda. Y, sobre todo, el paquete de upgrade lleva años sin recibir inversión significativa y está atado a las partes más antiguas del framework, así que debes confirmar que funciona con la versión de Angular a la que apuntas antes de construir un plan sobre él. Donde mejor encaja es en una aplicación grande, muy entrelazada y con mucho estado compartido, migrada por un equipo que pueda permitirse vivir en modo híbrido durante meses.
Entrega en paralelo
Aquí las dos aplicaciones permanecen totalmente separadas y un reverse proxy o un router en el edge decide cuál sirve cada ruta: /orders a la aplicación nueva, todo lo demás a la antigua. No hay ninguna interoperabilidad entre frameworks, lo que elimina toda la categoría de bugs del modo híbrido. Cada aplicación se construye, se prueba y se despliega de forma independiente, y el bundle antiguo desaparece el día en que se mueve la última ruta.
Los costes se desplazan a otro sitio. El estado de sesión y autenticación tiene que compartirse, lo que en la práctica significa una cookie o un token que ambas aplicaciones entiendan y un flujo de single sign-on que sobreviva al salto. Navegar entre las dos implica una recarga completa de página, así que el marco común de la interfaz hay que duplicarlo o extraerlo, y la transición se nota salvo que el diseño la absorba. Durante la convivencia es inevitable algo de código duplicado. Para aplicaciones cuyas pantallas son realmente independientes —la mayoría de los back-offices y sistemas de administración—, suele ser la vía de menor riesgo, y es el mismo razonamiento que lleva a los equipos, en general, hacia las arquitecturas micro-frontend.
La reescritura
A veces lo correcto es empezar de cero: cuando la aplicación es pequeña, cuando los requisitos han cambiado tanto que migrar el comportamiento actual no tiene sentido, o cuando el código no tiene tests, ni estructura, ni nadie que lo entienda. La reescritura es la única estrategia sin coste de interoperabilidad, y la única que arrastra el riesgo clásico de la rama larga que nunca llega a integrarse mientras el original sigue cambiando. Si la eliges, ponle un límite de tiempo, congela las funcionalidades en el original y prepárate para abandonar el intento antes que dejarlo correr indefinidamente.
Traducir las piezas
Una vez elegida la estrategia, la mayor parte del trabajo es mecánica. Esta correspondencia cubre casi todo lo que te vas a encontrar:
| AngularJS | Angular moderno |
|---|---|
.controller con $scope | Clase de componente con campos y signals |
.component con bindings | Componente con input() y output() |
.directive con una función link | Componente, o directiva de atributo con host bindings |
.factory / .service / .provider | Clase @Injectable, proporcionada en root o en una ruta |
.filter | Pipe |
$http | HttpClient |
$q | Promesas nativas u observables de RxJS |
$timeout / $interval | setTimeout / setInterval, sin envoltorio de digest |
$scope.$watch | computed, effect o un setter de input |
ng-repeat | @for con una expresión track obligatoria |
ng-if / ng-show | @if, o un binding de clase o de estilo |
ng-model en un formulario | Formularios reactivos con controles tipados |
$routeProvider o estados de ui-router | Array Routes con loadComponent en carga diferida |
Registro angular.module | Imports de módulos ES más el array imports del componente |
$rootScope.$broadcast | Un servicio que guarda un signal o un subject |
Dos conversiones merecen cuidado en vez de una sustitución mecánica. Las directivas cuya función link manipula el DOM directamente suelen quedar mejor reescritas como componentes con plantillas declarativas que transcritas al pie de la letra. Y todo lo que dependía del binding bidireccional a través de varios niveles hay que reestructurarlo en un flujo unidireccional, lo que normalmente significa subir el estado a un servicio. Todos los detalles del lado destino se tratan en fundamentos de Angular.
Probar y secuenciar el trabajo
Trata la migración como una serie de incrementos publicables, nunca como una rama. Convierte una pantalla, pásala por revisión, llévala a producción detrás de un feature flag si hace falta y solo entonces empieza con la siguiente. Una rama de migración que vive tres meses acumula conflictos con el trabajo normal de funcionalidades hasta que hacer el merge se convierte en un proyecto en sí mismo.
Durante la transición, las pruebas operan en tres niveles. Los tests end-to-end escritos antes de empezar se ejecutan sin cambios contra ambas versiones: son el contrato. Los tests unitarios, por lo general, no se pueden migrar: los de AngularJS dependen de angular.mock y del inyector, así que planifica reescribirlos en lugar de traducirlos, y escribe los nuevos a medida que conviertes cada pantalla. En un montaje híbrido, añade unos pocos tests de integración centrados específicamente en las costuras, porque ahí es donde viven los bugs de interacción entre frameworks.
Secuencia por riesgo y por dependencia. Empieza por una pantalla hoja que sea realmente representativa, no la más sencilla, que no te enseña nada sobre el coste real. Aborda pronto los servicios compartidos, porque todo depende de ellos, y también la autenticación, que es lo más difícil de ejecutar en dos sitios. Deja las pantallas con más tráfico y más críticas para el negocio hasta que el equipo lleve varias conversiones hechas. Mantén un burndown visible de las pantallas pendientes: las migraciones pierden financiación cuando el progreso no se ve.
Merece la pena poner nombre a los fallos recurrentes. Rediseñar mientras se migra, de modo que cada diferencia desata un debate sobre el comportamiento esperado. Migrar sin tests y descubrir las regresiones en producción. Dejar que el estado híbrido se vuelva permanente, lo que te obliga a mantener dos frameworks para siempre. Subestimar los widgets de terceros: un plugin de grid o de calendario sin equivalente moderno puede ser la partida más grande del plan. Y tratar la migración como una tarea de fondo sin responsable, que es como un proyecto de dos trimestres se convierte en uno de tres años.
Puntos clave
- AngularJS no tiene soporte desde finales de 2021; lo que justifica migrar es el riesgo sin parchear y una experiencia que desaparece, no la moda de los frameworks.
- Los frameworks difieren en los cimientos —bucle de digest frente a detección de cambios selectiva, herencia de scopes frente a inputs explícitos, DI por cadenas frente a tokens tipados—, así que parte de la migración consiste en repensar, no en transcribir.
- Preparar el código legacy convirtiendo controladores en componentes con bindings unidireccionales, adoptando módulos ES y retirando jQuery reduce cada paso posterior y tiene valor por sí mismo.
- El arranque híbrido encaja en aplicaciones muy entrelazadas, pero carga con dos frameworks y dos sistemas de detección de cambios; la entrega en paralelo encaja con fronteras claras entre pantallas y elimina los bugs de interoperabilidad a cambio de compartir la gestión de sesión.
- Casi toda la conversión cabe en una tabla de correspondencias, pero las directivas que manipulan el DOM y el binding bidireccional a varios niveles deben rediseñarse como flujo unidireccional.
- Publica en incrementos entregables con tests end-to-end escritos de antemano como contrato, aborda pronto los servicios compartidos y la autenticación, y mantén el progreso visible para que el trabajo siga financiado.
Fuentes y lecturas recomendadas
Preguntas frecuentes
- ¿De verdad es peligroso seguir ejecutando AngularJS?
- El soporte terminó el 31 de diciembre de 2021, y desde entonces no se ha publicado ningún parche de seguridad. El riesgo no es que el framework se rompa por sí solo, sino que una vulnerabilidad descubierta en él, en una dependencia incluida o en una directiva de terceros abandonada no se corrija nunca. Controles compensatorios como una content security policy estricta y una validación rigurosa en el servidor reducen la exposición, pero no la eliminan.
- ¿Usamos ngUpgrade o ejecutamos las dos aplicaciones en paralelo?
- ngUpgrade encaja en una aplicación muy entrelazada, donde una misma pantalla mezcla componentes antiguos y nuevos que comparten servicios. La entrega en paralelo, que enruta pantallas completas a una u otra aplicación en el edge, encaja en una aplicación con fronteras claras entre pantallas y evita meter dos frameworks en un mismo bundle. Comprueba la compatibilidad del paquete de upgrade con tu versión objetivo de Angular antes de comprometerte.
- ¿Cuánto dura una migración?
- Depende del número de pantallas y de la cantidad de estado compartido mucho más que de las líneas de código. Una buena estimación consiste en convertir de principio a fin dos pantallas representativas, medir con honestidad el tiempo real incluyendo revisión y pruebas, multiplicar por las pantallas restantes y añadir un margen generoso para servicios compartidos, autenticación y widgets de terceros. Menos de unos pocos meses para una aplicación grande suele ser puro optimismo.
- ¿Podemos saltarnos la preparación y empezar a convertir ya?
- Se puede, y en una aplicación pequeña es razonable. En una grande, la preparación se amortiza sola: convertir controladores en componentes con bindings unidireccionales, pasar a módulos ES y añadir tests de caracterización reducen el tamaño de cada conversión posterior y siguen siendo útiles aunque la migración se pause. Desenredar la herencia de scopes durante la migración cuesta muchísimo más que hacerlo antes.