AngularJS → Angular 21 : guide de migration

Front-end 03/04/2026 14:00:00 Mezgani said
Angularjs Migration Angular Ngupgrade Frontend
AngularJS → Angular 21 : guide de migration

Migrez votre app AngularJS 1.x vers Angular 21 : stratégie ngUpgrade, routes hybrides, migration des services et composants pas à pas.

Pourquoi migrer d'AngularJS vers Angular ?

AngularJS (version 1.x) a officiellement atteint sa fin de vie en décembre 2021. Cela signifie qu'aucune mise à jour de sécurité ni correctif n'est plus publié. Pour les équipes qui maintiennent des applications AngularJS en production, la migration vers Angular moderne (v21 en 2026) n'est plus une option — c'est une nécessité.

Les raisons principales pour migrer sont multiples : vulnérabilités de sécurité non corrigées, impossibilité de bénéficier des performances modernes (Signals, zoneless, Ivy), difficulté de recruter des développeurs AngularJS, et incompatibilité croissante avec l'écosystème npm.

Fin de vie officielle AngularJS : 31 décembre 2021. Toute application AngularJS encore en production est une dette technique et un risque de sécurité.

Comparaison des mécanismes clés

Fonctionnalité AngularJS 1.x Angular 21
ArchitectureMVC + $scopeComposants + Signals
Change DetectionDirty checking ($digest)Zoneless / Signals
DIString-based tokensType-based + inject()
RoutingngRoute / ui-routerRouter standalone
HTTP$http / $resourceHttpClient + httpResource()
Templatesng-if, ng-repeat@if, @for (control flow)
TestsKarma + JasmineVitest / Jest + Playwright
Bundle~200KB min+gzip~50KB standalone

Choisir sa stratégie de migration

Il existe trois grandes approches pour migrer une application AngularJS vers Angular. Le choix dépend de la taille de l'application, de la disponibilité de l'équipe et du niveau de tolérance aux risques.

Stratégie 1 : Big Bang (réécriture complète)

On réécrit toute l'application en Angular pendant que l'ancienne continue de tourner. Simple à planifier, mais risquée pour les grandes applications — on maintient deux codebases en parallèle pendant des mois.

Recommandée pour : petites applications (< 20 composants), prototypes, apps sans logique métier complexe.

Stratégie 2 : Migration hybride avec ngUpgrade

La bibliothèque @angular/upgrade permet de faire coexister AngularJS et Angular dans la même application. On migre composant par composant, service par service. C'est la stratégie officielle recommandée par l'équipe Angular.

Recommandée pour : applications moyennes à grandes, équipes qui ne peuvent pas bloquer les livraisons pendant la migration.

Stratégie 3 : Strangler Fig Pattern

On crée une nouvelle application Angular qui remplace progressivement les routes AngularJS via un reverse proxy. Chaque nouvelle fonctionnalité est développée en Angular, les anciennes pages AngularJS sont remplacées une à une.

Recommandée pour : très grandes applications monolithiques avec des équipes séparées par domaine fonctionnel.

Conseil terrain : Pour 90% des cas, la migration hybride avec ngUpgrade est le meilleur compromis. Elle permet de livrer de la valeur en continu tout en migrant progressivement.

ngUpgrade : installation et configuration

@angular/upgrade est la bibliothèque officielle qui crée un pont entre AngularJS et Angular. Elle permet d'utiliser des composants Angular dans AngularJS, et vice-versa.

Installation des dépendances

# Créer un nouveau projet Angular
ng new mon-app-migree --standalone --routing

# Ajouter le support de la migration
npm install @angular/upgrade
npm install angularjs # ou angular si vous utilisez l'ancien package

Configurer le module hybride

// main.ts — point d'entrée de l'application hybride
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { UpgradeModule } from '@angular/upgrade/static';
import { AppModule } from './app/app.module';

