लेख
बहु-लेआउट और बहु-थीम आर्किटेक्चर डिज़ाइन पैटर्न
एक frontend को 100+ थीम और कई लेआउट संभालने हों, लेकिन custom logic साझा components में न घुसे—तो इसे कैसे बाँटें? इस लेख में core / view / theme leaf की layering, typed config, theme family की completeness जाँच और theme CSS को ज़रूरत के अनुसार load करने के तरीके के साथ-साथ हर layer के अपने trade-offs समझाए गए हैं।
· अपडेट किया गया · 3 मिनट में पढ़ें
मैंने एक बड़े Nx / Angular frontend platform के architecture को फिर से व्यवस्थित करने की ज़िम्मेदारी संभाली थी। उसे 100+ थीम और कई लेआउट का समर्थन करना था। अक्सर दूसरे सदस्य अपनी विकास-शैली के मुताबिक तरीके से उस multi-layout, multi-theme subtask को पूरा करते थे। समय के साथ यह तकनीकी ऋण बन जाता और एक ही आवश्यकता के लिए कई तरह के implementation logic मौजूद रहते।
इस लेख में उस समय के core डिज़ाइन को स्पष्ट किया गया है: layers कैसे बाँटें, 100+ थीम को पूरा कैसे रखें और browser से केवल ज़रूरत का CSS कैसे download करवाएँ। मूल production code सार्वजनिक नहीं किया जा सकता, इसलिए मैंने मौलिक code और तीन काल्पनिक themes के साथ इसे फिर से बनाया। यह फ्रंटएंड इंजीनियरिंग केस स्टडीज़ में उपलब्ध है और ऑनलाइन Demo को सीधे आज़माया भी जा सकता है। लेख का code इसी सार्वजनिक version से लिया गया है।
निष्कर्ष पहले: “दिखता कैसा है” और “करता क्या है”—इन दोनों को अलग रखें, फिर compiler और tests को 100+ थीम में एकरूपता बनाए रखने दें।
समस्या सिर्फ़ रंग बदलने की नहीं है#
कई थीम को सपोर्ट करने की सबसे आम शुरुआत साझा कंपोनेंट में शर्तें जोड़ने से होती है। उदाहरण के लिए, थीम A को एक अतिरिक्त बटन चाहिए, जबकि थीम B को कैंपेन के दौरान किसी दूसरे पेज पर ले जाना होता है:
if (theme === 'a') {
showExtraButton = true;
} else if (theme === 'b' && isCampaign) {
redirectTo = '/campaign';
}
जब केवल 3 themes हों, तो यह तरीका तेज़ है। themes की संख्या 100 तक पहुँचने पर हर साझा component पर ढेर सारी ऐसी branches का बोझ आ जाता है जो “सिर्फ़ किसी एक theme के लिए सही” होती हैं। नतीजा:
- Theme A की ज़रूरत पूरी करने वाला बदलाव चुपचाप Theme B को बिगाड़ सकता है, और B की जाँच की ज़िम्मेदारी किसी की नहीं होती।
- एक ही तरह का अंतर अलग-अलग components में दोहराया जाता है, और हर जगह उसका तरीका भी अलग होता है।
- नई theme जोड़ते समय कोई पक्का नहीं कह सकता कि “जहाँ-जहाँ बदलाव चाहिए थे, वहाँ सब पूरा कर दिया गया है।”
इसलिए समस्या सिर्फ़ रंग बदलने की नहीं है: हर theme को रूप और product के मामले में अलग होने की छूट देते हुए पूरे theme family को complete और predictable बनाए रखना है।
पहले साफ़ करें: लेआउट और theme दो अलग चीज़ें हैं#
इसे समझाने के लिए घर की मिसाल सबसे आसान है:
- ढाँचा और पाइपलाइन: खंभे, बिजली और पानी की व्यवस्था। हर घर में एक जैसी होती है और रहने वालों की पसंद के कारण नहीं बदलनी चाहिए।
- लेआउट: कमरों की व्यवस्था। दो बेडरूम या तीन, लिविंग रूम कहाँ है—यह तय करता है कि “चीज़ें किस तरह रखी गई हैं।”
- theme: सजावट। दीवार का रंग, सामग्री और कोनों की गोलाई—यह तय करती है कि “चीज़ें कैसी दिखती हैं।”
frontend में ये तीनों अलग-अलग layers से मेल खाते हैं। इन्हें मिलाने से ऐसी स्थिति बनती है जहाँ रंग बदलने के लिए data flow बदलना पड़ता है।
चार-layer संरचना#
Runtime theme id
│
▼
selector ──► theme leaf (complete typed config)
│
▼
shared view ◄── data service
│ └──► actions facade
▼
pure core
नीचे से ऊपर की ओर:
core: सिर्फ़ render करने की ज़िम्मेदारी#
core components को केवल ऐसा data मिलता है जो “पहले से तैयार है और सीधे render किया जा सकता है।” वे बाहर सिर्फ़ user intent भेजते हैं। उन्हें नहीं पता होता कि इस समय कौन-सी theme है; वे routing या API response format को नहीं छूते और किसी brand asset का reference नहीं देते।
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>();
}
सभी themes core साझा करते हैं, इसलिए इसका असर सबसे व्यापक होता है और इसके नियम सबसे सख़्त हैं: ज़रूरी input ही लें, और output में सिर्फ़ intent दें।
view: तालमेल की ज़िम्मेदारी#
view बीच की कड़ी है। यह data service से data लेकर उसे core के ज़रूरी रूप में बदलता है; जब core intent भेजता है, तो उसे actions facade के हवाले करता है।
@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 { /* ... */ }
Data कहाँ से आता है (data service) और click करने पर क्या होना चाहिए (actions facade)—इन दोनों की स्पष्ट सीमाएँ हैं, इसलिए वे screen components में बिखरते नहीं हैं।
लेआउट के लिए यही layer सही है। अलग-अलग लेआउट अलग shared view हो सकते हैं। वे एक ही data service और facade का उपयोग करते हैं, लेकिन core को अलग-अलग तरीकों से जोड़ते हैं। सरलता के लिए सार्वजनिक Demo में केवल एक shared view है; उस production पुनर्गठन में 26 जोड़े गए थे।
theme leaf: हर theme के लिए एक leaf node#
हर theme एक बहुत पतला component है। इसका सिर्फ़ एक काम है: appearance का एक पूरा config चुनकर shared view को देना।
@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;
}
इसमें कोई business condition नहीं होती। production में एक प्रतिनिधि, पूर्ण theme leaf को refactor करने के बाद वह 23 lines का था; पुराने तरीके में इसी path पर कई सौ lines थीं।
selector: family के सदस्यों की स्पष्ट सूची#
selector runtime की theme id के आधार पर उससे मेल खाने वाला leaf render करता है:
@switch (theme()) {
@case ('aurora') { <case-study-theme-aurora /> }
@case ('summit') { <case-study-theme-summit /> }
@default { <case-study-theme-default /> }
}
यह देखने में साधारण है; अहम बात है इसकी “स्पष्टता”: कौन-कौन सी themes हैं और हर एक का कौन-सा leaf है, यह एक नज़र में दिखता है और tests से हर एक की जाँच की जा सकती है।
Compiler से हर कमी पकड़वाएँ#
बहुत सारी themes होने पर सबसे बड़ा डर कुछ छूट जाने का है। नई theme जोड़ते हुए config जोड़ना भूल जाना, या config का format बदलने पर कुछ पुरानी themes को अपडेट न करना।
तरीका यह है कि पहले theme list को एकमात्र source के रूप में लिखें, फिर TypeScript के satisfies से हर theme के लिए पूरा config अनिवार्य करें:
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 में एक theme और जोड़ें, लेकिन THEME_APPEARANCES पूरा न करें, तो compilation fail हो जाएगा। Field का नाम ग़लत लिखने या CSS path का format ग़लत होने पर भी यही होगा।
सीधे type annotation लगाने की तुलना में satisfies का लाभ यह है कि यह shape की जाँच करता है, लेकिन हर value का concrete type बनाए रखता है। बाद में THEME_APPEARANCES.aurora का इस्तेमाल करते समय उसका type सामान्य ThemeAppearance तक widen नहीं होता।
Tests से पूरे family की रक्षा करें#
Compiler config की जाँच कर सकता है, लेकिन यह नहीं कि “यह theme सच में render होती है या नहीं।” यह काम family completeness tests करते हैं, जो घोषित हर theme को एक-एक करके enumerate करते हैं:
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();
});
config के लिए भी इसी तरह का test है: हर theme config का id field, config table में उसकी key के समान होना चाहिए; CSS path को भी family के नियमों पर खरा उतरना चाहिए।
इन दोनों जाँचों से sibling themes में कुछ छूटा है या नहीं, इस पर इंसानी review की निर्भरता घटती है और CI उस ग़लती को रोक सकता है। 100+ themes के पैमाने पर इंसान हर चीज़ नहीं देख सकता।
सिर्फ़ वही theme CSS download करें जो आप इस्तेमाल कर रहे हैं#
Layering से maintenance की समस्या हल होती है, लेकिन बहु-थीम की एक और लागत सीधे users पर पड़ती है: CSS।
पहले सभी 136 theme stylesheets को एक साथ bundle किया जाता था। User कोई भी theme इस्तेमाल करे, उसे सब कुछ download और parse करना पड़ता था। ज़रूरत के अनुसार load करने पर एक theme के resources लगभग 2.4 MB से घटकर 17 KB रह गए—करीब 99% की कमी।
मेकैनिज़्म बहुत सरल है: हर theme config अपना stylesheetHref घोषित करता है और एकमात्र loader पेज पर theme के एक <link> को manage करता है:
@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 theme बदलते समय इसे call करता है:
effect(() => {
const appearance = THEME_APPEARANCES[this.selectedTheme()];
this.#stylesheetLoader.load(appearance.stylesheetHref);
});
यहाँ “एकमात्र” अहम है। अगर हर जगह अपना <link> जोड़ा जाए, तो बार-बार theme बदलने के बाद पुरानी theme की styles बची रह सकती हैं या दो CSS files एक-दूसरे के ऊपर लागू हो सकती हैं। Tests यह पुष्टि करते हैं कि कई बार बदलने के बाद भी पेज पर केवल एक managed stylesheet मौजूद है।
Theme CSS केवल design token (CSS variables) परिभाषित करता है, और core styles सिर्फ़ इन variables को पढ़ती हैं:
:root {
--demo-surface: #fffaf1;
--demo-text: #3b2620;
--demo-accent: #c65367;
--demo-radius: 30px;
}
इसलिए theme बदलने का मतलब सिर्फ़ variables की दूसरी सूची चुनना है; core styles को बदलने की ज़रूरत नहीं पड़ती। Light और dark mode के लिए उसी file में variables का एक और set होता है।
Trade-offs#
इस डिज़ाइन की कुछ लागतें हैं:
- Files की संख्या बढ़ती है। हर theme के लिए एक leaf और हर layout के लिए एक view होने से छोटी files बढ़ती हैं। बदले में हर file की ज़िम्मेदारी साफ़ दिखती है और completeness की जाँच हो सकती है।
- config को programming language न बनाएँ। config appearance और सीमित, स्पष्ट अंतर बताने के लिए सही है। config में conditions और flows लिखना शुरू किया, तो बस पुराने if/else को दूसरी जगह छिपा दिया।
- core के लिए मज़बूत contract tests चाहिए। सभी themes इसे साझा करती हैं, इसलिए एक बदलाव 100+ themes को प्रभावित करता है।
- runtime में CSS बदलने के लिए एक owner चाहिए। बिना तालमेल के stylesheet requests से flicker हो सकता है या पुरानी styles बच सकती हैं। इसी वजह से loader एक ही होना चाहिए।
- cache अब भी अहम है। भले ही सिर्फ़ एक theme load हो, स्थिर file names और cache header ही वापसी पर तेज़ी तय करते हैं।
यह तरीका कब अपनाना उचित है#
अगर आपके product में सिर्फ़ light और dark mode हैं, तो CSS variables काफ़ी हैं; इस पूरी संरचना की ज़रूरत नहीं।
जब ये स्थितियाँ दिखने लगें, तब इस पर विचार करना उचित है:
- Themes या customer versions की संख्या 10 से अधिक हो और बढ़ती जा रही हो।
- Themes के बीच का अंतर सिर्फ़ रंगों तक सीमित न हो, बल्कि layout, feature toggles और copy भी अलग हों।
- एक theme की ज़रूरत पूरी करने वाला बदलाव अक्सर अनजाने में दूसरी theme को प्रभावित करे।
- नई theme जोड़ते समय किसी को पक्का न पता हो कि किन जगहों पर बदलाव करना है।
सबूत की सीमाएँ#
इस लेख में दिए गए production आँकड़ों का दायरा साफ़ करना ज़रूरी है:
- 23 lines refactor के बाद एक प्रतिनिधि, पूर्ण theme leaf की लंबाई है; इसका अर्थ यह नहीं कि हर leaf और हर file 23 lines की है।
- उस migration में नए architecture के 70 modules शामिल थे (24 facade, 10 data service, 10 theme config और 26 shared view)। यह बदलाव का दायरा है, performance या code reduction का परिणाम नहीं।
- 2.4 MB → 17 KB production में मापा गया था। सार्वजनिक Demo के तीन छोटे CSS files से यह आँकड़ा दोहराया नहीं जा सकता।
- उसी quality project में, अहम माप वाले पेज का Lighthouse Performance score 50+ से बढ़कर 90+ हुआ, लेकिन यह कई सुधारों का संयुक्त परिणाम था, सिर्फ़ CSS को ज़रूरत के अनुसार load करने का नहीं।
सार्वजनिक repo architecture के अपने mechanisms साबित करता है: typed contracts, runtime switching, family completeness tests, on-demand CSS और इन जाँचों को चलाने वाला CI। दावों और सबूतों की पूरी तालिका repo के सबूत की सीमा के audit में है।
सीधा परिणाम देखना हो, तो ऑनलाइन Demo खोलकर तीन themes, भाषाएँ और light/dark mode बदलें, फिर GitHub पर source code से तुलना करें।