Mert Özen Her Satırda Daha İleri
Frontend Angular — #14

Angular Guard'lar ve Lazy Loading: CanActivate, CanDeactivate, CanLoad ve Resolve

Mert Özen 31 Eki 2026 8 dk 0 görüntülenme
Angular Guard'lar ve Lazy Loading: CanActivate, CanDeactivate, CanLoad ve Resolve

Angular 12'de class-based guard'larla route'ları koruyoruz: CanActivate, CanDeactivate, CanLoad ve Resolve. Feature module'ü loadChildren ile lazy yükleyip PreloadAllModules ile önceden indiriyoruz.

Sürüm notu: Bu yazıdaki kod ve komutlar Angular 12 içindir.

Kapıdaki Görevli

Daha önce gördüğümüz router, URL'e bakıp doğru component'i ekrana koyuyordu. Ama kimin hangi sayfaya girebileceğine hiç karışmıyordu. Giriş yapmamış biri adres çubuğuna /admin yazdığında yönetim paneli karşısına çıkıyordu.

Guard, router'ın her navigasyondan önce danıştığı bir görevli. Konser kapısındaki güvenlik gibi düşün: bileti olan girer, olmayan geri çevrilir, içeride yarım kalmış bir işin varsa çıkarken "emin misin?" diye sorulur. Bu yazıda dört görevliyle tanışacağız: girişi kontrol eden CanActivate, çıkışı kontrol eden CanDeactivate, bir module'ün indirilip indirilmeyeceğine karar veren CanLoad ve sayfa açılmadan veriyi hazırlayan Resolve. Arada da uygulamayı parçalara bölüp sadece gerekeni indiren lazy loading'i kuracağız.

Angular 12'de guard'lar birer class olarak yazılır. İleride Angular'ın bu konuyu nasıl değiştirdiğini sürüm yazılarında göreceğiz.

CanActivate: Bu Sayfaya Girebilir mi?

Elimizde basit bir AuthService olsun. Token localStorage'da duruyorsa kullanıcıyı giriş yapmış sayıyor:

import { Injectable } from '@angular/core';

@Injectable({ providedIn: 'root' })
export class AuthService {
  isLoggedIn(): boolean {
    return localStorage.getItem('token') !== null;
  }
}

Guard'ı CLI ile üretelim. ng generate guard auth komutu sana hangi interface'leri uygulamak istediğini soracak, listeden CanActivate'i seç. Oluşan dosyayı şöyle düzenle:

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate {

  constructor(private authService: AuthService, private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {
    if (this.authService.isLoggedIn()) {
      return true;
    }
    return this.router.createUrlTree(['/login'], { queryParams: { returnUrl: state.url } });
  }
}

Guard aslında bir service. @Injectable ile işaretli, bağımlılıklarını constructor'dan alıyor. Router navigasyondan önce canActivate metodunu çağırır ve dönen değere göre karar verir. true dönerse sayfa açılır, false dönerse navigasyon iptal edilir ve kullanıcı olduğu yerde kalır. UrlTree dönerse kullanıcı o adrese yönlendirilir.

Burada giriş yapmamış kullanıcıyı /login sayfasına gönderiyoruz ve gitmek istediği adresi returnUrl query param'ı olarak yanına ekliyoruz. Giriş başarılı olunca login sayfası kullanıcıyı kaldığı yere geri götürebilir. Guard'ın içinde router.navigate çağırmak yerine UrlTree döndürmek daha temiz; yönlendirmeyi router kendisi, doğru sırayla yapar. Sunucuya sorman gereken durumlar için metot Observable ya da Promise de döndürebilir.

Guard'ı route'a bağlamak tek satır:

{ path: 'admin', component: AdminComponent, canActivate: [AuthGuard] }

CanDeactivate: Kaydedilmemiş Değişiklikler

Kullanıcı profil formunu yarıya kadar doldurdu ve yanlışlıkla menüdeki başka bir linke tıkladı. Yazdığı her şey bir anda gitti. CanDeactivate tam bu durumu önler. Önce component'te değişiklik olup olmadığını takip edelim:

export class ProfileEditComponent {
  hasUnsavedChanges = false;

  save(): void {
    this.hasUnsavedChanges = false;
  }
}
<input (input)="hasUnsavedChanges = true" placeholder="Görünen ad">
<button (click)="save()">Kaydet</button>

Guard'ın kendisi:

import { Injectable } from '@angular/core';
import { CanDeactivate } from '@angular/router';
import { ProfileEditComponent } from './profile-edit/profile-edit.component';

@Injectable({ providedIn: 'root' })
export class UnsavedChangesGuard implements CanDeactivate<ProfileEditComponent> {

  canDeactivate(component: ProfileEditComponent): boolean {
    if (!component.hasUnsavedChanges) {
      return true;
    }
    return confirm('Kaydedilmemiş değişiklikler var. Sayfadan çıkmak istediğine emin misin?');
  }
}
{ path: 'profile/edit', component: ProfileEditComponent, canDeactivate: [UnsavedChangesGuard] }

CanDeactivate generic bir interface. Köşeli parantezdeki tip, terk edilen component'in tipi. Router guard'a o component'in kendisini verir, biz de hasUnsavedChanges alanına bakıp karar veriyoruz. Aynı guard'ı birden fazla component'te kullanmak istersen, ortak bir canDeactivate() metodu tanımlayan bir interface yazıp generic tip olarak onu verebilirsin.

Bir sınırı var: bu guard yalnızca uygulama içindeki navigasyonlarda çalışır. Kullanıcı sekmeyi kapatır ya da sayfayı yenilerse router devreye girmez; o durum için tarayıcının beforeunload event'ine ayrıca bakman gerekir.

Resolve: Veri Hazır Olmadan Sayfa Açılmasın

Ürün detay sayfası açılır, bir an boş görünür, sonra veri gelince dolar. Çoğu zaman bu kabul edilebilir. Ama bazen sayfanın verisi hazır olmadan açılmasını hiç istemezsin. Otelde oda temizlenmeden anahtarın verilmemesi gibi. Bunun için resolver yazarız; CLI'de karşılığı ng generate resolver product:

import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve } from '@angular/router';
import { Observable } from 'rxjs';
import { Product, ProductService } from './product.service';