// On démarre Angular, qui démarre AngularJS ensuite
platformBrowserDynamic()
  .bootstrapModule(AppModule)
  .then(platformRef => {
    const upgrade = platformRef.injector.get(UpgradeModule);
    // 'monAppAngularJS' = nom du module AngularJS existant
    upgrade.bootstrap(document.body, ['monAppAngularJS']);
  });

Configurer AppModule pour le mode hybride

// app.module.ts — module Angular racine
import { NgModule } from '@angular/core';
import { BrowserModule } from '@angular/platform-browser';
import { UpgradeModule } from '@angular/upgrade/static';
import { HttpClientModule } from '@angular/common/http';

@NgModule({
  imports: [
    BrowserModule,
    UpgradeModule,       // active le pont AngularJS ↔ Angular
    HttpClientModule,
  ],
  declarations: [],
  providers: [],
})
export class AppModule {
  // Pas de bootstrap ici — c'est ngUpgrade qui s'en charge
  ngDoBootstrap() {}
}
Point critique : En mode hybride, ngDoBootstrap() vide est obligatoire. Sans lui, Angular tente de démarrer seul et écrase le bootstrap AngularJS.

Migrer les controllers vers les composants

En AngularJS, la logique métier réside dans les controllers attachés au $scope. En Angular, tout est composant. La migration consiste à transformer chaque controller+template AngularJS en un composant Angular autonome.

Avant — Controller AngularJS

// user-list.controller.js (AngularJS 1.x)
angular.module('monApp')
  .controller('UserListController', function($scope, UserService) {

    // Variables attachées au $scope — accessibles dans le template
    $scope.users = [];
    $scope.loading = true;
    $scope.searchQuery = '';

    // Chargement initial des utilisateurs
    UserService.getAll().then(function(users) {
      $scope.users = users;
      $scope.loading = false;
    });

    // Filtre les utilisateurs par nom
    $scope.filteredUsers = function() {
      return $scope.users.filter(function(u) {
        return u.name.toLowerCase().includes($scope.searchQuery.toLowerCase());
      });
    };

    // Supprime un utilisateur par ID
    $scope.deleteUser = function(id) {
      UserService.delete(id).then(function() {
        $scope.users = $scope.users.filter(function(u) {
          return u.id !== id;
        });
      });
    };
  });

Template AngularJS associé

<!-- user-list.html (AngularJS) -->
<div ng-controller="UserListController">
  <input ng-model="searchQuery" placeholder="Rechercher..." />
  <div ng-if="loading">Chargement...</div>
  <ul ng-if="!loading">
    <li ng-repeat="user in filteredUsers()">
      {{ user.name }}
      <button ng-click="deleteUser(user.id)">Supprimer</button>
    </li>
  </ul>
</div>

Après — Composant Angular avec Signals

// user-list.component.ts (Angular 21)
import { Component, OnInit, signal, computed, inject } from '@angular/core';
import { FormsModule } from '@angular/forms';
import { UserService } from './user.service';

@Component({
  selector: 'app-user-list',
  standalone: true,
  imports: [FormsModule],
  template: `
    <input [(ngModel)]="searchQuery" placeholder="Rechercher..." />
    @if (loading()) {
      <p>Chargement...</p>
    } @else {
      <ul>
        @for (user of filteredUsers(); track user.id) {
          <li>
            {{ user.name }}
            <button (click)="deleteUser(user.id)">Supprimer</button>
          </li>
        }
      </ul>
    }
  `,
})
export class UserListComponent implements OnInit {
  private userService = inject(UserService); // inject() remplace DI constructeur

  users = signal<User[]>([]);        // signal réactif (remplace $scope.users)
  loading = signal(true);             // signal pour l'état de chargement
  searchQuery = signal('');           // signal lié au champ de recherche

  // computed() recalcule automatiquement quand users ou searchQuery change
  filteredUsers = computed(() =>
    this.users().filter(u =>
      u.name.toLowerCase().includes(this.searchQuery().toLowerCase())
    )
  );

