Artikel
Corak Reka Bentuk Seni Bina untuk Pelbagai Reka Letak dan Tema
Satu platform frontend perlu menyokong 100+ tema dan pelbagai reka letak tanpa membiarkan logik penyesuaian meresap ke dalam komponen kongsi—bagaimana anda menstrukturkannya? Artikel ini menghuraikan pemisahan lapisan core / view / theme leaf, konfigurasi ditaip, ujian integriti famili, dan pemuatan CSS tema atas permintaan, berserta kompromi pada setiap lapisan.
· Dikemas kini · 9 minit bacaan
Saya pernah memimpin penstrukturan semula seni bina sebuah platform frontend Nx / Angular berskala besar yang perlu menyokong lebih 100 tema dan beberapa reka letak. Selain itu, ahli pasukan lain sering menyiapkan subtugasan pelbagai reka letak dan pelbagai tema mengikut tabiat pembangunan masing-masing. Lama-kelamaan, ini terkumpul menjadi hutang teknikal, di mana wujud pelbagai logik pelaksanaan berbeza untuk keperluan yang sama pada masa yang sama.
Artikel ini memperjelas reka bentuk teras pada masa itu: bagaimana membahagikan lapisan, bagaimana mengekalkan integriti lebih seratus tema, dan bagaimana memastikan pelayar hanya memuat turun CSS yang benar-benar digunakan. Kod production asal tidak boleh didedahkan kepada umum, jadi saya melaksanakannya semula menggunakan kod asli dan tiga tema rekaan di frontend-engineering-case-studies, berserta Demo dalam talian yang boleh dicuba secara langsung. Semua kod dalam artikel ini diambil daripada versi awam ini.
Kesimpulannya terlebih dahulu: Asingkan "rupa luarannya" daripada "apa fungsinya", kemudian biarkan pengkompil dan ujian menjaga konsistensi lebih seratus tema untuk anda.
Titik permulaan yang paling lazim untuk pelbagai tema adalah dengan menambah syarat dalam komponen kongsi. Sebagai contoh, tema A memerlukan butang tambahan, manakala tema B perlu melencongkan ke halaman kempen lain semasa tempoh promosi:
if (theme === 'a') {
showExtraButton = true;
} else if (theme === 'b' && isCampaign) {
redirectTo = '/campaign';
}
Apabila hanya ada tiga tema, menulis begini memang pantas. Namun apabila tema meningkat kepada seratus, setiap komponen kongsi terbeban dengan timbunan cabang yang "hanya sah untuk tema tertentu". Akibatnya:
- Mengubah keperluan untuk tema A mungkin merosakkan tema B secara senyap, tanpa sesiapa yang bertanggungjawab memeriksa tema B.
- Perbezaan yang sama ditulis berulang kali dalam komponen berbeza, malah dengan cara pelaksanaan yang berlainan.
- Apabila menambah tema baharu, tiada siapa yang pasti sama ada "semua bahagian yang perlu diisi telah pun lengkap".
Oleh itu, apa yang perlu diselesaikan bukan sekadar pertukaran warna, sebaliknya: Membenarkan setiap tema mempunyai perbezaan dari segi visual dan produk, sambil mengekalkan keseluruhan famili tema kekal lengkap dan boleh dijangka.
Fahami Dahulu: Reka Letak dan Tema Merupakan Dua Perkara Berbeza#
Perumpamaan sebuah rumah adalah yang paling mudah difahami:
- Struktur dan perpaipan: Tiang, bekalan air dan elektrik. Setiap unit adalah sama, dan tidak sepatutnya diubah semata-mata mengikut cita rasa penghuni.
- Reka letak: Susun atur ruang. Sama ada dua bilik atau tiga bilik, di mana ruang tamu diletakkan. Menentukan "bagaimana sesuatu disusun".
- Tema: Kemasan hiasan dalaman. Warna dinding, tekstur bahan, saiz jejari sudut. Menentukan "bagaimana sesuatu kelihatan".
Dalam frontend, ketiga-tiga perkara ini masing-masing dipetakan kepada lapisan yang berbeza. Jika dicampur aduk, anda akan berdepan situasi seperti "mengubah aliran data semata-mata untuk menukar warna".
Struktur Empat Lapisan#
Runtime theme id
│
▼
selector ──► theme leaf (complete typed config)
│
▼
shared view ◄── data service
│ └──► actions facade
▼
pure core
Melihat dari bawah ke atas:
core:Hanya Bertanggungjawab Merender#
Komponen core hanya menerima data yang "sudah sedia dan boleh dirender secara terus", serta hanya memancarkan niat (intent) pengguna ke luar. Ia tidak tahu tema apa yang sedang aktif, tidak menyentuh routing mahupun format respons API, dan tidak merujuk kepada sebarang aset penjenamaan.
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 dikongsi oleh semua tema dan mempunyai skop impak paling luas, jadi peraturannya paling ketat: Input hanya apa yang perlu, output hanya niat.
view:Bertanggungjawab Menyelaras#
view bertindak sebagai perantara. Ia mengambil data daripada data service dan menukarnya kepada bentuk yang diperlukan oleh core; apabila core mengeluarkan niat, ia diserahkan kepada actions facade untuk dikendalikan.
@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 { /* ... */ }
Dari mana data diperoleh (data service) dan apa yang perlu dilakukan apabila diklik (actions facade) masing-masing mempunyai sempadan yang jelas, dan tidak berselerak di dalam komponen paparan.
Reka letak sangat sesuai ditempatkan pada lapisan ini. Susun atur ruang yang berbeza boleh diwujudkan sebagai shared view yang berbeza; kesemuanya bersambung ke data service dan facade yang sama, cuma menggabungkan komponen core ke dalam susunan berbeza. Demo awam hanya mempunyai satu shared view untuk memastikan kesederhanaan, manakala penstrukturan semula di production memperkenalkan 26 shared view.
theme leaf:Satu Nod Daun untuk Setiap Tema#
Setiap tema ialah komponen yang sangat nipis, dan ia hanya melakukan satu perkara: memilih satu set konfigurasi rupa yang lengkap dan menyerahkannya kepada 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;
}
Tiada sebarang syarat perniagaan di dalamnya. Dalam production, satu theme leaf lengkap yang representatif mempunyai 23 baris selepas distrukturkan semula, sedangkan laluan yang sama dalam kod lama mencecah ratusan baris.
selector:Menyenaraikan Ahli Famili Secara Jelas#
selector merender leaf yang sepadan berdasarkan id tema runtime:
@switch (theme()) {
@case ('aurora') { <case-study-theme-aurora /> }
@case ('summit') { <case-study-theme-summit /> }
@default { <case-study-theme-default /> }
}
Ia kelihatan biasa sahaja, namun kekuatannya terletak pada sifatnya yang "eksplisit": tema apa yang wujud, dan leaf mana yang dipadankan dengan setiap tema dapat dilihat secara sepintas lalu, serta boleh diperiksa satu demi satu oleh ujian automatik.
Biarkan Pengkompil Memeriksa Kehadiran untuk Anda#
Apabila bilangan tema bertambah, perkara yang paling ditakuti ialah keciciran. Tema baharu ditambah tetapi terlupa melengkapkan konfigurasi; format konfigurasi diubah tetapi beberapa tema lama tertinggal.
Penyelesaiannya adalah dengan menetapkan senarai tema sebagai punca kebenaran tunggal, kemudian menggunakan satisfies dalam TypeScript untuk mewajibkan setiap tema mempunyai konfigurasi yang lengkap:
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>;
Jika DEMO_THEMES ditambah satu tema tetapi THEME_APPEARANCES tidak dilengkapi, kompilasi akan gagal serta-merta. Perkara yang sama berlaku jika terdapat kesalahan ejaan medan atau format laluan CSS yang tidak sah.
Kelebihan satisfies berbanding penetapan jenis secara langsung ialah: ia mengesahkan bentuk tetapi mengekalkan jenis konkrit bagi setiap nilai, supaya apabila mengakses THEME_APPEARANCES.aurora kemudiannya, jenisnya tidak dilonggarkan menjadi ThemeAppearance umum.
Biarkan Ujian Melindungi Keseluruhan Famili#
Pengkompil boleh mengawal konfigurasi, tetapi tidak dapat memastikan sama ada "tema ini benar-benar boleh dirender". Bahagian ini diserahkan kepada ujian integriti famili, yang menyenaraikan setiap tema yang telah diisytiharkan satu demi satu:
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();
});
Di bahagian konfigurasi juga terdapat ujian yang sepadan: medan id bagi setiap konfigurasi tema mesti sepadan dengan kuncinya dalam jadual konfigurasi; laluan CSS juga mesti mematuhi peraturan famili tema.
Gabungan kedua-dua jenis pemeriksaan ini menukar kerja menyemak sama ada "tema sekeluarga ada yang tercicir" secara manual kepada ralat yang disekat secara automatik oleh CI. Pada skala melebihi seratus tema, mata manusia tidak mungkin dapat memeriksa semuanya.
Hanya Muat Turun CSS Tema yang Sedang Digunakan#
Pemisahan lapisan menyelesaikan masalah penyelenggaraan, namun pelbagai tema masih mempunyai satu kos yang memberi kesan langsung kepada pengguna: CSS.
Pendekatan asal adalah dengan membundelkan kesemua 136 stylesheet tema bersama-sama; pengguna terpaksa memuat turun dan menghurai kesemuanya tanpa mengira tema mana yang digunakan. Selepas beralih kepada pemuatan atas permintaan, saiz sumber untuk satu tema berkurangan daripada kira-kira 2.4 MB kepada 17 KB, iaitu pengurangan sekitar 99%.
Mekanismenya sendiri sangat mudah: setiap konfigurasi tema mengisytiharkan stylesheetHref miliknya, dan hanya satu pemuat tunggal yang menguruskan teg <link> tema tersebut pada halaman:
@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 memanggilnya apabila berlaku pertukaran tema:
effect(() => {
const appearance = THEME_APPEARANCES[this.selectedTheme()];
this.#stylesheetLoader.load(appearance.stylesheetHref);
});
"Tunggal" ialah kuncinya. Jika setiap tempat memasukkan <link> sendiri, pertukaran berulang kali akan meninggalkan sisa gaya daripada tema lama, atau dua fail CSS akan saling menimpa. Ujian akan memastikan bahawa selepas bertukar tema beberapa kali, hanya satu stylesheet terurus yang kekal pada halaman.
CSS tema itu sendiri hanya mentakrifkan design token (pemboleh ubah CSS), manakala gaya pada core hanya membaca pemboleh ubah ini:
:root {
--demo-surface: #fffaf1;
--demo-text: #3b2620;
--demo-accent: #c65367;
--demo-radius: 30px;
}
Oleh itu, menukar tema hanyalah menukar satu jadual pemboleh ubah tanpa perlu menyentuh gaya core sama sekali. Mod cerah dan gelap pula merupakan satu lagi set pemboleh ubah dalam fail yang sama.
Kompromi#
Reka bentuk ini bukan percuma tanpa kompromi:
- Fail bertambah banyak. Satu leaf untuk setiap tema, satu view untuk setiap reka letak—bilangan fail kecil akan meningkat. Sebagai ganti, tanggungjawab setiap fail jelas kelihatan sepintas lalu, dan integritinya boleh diperiksa.
- Konfigurasi tidak boleh bertukar menjadi bahasa pengaturcaraan. config sesuai untuk menerangkan penampilan visual dan perbezaan yang bersempadan. Sebaik sahaja anda mula menulis syarat dan aliran logik di dalam konfigurasi, anda sebenarnya sekadar menyembunyikan if/else di tempat lain.
- core memerlukan ujian kontrak yang lebih mantap. Ia dikongsi oleh semua tema, jadi satu perubahan sahaja boleh memberi kesan kepada lebih seratus tema.
- Pertukaran CSS semasa runtime memerlukan pemilik tunggal. Permintaan stylesheet tanpa penyelarasan akan menyebabkan kelipan atau kesan sisa gaya lama, dan inilah sebabnya loader mesti bersifat tunggal.
- Caching tetap penting. Walaupun hanya memuatkan satu tema, nama fail yang stabil dan cache header masih menentukan kepantasan semasa lawatan ulangan pengguna.
Bila Pendekatan Ini Berbaloi Digunakan#
Jika produk anda hanya mempunyai dua mod iaitu cerah dan gelap, pemboleh ubah CSS sudah memadai dan anda tidak memerlukan keseluruhan seni bina ini.
Namun apabila anda mula menghadapi situasi berikut, ia wajar dipertimbangkan:
- Bilangan tema atau versi pelanggan melebihi sepuluh dan terus bertambah.
- Perbezaan antara tema bukan sekadar warna, tetapi juga susun atur, togol ciri, dan teks paparan.
- Mengubah keperluan untuk satu tema kerap kali memberi kesan tidak dijangka kepada tema lain.
- Apabila menambah tema baharu, tiada siapa yang yakin bahagian mana yang perlu dilengkapkan.
Batasan Bukti#
Skop bagi angka-angka production yang dinyatakan dalam artikel ini perlu dijelaskan dengan telus:
- 23 baris ialah saiz satu theme leaf lengkap yang representatif selepas penstrukturan semula, dan ini tidak bermakna setiap leaf atau setiap fail berukuran 23 baris.
- Migrasi tersebut memperkenalkan 70 modul seni bina baharu (24 facade, 10 data service, 10 theme config, 26 shared view). Ini menggambarkan skop transformasi, bukannya hasil pengurangan saiz kod atau peningkatan prestasi semata-mata.
- 2.4 MB → 17 KB adalah hasil pengukuran sebenar dalam production. Tiga fail CSS kecil dalam Demo awam tidak akan menghasilkan semula angka ini.
- Dalam projek kualiti yang sama, skor Lighthouse Performance untuk halaman ujian utama meningkat daripada 50+ kepada 90+, tetapi itu adalah gabungan pelbagai penambahbaikan dan bukan disumbangkan oleh pemuatan CSS atas permintaan sahaja.
Repo awam ini membuktikan mekanisme seni bina itu sendiri: kontrak ditaip, pertukaran semasa runtime, ujian integriti famili, CSS atas permintaan, serta CI yang menjalankan semua pemeriksaan ini. Jadual perbandingan tuntutan yang lengkap boleh didapati dalam Audit Batasan Bukti di repo tersebut.
Jika anda ingin melihat kesannya secara langsung, anda boleh membuka Demo dalam talian untuk menukar antara ketiga-tiga tema, bahasa serta mod cerah/gelap, dan membandingkannya dengan kod sumber di GitHub.