@Injectable({ providedIn: 'root' })
export class ProductResolver implements Resolve<Product> {

  constructor(private productService: ProductService) { }

  resolve(route: ActivatedRouteSnapshot): Observable<Product> {
    const id = Number(route.paramMap.get('id'));
    return this.productService.getProduct(id);
  }
}
{ path: 'products/:id', component: ProductDetailComponent, resolve: { product: ProductResolver } }

Route tanımındaki resolve nesnesi "bu route'a girmeden önce ProductResolver'ı çalıştır, sonucunu product adıyla sakla" diyor. ProductService.getProduct metodunun daha önce gördüğümüz HttpClient ile tek bir ürün çektiğini varsayıyorum. Component tarafında veri, route'un data observable'ında hazır bekler:

export class ProductDetailComponent implements OnInit {
  product?: Product;

  constructor(private route: ActivatedRoute) { }

  ngOnInit(): void {
    this.route.data.subscribe(data => this.product = data.product);
  }
}

Router, resolver'ın döndürdüğü observable tamamlanana kadar navigasyonu bekletir. HttpClient isteği zaten tek cevaptan sonra tamamlandığı için sorun yok. Ama hiç tamamlanmayan bir stream döndürürsen sayfa asla açılmaz. İstek hata verirse navigasyon iptal olur, bu yüzden resolver'da catchError ile bir yedek plan kurmak iyi fikir. Unutma ki bekleme süresince kullanıcı eski sayfada, hiçbir şey olmuyormuş gibi bekler. Yavaş bir API'de bu his, boş bir sayfadan daha kötü olabilir.

Lazy Loading: Her Şeyi Baştan İndirmemek

Varsayılan olarak bütün component'ler ve module'ler tek bir büyük JavaScript paketinde toplanır. Uygulamanı ziyaret eden herkes, sadece birkaç yöneticinin kullandığı admin panelinin kodunu da indirir. Uygulama büyüdükçe ilk açılış yavaşlar.

Lazy loading bu paketi böler. Bir feature module ayrı bir dosyaya, yani ayrı bir chunk'a ayrılır ve ancak kullanıcı o bölümün adresine ilk kez gittiğinde indirilir. Bir ansiklopedi gibi; bütün ciltleri sırtında taşımazsın, hangi konuya bakacaksan o cildi raftan alırsın. Lazy loading'in birimi daha önce gördüğümüz feature module'dür; tek bir component'i değil, bir module'ü ve onun route'larını tembel yükleriz.

Feature Module'ü Lazy Yüklemek

CLI, lazy yüklenen bir module'ü tek komutla kurar:

ng generate module admin --route admin --module app.module

Bu komut admin klasöründe module'ü, onun routing dosyasını ve bir AdminComponent üretir, ardından app-routing.module.ts dosyasına şu satırı ekler:

const routes: Routes = [
  { path: '', component: HomeComponent },
  { path: 'admin', loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule) }
];

loadChildren bir fonksiyon alır ve bu fonksiyon dinamik bir import() döndürür. Build aracı bu import() ifadesini görünce admin module'ünü ana paketten ayırıp kendi chunk'ına koyar. Router da kullanıcı /admin adresine gittiğinde fonksiyonu çağırır, dosyayı indirir ve AdminModule'ü yükler.

Admin module'ünün kendi routing dosyası şöyle görünür:

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { AdminComponent } from './admin.component';