  ngOnInit() {
    this.userService.getAll().subscribe(users => {
      this.users.set(users);
      this.loading.set(false);
    });
  }

  deleteUser(id: number) {
    this.userService.delete(id).subscribe(() =>
      this.users.update(list => list.filter(u => u.id !== id))
    );
  }
}

Exposer le composant Angular dans AngularJS (étape transitoire)

// app.module.ts — downgrade pour utiliser le composant Angular dans AngularJS
import { downgradeComponent } from '@angular/upgrade/static';
import { UserListComponent } from './user-list/user-list.component';

// On déclare le composant Angular comme directive AngularJS
angular.module('monApp')
  .directive(
    'appUserList',
    downgradeComponent({ component: UserListComponent })
  );
<!-- Dans un template AngularJS — utilisation du composant Angular migré -->
<app-user-list></app-user-list>

Migrer les services AngularJS

Les services AngularJS sont des singletons créés avec .service(), .factory() ou .provider(). En Angular, ce sont des classes décorées avec @Injectable(). La migration est généralement la plus simple des trois étapes.

Avant — Service AngularJS

// user.service.js (AngularJS)
angular.module('monApp')
  .service('UserService', function($http) {

    var BASE_URL = '/api/users';

    // Récupère tous les utilisateurs
    this.getAll = function() {
      return $http.get(BASE_URL).then(function(response) {
        return response.data;
      });
    };

    // Récupère un utilisateur par son ID
    this.getById = function(id) {
      return $http.get(BASE_URL + '/' + id).then(function(res) {
        return res.data;
      });
    };

    // Supprime un utilisateur
    this.delete = function(id) {
      return $http.delete(BASE_URL + '/' + id);
    };
  });

Après — Service Angular

// user.service.ts (Angular 21)
import { Injectable, inject } from '@angular/core';
import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs';
import { map } from 'rxjs/operators';

export interface User {
  id: number;
  name: string;
  email: string;
}

@Injectable({ providedIn: 'root' }) // singleton global
export class UserService {
  private http = inject(HttpClient);
  private BASE_URL = '/api/users';

  // Retourne un Observable<User[]> au lieu d'une Promise
  getAll(): Observable<User[]> {
    return this.http.get<User[]>(this.BASE_URL);
  }

  // Typage fort grâce à TypeScript
  getById(id: number): Observable<User> {
    return this.http.get<User>(`${this.BASE_URL}/${id}`);
  }

  delete(id: number): Observable<void> {
    return this.http.delete<void>(`${this.BASE_URL}/${id}`);
  }
}

Utiliser un service AngularJS dans Angular (upgradeProvider)

// app.module.ts — upgrade d'un service AngularJS pour Angular
import { upgradeInjectable } from '@angular/upgrade/static';

// Expose le service AngularJS 'AuthService' comme service Angular
@NgModule({
  providers: [
    {
      provide: 'AuthService',            // token d'injection Angular
      useFactory: upgradeInjectable('AuthService'), // délègue à AngularJS
    },
  ],
})
export class AppModule {}
Ordre recommandé : Migrez d'abord les services partagés (auth, HTTP, utils), puis les composants feuilles (sans enfants), et remontez progressivement vers les composants racines.

Migrer le routing

Le routing est souvent la partie la plus délicate. En mode hybride, on peut faire coexister ngRoute/ui-router et le Router Angular, avec une répartition des routes par préfixe d'URL.

Stratégie de routing hybride

// app.routes.ts — Routes Angular (nouvelles pages uniquement)
import { Routes } from '@angular/router';
import { UserListComponent } from './user-list/user-list.component';
import { DashboardComponent } from './dashboard/dashboard.component';

