Makaleler
Çok Yerleşimli ve Çok Temalı Mimari Tasarım Kalıpları
Tek bir ön yüz 100'den fazla temayı ve birden çok yerleşimi taşırken özel mantığın ortak bileşenlere sızmasını nasıl önleyebilirsiniz? Bu yazı core / view / theme leaf katmanlarını, tiplenmiş yapılandırmayı, aile bütünlüğü testlerini ve tema CSS'inin isteğe bağlı yüklenmesini, ayrıca her katmanın kendi ödünleşimlerini inceliyor.
· Güncellendi · 8 dk okuma
Bir zamanlar büyük bir Nx / Angular ön yüz platformunun mimari yeniden yapılandırmasından sorumluydum. Platformun 100'den fazla temayı ve birkaç yerleşimi desteklemesi gerekiyordu; üstelik başka ekip üyeleri sık sık, kendi geliştirme alışkanlıklarına uygun biçimde, o seferki çok yerleşimli ve çok temalı alt görevi tamamlıyordu. Bu uzun vadede birikip teknik borca dönüşüyor, aynı ihtiyaç için aynı anda birden çok uygulama mantığı yaşıyordu.
Bu yazıda o dönemin çekirdek tasarımını açıkça anlatıyorum: nasıl katmanlandırdık, yüz küsur temanın hepsini nasıl eksiksiz tuttuk, tarayıcının yalnızca kullanılan CSS'i indirmesini nasıl sağladık. Orijinal production kodu açıklanamayacağı için, özgün kod ve üç kurgusal temayla yeniden uyguladım ve frontend-engineering-case-studies içine koydum; doğrudan deneyebileceğiniz bir çevrimiçi Demo de var. Yazıdaki kodların hepsi bu herkese açık sürümden geliyor.
Önce sonucu söyleyeyim: "neye benzediği" ile "ne yaptığını" ayırın, sonra yüz küsur temanın tutarlılığını derleyiciye ve testlere koruttur.
Sorun renk değiştirmek değil#
Çok temalının en yaygın başlangıcı, ortak bileşene koşul eklemektir. Örneğin A temasında fazladan bir düğme olacak, B temasında kampanya döneminde başka bir sayfaya yönlendirme yapılacak:
if (theme === 'a') {
showExtraButton = true;
} else if (theme === 'b' && isCampaign) {
redirectTo = '/campaign';
}
Yalnızca üç tema varken bu yazış hızlıdır. Tema sayısı yüze çıkınca her ortak bileşen, "yalnızca belirli bir tema için geçerli" dallarla yüklenir. Sonuçlar şunlar:
- A temasının bir ihtiyacını değiştirmek, kimsenin kontrol etmediği B temasını sessizce bozabilir.
- Aynı tür fark farklı bileşenlerde birer kez, üstelik farklı biçimlerde yazılır.
- Yeni bir tema eklendiğinde "doldurulması gereken her yer dolduruldu" diye kimse emin olamaz.
Dolayısıyla çözülmesi gereken yalnızca renk değiştirmek değil: her temanın görünüşte ve üründe farklı olmasına izin verirken tüm tema ailesini eksiksiz ve öngörülebilir tutmak.
Önce ayırın: yerleşim ve tema iki ayrı şeydir#
En kolay anlaşılan benzetme bir ev:
- Yapı ve tesisat hattı: Kolonlar, elektrik ve su tesisatı. Her daire için aynıdır ve sakinlerin zevkine göre değişmemelidir.
- Yerleşim: Planlama. İki oda mı üç oda mı, salon nerede. "Şeylerin nasıl dizildiğini" belirler.
- Tema: Dekorasyon. Duvar rengi, malzeme, köşe yuvarlaklığı. "Şeylerin nasıl göründüğünü" belirler.
Ön yüzde bu üç şey farklı katmanlara karşılık gelir. Karıştırılırsa "rengi değiştirmek için veri akışına dokunmak" gibi durumlar ortaya çıkar.
Dört katmanlı yapı#
Runtime theme id
│
▼
selector ──► theme leaf (complete typed config)
│
▼
shared view ◄── data service
│ └──► actions facade
▼
pure core
Aşağıdan yukarı bakalım:
core: yalnızca çizmekle ilgilenir#
core bileşeni yalnızca "hazırlanmış ve doğrudan render edilebilir" veriyi alır, dışarıya yalnızca kullanıcının niyetini (intent) gönderir. Şu anki temanın hangisi olduğunu bilmez, routing'e dokunmaz, API dönüş biçimine dokunmaz, hiçbir marka malzemesine de başvurmaz.
export class ThemeCardCoreComponent {
readonly appearance = input.required<ThemeAppearance>();
readonly content = input.required<ThemeContent>();
readonly items = input.required<readonly ShowcaseItem[]>();
readonly selectedId = input<string | null>(null);
readonly itemSelected = output<ShowcaseItem>();
readonly selectionCleared = output<void>();
}
core tüm temalarca paylaşıldığı için etki alanı en geniş olandır, bu yüzden kuralları da en katıdır: girdiler yalnızca gerekli olanlardır, çıktılar yalnızca niyettir.
view: eşgüdümden sorumludur#
view arabulucudur. Veriyi data service'ten alır, core'un istediği biçime dönüştürür; core bir niyet gönderdiğinde onu actions facade'e devreder.
@Component({
selector: 'case-study-theme-card-view',
providers: [FeatureActionsFacade, FeatureDataService],
template: `
<case-study-theme-card-core
[appearance]="appearance()"
[content]="themeContent()"
[items]="data.items()"
[selectedId]="actions.selectedId()"
(itemSelected)="select($event)"
(selectionCleared)="actions.reset()"
/>
`,
})
export class ThemeCardViewComponent { /* ... */ }
Verinin nereden geldiği (data service) ve tıklanınca ne yapılacağı (actions facade) için ayrı ve net sınırlar vardır; görünüm bileşenlerine dağılmazlar.
Yerleşim bu katmana uygundur. Farklı planlar farklı shared view'lar olabilir; aynı data service ve facade'e bağlanırlar, yalnızca core'u farklı diziliş biçimlerinde birleştirirler. Herkese açık Demo basitlik için tek bir shared view içerir; production'daki yeniden yapılandırma ise 26 tane getirdi.
theme leaf: her tema için bir yaprak düğüm#
Her tema çok ince bir bileşendir ve tek bir iş yapar: eksiksiz bir görünüm yapılandırması seçip shared view'a verir.
@Component({
selector: 'case-study-theme-aurora',
imports: [ThemeCardViewComponent],
template:
'<case-study-theme-card-view [appearance]="appearance" data-theme-leaf="aurora" />',
})
export class ThemeAuroraComponent {
protected readonly appearance = THEME_APPEARANCES.aurora;
}
İçinde hiçbir iş koşulu yoktur. Production'da, temsilî ve eksiksiz bir theme leaf yeniden yapılandırmadan sonra 23 satırdı; eski yazımda aynı yol birkaç yüz satırdı.
selector: aile üyelerini açıkça sıralar#
selector, runtime'daki tema id'sine göre ilgili leaf'i render eder:
@switch (theme()) {
@case ('aurora') { <case-study-theme-aurora /> }
@case ('summit') { <case-study-theme-summit /> }
@default { <case-study-theme-default /> }
}
Sıradan görünür, ama asıl nokta "açık olması"dır: hangi temaların olduğu ve her birinin hangi leaf'e karşılık geldiği bir bakışta görülür ve tek tek test edilebilir.
Derleyicinin sizin yerinize yoklama yapmasını sağlayın#
Tema sayısı artınca en çok korkulan şey eksik kalmaktır. Yeni bir tema eklenir ama yapılandırması unutulur; yapılandırma biçimi değişir ama birkaç eski tema ayak uyduramaz.
Yöntem, önce tema listesini tek bir kaynak olarak yazmak, sonra TypeScript'in satisfies özelliğiyle her temanın eksiksiz bir yapılandırmaya sahip olmasını zorunlu kılmaktır:
export const DEMO_THEMES = ['default', 'aurora', 'summit'] as const;
export type DemoTheme = (typeof DEMO_THEMES)[number];
export interface ThemeAppearance {
readonly density: 'comfortable' | 'compact';
readonly id: DemoTheme;
readonly stylesheetHref: `themes/${string}.css`;
}
export const THEME_APPEARANCES = {
default: { id: 'default', density: 'comfortable', stylesheetHref: 'themes/default.css' },
aurora: { id: 'aurora', density: 'comfortable', stylesheetHref: 'themes/aurora.css' },
summit: { id: 'summit', density: 'compact', stylesheetHref: 'themes/summit.css' },
} satisfies Record<DemoTheme, ThemeAppearance>;
DEMO_THEMES'e bir tema eklenir de THEME_APPEARANCES tamamlanmazsa derleme başarısız olur. Alan adı yanlış yazılırsa ya da CSS yolunun biçimi hatalıysa da aynısı olur.
satisfies'ın doğrudan tip belirtmeye göre üstünlüğü şudur: şekli denetler ama her değerin somut tipini korur; sonradan THEME_APPEARANCES.aurora kullanıldığında genel bir ThemeAppearance'a genişletilmez.
Testlerin tüm aileyi korumasını sağlayın#
Derleyici yapılandırmayı denetleyebilir, ama "bu tema gerçekten render ediliyor mu" sorusunu denetleyemez. Bu kısım, bildirilmiş her temayı tek tek sayan aile bütünlüğü testlerine bırakılır:
it.each(DEMO_THEMES)('renders the %s family leaf', (theme) => {
const fixture = TestBed.createComponent(ThemeSelectorComponent);
fixture.componentRef.setInput('theme', theme);
fixture.detectChanges();
expect(
fixture.nativeElement.querySelector(`[data-theme-leaf="${theme}"]`),
).toBeTruthy();
});
Yapılandırma tarafında da karşılık gelen testler vardır: her tema yapılandırmasındaki id alanı, yapılandırma tablosundaki key ile aynı olmalıdır; CSS yolu da aile kurallarına uymalıdır.
Bu iki denetim birlikte, "kardeş temalar eksik mi" sorusunu insan review'una bırakan işi CI'ın engelleyeceği bir hataya dönüştürür. Yüz küsur temalık ölçekte göz bunu bitiremez.
Yalnızca kullandığınız temanın CSS'ini indirin#
Katmanlama bakım sorununu çözer, ama çok temalılığın doğrudan kullanıcıya binen bir maliyeti daha vardır: CSS.
Önceki yöntemde 136 tema stylesheet'inin hepsi birlikte paketleniyordu; kullanıcı hangi temayı kullanırsa kullansın hepsini indirmek ve ayrıştırmak zorundaydı. İsteğe bağlı yüklemeye geçildikten sonra tek bir temanın kaynağı yaklaşık 2,4 MB'tan 17 KB'a düştü, yani yaklaşık %99 azaldı.
Mekanizmanın kendisi basittir: her tema yapılandırması kendi stylesheetHref değerini bildirir ve sayfadaki o tek tema <link>'ini tek bir loader yönetir:
@Injectable()
export class ThemeStylesheetLoader {
readonly #document = inject(DOCUMENT);
load(href: ThemeAppearance['stylesheetHref']): void {
const id = 'case-study-theme-stylesheet';
const existing = this.#document.getElementById(id) as HTMLLinkElement | null;
const link = existing ?? this.#document.createElement('link');
link.id = id;
link.rel = 'stylesheet';
if (link.getAttribute('href') !== href) link.setAttribute('href', href);
if (!existing) this.#document.head.append(link);
}
}
Platform, tema değiştiğinde bunu çağırır:
effect(() => {
const appearance = THEME_APPEARANCES[this.selectedTheme()];
this.#stylesheetLoader.load(appearance.stylesheetHref);
});
"Tek" olması önemli. Her yer kendi <link>'ini eklerse, tekrar tekrar geçiş yapıldığında eski temanın stilleri kalır ya da iki CSS birbirinin üstüne yazar. Testler, birkaç kez geçişten sonra sayfada hâlâ yalnızca bir yönetilen stylesheet olduğunu doğrular.
Tema CSS'inin kendisi yalnızca design token'ları (CSS değişkenlerini) tanımlar, core'un stilleri de yalnızca bu değişkenleri okur:
:root {
--demo-surface: #fffaf1;
--demo-text: #3b2620;
--demo-accent: #c65367;
--demo-radius: 30px;
}
Yani tema değiştirmek yalnızca bir değişken tablosunu değiştirmektir; core'un stillerine hiç dokunulmaz. Açık/koyu mod ise aynı dosyadaki başka bir değişken setidir.
Ödünleşimler#
Bu tasarım bedava değil:
- Dosya sayısı artar. Her tema için bir leaf, her yerleşim için bir view; küçük dosyaların sayısı çoğalır. Karşılığında her dosyanın sorumluluğu bir bakışta görülür ve bütünlük denetlenebilir.
- Yapılandırma bir programlama diline dönüşmemeli. config, görünümü ve sınırlı farkları anlatmaya uygundur. Yapılandırmanın içine koşul ve akış yazmaya başlarsanız, eski if/else'i yalnızca başka bir yere saklamış olursunuz.
- core için daha güçlü sözleşme testleri gerekir. Tüm temalarca paylaşıldığı için tek bir değişiklik yüz küsur temayı etkiler.
- Runtime'da CSS değiştirmek tek bir sahip gerektirir. Eşgüdümsüz stylesheet istekleri titremeye veya artıklara yol açar; loader'ın tek olması gereken neden de budur.
- Önbellek hâlâ önemli. Yalnızca tek bir tema yüklense bile, kararlı dosya adları ve cache header'ları geri gelen ziyaretçinin hızını belirler.
Ne zaman bunu yapmaya değer#
Ürününüzde yalnızca açık ve koyu mod varsa CSS değişkenleri yeter, bu paketin tamamına gerek yok.
Şu durumlarla karşılaşmaya başladıysanız düşünmeye değer:
- Tema veya müşteri sürümü sayısı on'u aşıyor ve artmaya devam ediyor.
- Temalar arasındaki fark yalnızca renk değil; dizilim, özellik anahtarları ve metinler de farklı.
- Bir temanın ihtiyacını değiştirmek sık sık beklenmedik biçimde başka bir temayı etkiliyor.
- Yeni bir tema eklenirken nerelerin doldurulması gerektiğinden kimse emin olamıyor.
Kanıtın sınırı#
Bu yazıda geçen production sayılarının kapsamını net söylemek gerekir:
- 23 satır, yeniden yapılandırmadan sonra temsilî ve eksiksiz bir theme leaf'in satır sayısıdır; her leaf'in ya da her dosyanın 23 satır olduğu anlamına gelmez.
- O migration 70 yeni mimari modül getirdi (24 facade, 10 data service, 10 tema config'i, 26 shared view). Bu, dönüşümün kapsamıdır; performans ya da kod azaltma kazanımı değildir.
- 2,4 MB → 17 KB, production'da yapılan gerçek ölçümdür. Herkese açık Demo'daki üç küçük CSS bu sayıyı yeniden üretmez.
- Aynı kalite projesinde, kilit ölçüm sayfasının Lighthouse Performance puanı 50+'dan 90+'a çıktı, ama bu birden çok iyileştirmenin toplamıdır; yalnızca CSS'in isteğe bağlı yüklenmesinin sonucu değildir.
Herkese açık repo, mimari mekanizmanın kendisini kanıtlar: tiplenmiş sözleşme, runtime geçişi, aile bütünlüğü testleri, isteğe bağlı CSS ve bu denetimleri çalıştıran CI. Tüm iddiaların karşılaştırma tablosu repo'daki kanıt sınırı denetimi belgesindedir.
Etkiyi doğrudan görmek isterseniz çevrimiçi Demo'yu açıp üç tema, dil ve açık/koyu mod arasında geçiş yapabilir, sonra GitHub'daki kaynak kodla karşılaştırabilirsiniz.