Desenvolvimento web e mobile
Migrando de AngularJS para Angular: estratégias, sequenciamento e riscos
Avançado11 min de leituraAtualizado
Esta página foi traduzida do inglês com a ajuda de IA e conferida com o original. Ler a versão em inglês
O AngularJS chegou ao fim da vida em 2021, então toda aplicação que ainda o usa roda sem correções sobre um framework que ninguém mais mantém. Este guia compara com honestidade as duas arquiteturas, mostra como preparar uma base de código legada para que qualquer caminho de migração fique mais barato, pesa o bootstrap híbrido contra a entrega lado a lado e a reescrita, e mapeia cada conceito do AngularJS para o seu substituto moderno.
O que você vai aprender
- Justificar uma migração em termos de risco, custo e manutenibilidade, e não de moda
- Explicar as diferenças de arquitetura entre scopes com um digest loop e componentes com signals
- Preparar uma base de código AngularJS para que a migração futura seja menor e mais segura
- Escolher entre bootstrap híbrido, entrega lado a lado e reescrita conforme a sua situação
- Converter controllers, diretivas, filtros, services e rotas para os equivalentes modernos
- Manter a aplicação publicável e testada durante uma transição de vários meses
Toda aplicação AngularJS ainda em produção roda sobre um framework que parou de receber correções no último dia de 2021. Nada de dramático aconteceu nessa data — o código continuou funcionando —, mas o perfil de risco mudou para sempre. A próxima vulnerabilidade encontrada no framework, ou na longa cauda de diretivas abandonadas das quais a maioria dessas aplicações depende, nunca será corrigida; os navegadores continuam evoluindo por baixo, e o número de desenvolvedores dispostos a trabalhar com ele diminui a cada ano.
Esse é o argumento para migrar, e é um argumento de risco, não de estética. Isso importa porque migrações são aprovadas por pessoas que precisam de um motivo melhor do que "o novo é mais bonito". O que vem a seguir é o lado prático: como as duas arquiteturas realmente diferem, o que fazer na base de código legada antes de começar, como escolher uma estratégia de entrega e como continuar entregando funcionalidades enquanto a transição acontece.
Dois frameworks com o mesmo nome e fundações diferentes
O nome é infeliz: AngularJS e Angular compartilham uma marca e quase nada além disso. Entender onde os modelos divergem mostra quais partes de uma migração são mecânicas e quais exigem repensar o design.
| Aspecto | AngularJS | Angular |
|---|---|---|
| Unidade de interface | Controller mais template, depois .component() | Classe com um decorator, standalone por padrão |
| Contêiner de estado | $scope, com herança prototípica | Campos de classe, geralmente signals |
| Mecanismo de atualização | Digest loop que reavalia cada watcher até estabilizar | Detecção de mudanças direcionada, guiada por inputs, eventos e signals |
| Fluxo de dados | Two-way binding em quase todo lugar | Unidirecional para baixo, eventos para cima |
| Injeção de dependências | Por nome em string, com anotação necessária para a minificação | Por tipo ou token, resolvida por um injetor hierárquico |
| Linguagem | JavaScript ES5, depois ES6 | TypeScript, templates fortemente tipados |
| Modularidade | Registros angular.module e um namespace global | ES modules e o grafo de imports do compilador |
| Unidade de composição | Diretivas com uma função link | Componentes e diretivas de atributo |
| Ferramentas | Concatenação, Grunt ou Gulp | CLI, compilação ahead-of-time, tree shaking |
O digest loop é o que mais molda uma base de código legada. Como o AngularJS não tinha como saber o que tinha mudado, ele reavaliava todos os watchers registrados repetidamente até os valores se estabilizarem, com limite de dez passagens. Daí a conhecida queda brusca de desempenho a partir de alguns milhares de bindings, os erros $apply already in progress quando um código rodava sem o framework saber, e o hábito de adicionar $scope.$watch para colar as peças. O Angular moderno inverte isso: ele sabe exatamente quais bindings dependem de qual estado, então uma atualização toca apenas os componentes afetados. Qualquer lógica da sua aplicação que existe para compensar o digest loop pode simplesmente ser apagada, mas antes você precisa reconhecê-la.
A segunda diferença estrutural é a herança de scopes. Um scope filho herdava do pai, então um template podia ler um valor definido três níveis acima e — graças aos protótipos do JavaScript — escrever um valor primitivo no filho sombreava silenciosamente o do pai em vez de atualizá-lo. Isso gerou todo um gênero de bugs e uma regra popular ("sempre tenha um ponto no seu ng-model"). No novo modelo não existe equivalente: os dados chegam por inputs explícitos, e nada fica implicitamente visível a partir de um ancestral.
Avalie antes de planejar
Comece por um inventário, não por uma prova de conceito. Conte as telas e as definições de rotas. Liste cada diretiva, módulo e plugin jQuery de terceiros, e marque quais estão abandonados. Meça quanta lógica vive nos controllers em comparação com os services: a lógica que já está em services é em grande parte portável; a dos controllers, em geral, não. Encontre o estado compartilhado: autenticação, usuário atual, permissões, um carrinho, um cache em memória. É o estado compartilhado que determina se a aplicação antiga e a nova podem rodar lado a lado ou precisam ser entrelaçadas.
Depois, seja honesto sobre o destino. Uma migração é uma rara oportunidade de corrigir uma arquitetura e, na mesma medida, uma rara oportunidade de descarrilar um projeto tentando corrigir tudo de uma vez. Decida explicitamente o escopo: só o framework, o framework mais o design system, ou o framework mais um novo contrato com o backend. Cada acréscimo multiplica o risco. A maioria das migrações bem-sucedidas mantém fixos o design visual e o contrato da API, para que qualquer diferença de comportamento seja inequivocamente um defeito.
Preparando a base de código legada
Cada hora investida aqui encolhe a conversão seguinte, e nenhuma é desperdiçada mesmo que a migração seja adiada.
Passe de controllers para componentes. O AngularJS 1.5 introduziu .component(), que é propositalmente próximo do modelo moderno: um scope isolado, bindings de entrada unidirecionais com <, callbacks de saída com & e uma classe controller com controllerAs em vez de $scope. Dar esse formato a uma tela é o passo de preparação de maior valor, porque o componente resultante se converte quase linha a linha no seu substituto.
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 */ },
});
Elimine a dependência de $scope. Substitua $scope.$watch por chamadas explícitas de métodos, $scope.$emit e $scope.$broadcast por um service que guarde o estado, e qualquer dependência da herança de scopes por um binding explícito. Barramentos de eventos que atravessam scopes são o mais difícil de migrar, porque as dependências são invisíveis.
Adote ES modules e um build. Troque variáveis globais e ordem de concatenação por import/export, para que o grafo de dependências fique explícito e as ferramentas consigam analisá-lo. É também nesse momento que se torna possível introduzir TypeScript no código antigo, arquivo por arquivo, com allowJs habilitado — os tipos escritos agora sobrevivem à migração.
Padronize a estrutura. Um componente, service ou filtro por arquivo; pastas por funcionalidade, e não por tipo; nomenclatura consistente; anotação de DI explícita em todo lugar, para que nada dependa de nomes de parâmetros sobreviverem à minificação.
Remova o jQuery e os plugins abandonados. Dois sistemas disputando o DOM da mesma página são uma fonte recorrente de dor na migração, porque uma nova renderização do framework descarta silenciosamente as alterações feitas pela outra biblioteca. A abordagem em etapas do guia de jQuery se aplica diretamente.
Adicione testes de caracterização. Não dá para verificar uma conversão em relação a uma especificação que ninguém escreveu. Testes end-to-end sobre as jornadas críticas, escritos contra a aplicação atual antes de mexer nela, são o que transforma "parece igual" em evidência. Rode-os a cada commit no pipeline descrito em integração contínua, e veja estratégia de automação de testes para manter essa suíte viável.
Escolhendo uma estratégia de entrega
Bootstrap híbrido
O pacote @angular/upgrade inicializa os dois frameworks na mesma página. O UpgradeModule inicia o AngularJS de dentro de uma aplicação Angular; o downgradeComponent envolve um componente moderno para que um template antigo possa usá-lo; downgradeInjectable e UpgradeModule.$injector.get permitem que services cruzem nos dois sentidos. Você migra primeiro os componentes folha, depois os pais deles e, por fim, o roteamento.
O atrativo é que uma mesma tela pode conter os dois, então não há fronteira rígida nem gerenciamento de sessão duplicado. Os custos são reais. Os dois frameworks vão no bundle, então a aplicação cresce antes de diminuir. Dois sistemas de detecção de mudanças interagem, e as junções — um digest antigo disparado por um evento novo, ou o contrário — produzem bugs de timing difíceis de diagnosticar. O roteamento precisa pertencer a um dos lados, o que geralmente significa uma fase intermediária desajeitada. E, crucialmente, o pacote de upgrade não recebe investimento relevante há anos e está preso às partes mais antigas do framework, então você precisa confirmar que ele funciona com a versão do Angular que pretende adotar antes de montar um plano em cima dele. Ele se encaixa melhor em uma aplicação grande, muito entrelaçada e com bastante estado compartilhado, migrada por um time que pode se dar ao luxo de viver no modo híbrido por meses.
Entrega lado a lado
Aqui as duas aplicações continuam totalmente separadas, e um reverse proxy ou roteador na borda decide qual delas atende cada caminho: /orders para a aplicação nova, todo o resto para a antiga. Não há nenhuma interoperabilidade entre frameworks, o que elimina toda a categoria de bugs do modo híbrido. Cada aplicação é construída, testada e implantada de forma independente, e o bundle antigo desaparece no dia em que a última rota muda de lado.
Os custos se deslocam para outro lugar. O estado de sessão e autenticação precisa ser compartilhado, o que na prática significa um cookie ou token que as duas aplicações entendam e um fluxo de single sign-on que sobreviva ao salto. Navegar entre as duas causa um recarregamento completo da página, então a moldura comum da interface precisa ser duplicada ou extraída, e a transição fica visível a menos que o design a absorva. Algum código duplicado é inevitável durante a sobreposição. Para aplicações cujas telas são de fato independentes — a maioria dos back-offices e sistemas administrativos —, essa costuma ser a rota de menor risco, e é o mesmo raciocínio que leva os times, de forma geral, às arquiteturas de micro-frontend.
A reescrita
Às vezes a resposta certa é começar do zero: quando a aplicação é pequena, quando os requisitos mudaram tanto que portar o comportamento atual não faz sentido, ou quando a base de código não tem testes, nem estrutura, nem ninguém que a entenda. A reescrita é a única estratégia sem custo de interoperabilidade e a única que carrega o risco clássico da branch longa que nunca chega a ser integrada enquanto o original continua mudando. Se optar por ela, limite-a no tempo, congele as funcionalidades no original e esteja preparado para abandonar a tentativa em vez de deixá-la correr indefinidamente.
Convertendo as peças
Escolhida a estratégia, a maior parte do trabalho é mecânica. Este mapeamento cobre a maioria do que você vai encontrar:
| AngularJS | Angular moderno |
|---|---|
.controller com $scope | Classe de componente com campos e signals |
.component com bindings | Componente com input() e output() |
.directive com uma função link | Componente, ou diretiva de atributo com host bindings |
.factory / .service / .provider | Classe @Injectable, fornecida em root ou em uma rota |
.filter | Pipe |
$http | HttpClient |
$q | Promises nativas ou observables do RxJS |
$timeout / $interval | setTimeout / setInterval, sem wrapper de digest |
$scope.$watch | computed, effect ou um setter de input |
ng-repeat | @for com uma expressão track obrigatória |
ng-if / ng-show | @if, ou um binding de classe ou de estilo |
ng-model em um formulário | Formulários reativos com controles tipados |
$routeProvider ou estados do ui-router | Array Routes com loadComponent em lazy loading |
Registro angular.module | Imports de ES modules mais o array imports do componente |
$rootScope.$broadcast | Um service que guarda um signal ou um subject |
Duas conversões merecem cuidado em vez de substituição mecânica. Diretivas com funções link que manipulam o DOM diretamente geralmente ficam melhores reescritas como componentes com templates declarativos do que transcritas ao pé da letra. E tudo o que dependia de two-way binding através de vários níveis precisa ser reestruturado em fluxo unidirecional, o que normalmente significa subir o estado para um service. Os detalhes do lado de destino estão todos em fundamentos de Angular.
Testando e sequenciando o trabalho
Trate a migração como uma sequência de incrementos publicáveis, nunca como uma branch. Converta uma tela, passe-a pela revisão, coloque-a em produção atrás de uma feature flag se necessário, e só então comece a próxima. Uma branch de migração que dura três meses acumula conflitos com o trabalho normal de funcionalidades até que fazer o merge vire um projeto à parte.
Durante a transição, os testes atuam em três níveis. Os testes end-to-end escritos antes do início do trabalho rodam sem alteração contra as duas versões — eles são o contrato. Os testes unitários, em geral, não são portáveis: os do AngularJS dependem de angular.mock e do injetor, então planeje reescrevê-los em vez de traduzi-los, e escreva os novos à medida que converte cada tela. Em um cenário híbrido, adicione alguns testes de integração focados especificamente nas junções, porque é lá que vivem os bugs de interação entre frameworks.
Sequencie por risco e por dependência. Comece por uma tela folha que seja de fato representativa — não a mais simples, que não ensina nada sobre o custo real. Faça cedo os services compartilhados, porque tudo depende deles, e a autenticação também, já que é o mais difícil de rodar em dois lugares. Deixe as telas de maior tráfego e mais críticas para o negócio para quando o time já tiver feito várias conversões. Mantenha um burndown visível das telas restantes; migrações perdem financiamento quando o progresso é invisível.
Vale a pena dar nome aos modos de falha recorrentes. Redesenhar enquanto migra, de modo que cada diferença dispara um debate sobre o comportamento esperado. Migrar sem testes e descobrir regressões em produção. Deixar o estado híbrido virar permanente, o que obriga você a manter dois frameworks para sempre. Subestimar widgets de terceiros — um plugin de grid ou de agenda sem equivalente moderno pode ser o maior item isolado do plano. E tratar a migração como uma tarefa de fundo sem dono, que é como um projeto de dois trimestres vira um de três anos.
Principais conclusões
- O AngularJS está sem suporte desde o fim de 2021; o motivo para migrar é o risco sem correção e a expertise que está desaparecendo, não a moda dos frameworks.
- Os frameworks diferem na fundação — digest loop versus detecção de mudanças direcionada, herança de scopes versus inputs explícitos, DI por string versus tokens tipados —, então partes da migração exigem repensar em vez de transcrever.
- Preparar o código legado convertendo controllers em componentes com bindings unidirecionais, adotando ES modules e removendo o jQuery diminui cada passo posterior e tem valor por si só.
- O bootstrap híbrido serve para aplicações muito entrelaçadas, mas carrega dois frameworks e dois sistemas de detecção de mudanças; a entrega lado a lado serve para fronteiras claras entre telas e elimina bugs de interoperabilidade, ao custo de compartilhar o gerenciamento de sessão.
- A maior parte da conversão é uma tabela de consulta, mas diretivas que manipulam o DOM e two-way binding em vários níveis precisam ser redesenhados como fluxo unidirecional.
- Entregue em incrementos publicáveis, com testes end-to-end escritos antes como contrato, faça cedo os services compartilhados e a autenticação, e mantenha o progresso visível para que o trabalho continue financiado.
Fontes e leituras complementares
Perguntas frequentes
- Manter o AngularJS rodando é realmente inseguro?
- O suporte terminou em 31 de dezembro de 2021, e nenhuma correção de segurança foi publicada desde então. O risco não é o framework quebrar sozinho; é que uma vulnerabilidade divulgada nele, em uma dependência empacotada ou em uma diretiva de terceiros abandonada nunca seja corrigida. Controles compensatórios como uma content security policy rígida e validação rigorosa no servidor reduzem a exposição, mas não a eliminam.
- Devemos usar o ngUpgrade ou rodar as duas aplicações lado a lado?
- O ngUpgrade serve para uma aplicação muito entrelaçada, em que uma mesma tela mistura componentes antigos e novos que compartilham services. A entrega lado a lado, que roteia telas inteiras para uma ou outra aplicação na borda, serve para uma aplicação com fronteiras claras entre telas e evita empacotar dois frameworks no mesmo bundle. Verifique a compatibilidade do pacote de upgrade com a sua versão alvo do Angular antes de se comprometer.
- Quanto tempo leva uma migração?
- Depende do número de telas e da quantidade de estado compartilhado muito mais do que das linhas de código. Uma boa estimativa é converter de ponta a ponta duas telas representativas, medir com honestidade o tempo gasto incluindo revisão e testes, multiplicar pelo número de telas restantes e somar uma folga generosa para services compartilhados, autenticação e widgets de terceiros. Menos de alguns meses para uma aplicação grande costuma ser otimismo.
- Podemos pular a preparação e começar a converter imediatamente?
- Pode, e em uma aplicação pequena isso é razoável. Em uma grande, a preparação se paga: converter controllers em componentes com bindings unidirecionais, migrar para ES modules e adicionar testes de caracterização reduzem o tamanho de cada conversão posterior e continuam úteis mesmo que a migração seja pausada. Desembaraçar a herança de scopes durante a migração sai muito mais caro do que fazer isso antes.