export const routes: Routes = [
  // Routes migrées vers Angular
  { path: 'users', component: UserListComponent },
  { path: 'dashboard', component: DashboardComponent },
  // Toutes les autres routes sont gérées par AngularJS
  { path: '**', component: AngularJSFallbackComponent },
];
// Dans le module AngularJS — les anciennes routes restent actives
angular.module('monApp')
  .config(function($routeProvider) {
    $routeProvider
      // Routes non encore migrées
      .when('/products', { template: '<product-list></product-list>' })
      .when('/orders', { template: '<order-list></order-list>' });
      // /users et /dashboard sont maintenant gérés par Angular Router
  });
Astuce : Utilisez le préfixe /app/ pour les nouvelles routes Angular et conservez les routes AngularJS sans préfixe. Cela évite les conflits et facilite l'identification des zones migrées.

Adapter les tests

Les tests AngularJS utilisaient $injector, $rootScope.$apply() et jasmine. Les tests Angular modernes s'appuient sur TestBed et Vitest/Jest.

Avant — Test AngularJS avec ngMock

// user.service.spec.js (AngularJS)
describe('UserService', function() {
  var UserService, $httpBackend;

  // Charge le module AngularJS et injecte les dépendances
  beforeEach(module('monApp'));
  beforeEach(inject(function(_UserService_, _$httpBackend_) {
    UserService = _UserService_;
    $httpBackend = _$httpBackend_;
  }));

  afterEach(function() {
    $httpBackend.verifyNoOutstandingExpectation();
  });

  it('devrait charger les utilisateurs', function(done) {
    var mockUsers = [{ id: 1, name: 'Alice' }];
    $httpBackend.expectGET('/api/users').respond(200, mockUsers);

    UserService.getAll().then(function(users) {
      expect(users.length).toBe(1);
      expect(users[0].name).toBe('Alice');
      done();
    });

    $httpBackend.flush(); // déclenche les requêtes HTTP mockées
  });
});

Après — Test Angular avec TestBed et Vitest

// user.service.spec.ts (Angular 21 + Vitest)
import { TestBed } from '@angular/core/testing';
import { HttpClientTestingModule, HttpTestingController } from '@angular/common/http/testing';
import { UserService } from './user.service';

describe('UserService', () => {
  let service: UserService;
  let httpMock: HttpTestingController;

  beforeEach(() => {
    TestBed.configureTestingModule({
      imports: [HttpClientTestingModule],
      providers: [UserService],
    });
    service = TestBed.inject(UserService);
    httpMock = TestBed.inject(HttpTestingController);
  });

  afterEach(() => httpMock.verify()); // vérifie qu'aucune requête n'est en attente

  it('devrait charger les utilisateurs', () => {
    const mockUsers = [{ id: 1, name: 'Alice', email: 'alice@test.com' }];

    service.getAll().subscribe(users => {
      expect(users.length).toBe(1);
      expect(users[0].name).toBe('Alice');
    });

    // Intercepte et répond à la requête HTTP
    const req = httpMock.expectOne('/api/users');
    expect(req.request.method).toBe('GET');
    req.flush(mockUsers); // simule la réponse du serveur
  });
});

Checklist de migration complète

Utilisez cette checklist pour suivre l'avancement de votre migration. Chaque étape validée réduit le risque et rapproche de la fin du mode hybride.

  • Inventaire complet : lister tous les controllers, services, directives et routes
  • Installer @angular/upgrade et configurer le mode hybride
  • Migrer les services partagés (auth, HTTP, utils) en premier
  • Créer les composants Angular en remplacement des controllers feuilles
  • Downgrader les composants Angular pour les utiliser dans les templates AngularJS
  • Migrer les routes les moins critiques vers Angular Router
  • Réécrire les tests avec TestBed + Vitest
  • Supprimer progressivement les dépendances AngularJS des zones migrées
  • Retirer UpgradeModule quand le dernier composant AngularJS est supprimé
  • Activer le mode zoneless et les Signals pour les performances maximales
Indicateur de fin de migration : Quand angular.js n'est plus dans votre index.html et que UpgradeModule est retiré de AppModule, la migration est terminée. Félicitations !

Ressources officielles

Partager