const routes: Routes = [{ path: '', component: AdminComponent }];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class AdminRoutingModule { }

Burada forRoot değil forChild var. forChild yeni bir Router service'i oluşturmaz, sadece mevcut router'a bu module'ün route'larını ekler. Buradaki path'ler de admin'e göre relative'dir: boş path /admin demek, { path: 'users', ... } eklersen o da /admin/users olur.

Kritik bir uyarı: AdminModule'ü AppModule'ün imports dizisine ekleme. Eklersen module ana pakete geri girer ve lazy loading sessizce bozulur. CLI bunu zaten eklemez ama elle düzenleme yaparken kolayca kaçabilir. ng build çıktısında, ilk açılışta yüklenen dosyaların altında Lazy Chunk Files başlıklı ayrı bir tablo görüyorsan doğru yoldasın. Tarayıcının Network sekmesinde de o dosyanın ancak /admin'e gittiğinde indiğini izleyebilirsin.

CanLoad: Module'ü Hiç İndirtmemek

Lazy route'a canActivate koyarsan giriş yapmamış kullanıcı admin sayfasını göremez, ama router yine de module'ü indirir. Kod kullanıcının tarayıcısına kadar gelmiş olur. Bunu tamamen engellemek için CanLoad kullanırız. Mevcut AuthGuard'a ikinci bir interface ekleyelim:

import { Injectable } from '@angular/core';
import {
  ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router,
  RouterStateSnapshot, UrlSegment, UrlTree
} from '@angular/router';
import { AuthService } from './auth.service';

@Injectable({ providedIn: 'root' })
export class AuthGuard implements CanActivate, CanLoad {

  constructor(private authService: AuthService, private router: Router) { }

  canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean | UrlTree {
    return this.authService.isLoggedIn() || this.router.createUrlTree(['/login']);
  }

  canLoad(route: Route, segments: UrlSegment[]): boolean | UrlTree {
    return this.authService.isLoggedIn() || this.router.createUrlTree(['/login']);
  }
}
{
  path: 'admin',
  loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
  canLoad: [AuthGuard],
  canActivate: [AuthGuard]
}

canLoad module indirilmeden önce çalışır. false ya da UrlTree dönerse chunk hiç talep edilmez. Neden canActivate'i de tuttuk? Çünkü module bir kez yüklendikten sonra canLoad bir daha çalışmaz. Kullanıcı çıkış yapıp tekrar /admin'e gitmeye kalkarsa onu durduracak olan canActivate'tir.

Preloading: Boş Zamanı Değerlendirmek

Lazy loading ilk açılışı hızlandırır ama küçük bir bedeli vardır: kullanıcı bölüme ilk kez girdiğinde chunk'ın inmesini bekler. Preloading ikisinin ortasını bulur:

import { PreloadAllModules, RouterModule } from '@angular/router';

@NgModule({
  imports: [RouterModule.forRoot(routes, { preloadingStrategy: PreloadAllModules })],
  exports: [RouterModule]
})
export class AppRoutingModule { }

PreloadAllModules ile uygulama önce sadece ilk ekran için gerekeni indirip açılır. Ardından, kullanıcı ekrana bakarken lazy module'leri arka planda sessizce indirir. Kullanıcı bölüme tıkladığında kod zaten hazırdır. Varsayılan strateji hiçbir şeyi önceden yüklememektir. Bir istisna var: canLoad ile korunan route'lar preload edilmez, çünkü onları indirmek için zaten izin şartı koymuştuk. Hangi module'ün önceden yükleneceğini kendin seçmek istersen PreloadingStrategy interface'ini uygulayan kendi stratejini de yazabilirsin.

Küçük Bir Deneme

Şimdi sıra sende. ng g module dashboard --route dashboard --module app.module ile lazy bir module oluştur. ng serve ile çalıştır, Network sekmesini aç ve /dashboard adresine ilk gittiğinde yeni bir JavaScript dosyasının indiğini gör.

Sonra yukarıdaki AuthService ve AuthGuard'ı kur. Konsoldan localStorage.setItem('token', 'deneme') ile giriş yapmış gibi ol, localStorage.removeItem('token') ile çık. Dashboard route'una canLoad ekle ve çıkış yapmışken dosyanın hiç inmediğini doğrula. Son olarak PreloadAllModules'ü aç ve korumasız bir lazy module'ün sayfa açıldıktan hemen sonra kendiliğinden indiğini izle.

Profil düzenleme örneğinde input'a bir şey yazılıp yazılmadığını elle takip ettik. Gerçek bir formda bundan fazlası gerekir: alanların değerlerini toplamak, zorunlu alanları ve e-posta biçimini kontrol etmek, kullanıcıya anlaşılır hata mesajları göstermek. Bir sonraki yazıda Angular'da template-driven form'ları konuşacağız.