14. modulLayers és white-label
Nuxt for Devs · 14. modul

Layers: kód-újrafelhasználás és white-label

A Nuxt egyik legalulértékeltebb képessége: egy projekt kiterjeszthet egy másikat, és fájlonként felül tudja írni. A modul második fele a te esetedről szól — tenant- vagy márkaspecifikus arculat és funkció egy közös alap fölött —, és arról a döntésről, ami itt tényleg számít: build-időben vagy futásidőben különböznek a tenantjaid?

14.1Mi az a layer?

Egy layer maga is egy Nuxt-projekt: van app/-ja, server/-e, nuxt.config.ts-e. A különbség csak annyi, hogy nem önmagában futtatod, hanem egy másik projekt kiterjeszti:

// a márka-projekt nuxt.config.ts-e
export default defineNuxtConfig({
  extends: [
    '../base',                        // lokális könyvtár
    '@acme/nuxt-base',                 // npm-csomag
    'github:acme/nuxt-base#v2',        // távoli git (giget)
  ],
})

Innentől a base layer minden oldala, komponense, composable-je és server route-ja úgy viselkedik, mintha a te projektedben lenne — de bármelyiket felülírhatod azzal, hogy létrehozod ugyanazt a fájlt nálad.

Layer vagy modul?

Modul (13. modul)Layer
Mit oszt megViselkedést — programozottanFájlokat — konvenció szerint
HogyandefineNuxtModule, addServerHandlerSima Nuxt-projektszerkezet
Felülírható-eCsak amit a modul kiajánl (options)Fájlonként, bármi
Mire jóMiddleware, composable, konfigurációs konvencióOldalak, layoutok, arculat, teljes al-alkalmazás

14.2A feloldási sorrend — a legfontosabb szabály

Amikor több helyen létezik ugyanaz a fájl, ez a prioritási sorrend dönt (fentről lefelé csökkenő erősséggel):

1 · A SAJÁT PROJEKTED FÁJLJAI — mindig nyernek app/, server/, bármi a gyökérben 2 · ~~/layers/* — automatikusan betöltve, ÁBÉCÉ-SORRENDBEN, Z > A layers/zeta felülírja a layers/alpha-t — ezért szokás számmal prefixálni 3 · extends: [...] — az ELSŐ elem a legerősebb extends: ['../brand', '../base'] → a brand felülírja a base-t Konfliktus esetén a feljebb lévő nyer — de csak fájl-szinten. A konfig-értékek MERGE-elődnek.
Két külön mechanizmus: a fájlok felülíródnak, a konfigurációs értékek összeolvadnak.
A leggyakoribb meglepetés az extends sorrendje. Ösztönösen azt gondolnád, hogy a lista végén lévő nyer (mint egy CSS-import-láncnál). Nem: az első elem a legerősebb. Ha a ['../base', '../brand'] sorrendet írod, akkor a base írja felül a brandet — vagyis pont fordítva, mint ahogy szeretnéd. A helyes: ['../brand', '../base'].

Mi olvad össze és mi íródik felül

Viselkedés
KomponensekFelülírás azonos név alapján — a Button.vue a te projektedben nyer
OldalakA route-ok összeadódnak; azonos útvonalnál a magasabb prioritású nyer
Composable-ök, utilokFelülírás azonos fájlnév alapján
Server route-okFelülírás azonos útvonal alapján
modules: [], css: []Összeolvadnak (a listák összefűződnek)
runtimeConfigMélységi merge — a magasabb prioritású kulcsai nyernek
routeRulesMerge, mintánként

Hivatkozás a layerre

// a layerek srcDir-jére automatikus alias készül
import BaseButton from '#layers/base/components/Button.vue'

// hasznos, ha felülírsz, de az eredetit is használni akarod:
<script setup>
import BaseHeader from '#layers/base/components/AppHeader.vue'
</script>
<template>
  <div class="brand-bar"><BrandLogo /></div>
  <BaseHeader />          <!-- nem másoltuk, csak kiegészítettük -->
</template>

14.3A white-label kérdés — ez a modul lényege

Multitenant SaaS-nál előbb-utóbb jön az igény: „a mi ügyfeleink a saját logónkkal lássák”. A layers erre kézenfekvő eszköznek tűnik — de a legtöbb esetben nem az a helyes válasz. A döntés egyetlen kérdésen múlik:

Build-időben ismered a tenantokat, vagy futásidőben?

Futásidejű témázásLayer tenantonként
Mi különbözikSzínek, logó, szövegek, funkció-kapcsolókOldalak, komponensek, üzleti logika
Honnan jönA tenant-rekordból, kérésenként (4. modul)Build-időben, a layer fájljaiból
Deployok számaEgyTenantonként egy
Új tenant felvételeEgy adatbázis-sorÚj build és deploy
SkálázódásEzer tenantig gond nélkülNéhány tíznél már fáj
Meddig lehet elmenniCSS-változó, logó, feliratok, feature flagBármeddig — más oldalak, más folyamat

A futásidejű út — ezzel indulj

// server/api/bootstrap.get.ts — a tenant publikus arculata (4. modul)
export default defineEventHandler((event) => {
  const t = event.context.tenant
  return t && {
    slug: t.slug, name: t.name, logoUrl: t.logoUrl,
    theme:    t.theme,                // { primary: '#0b7285', radius: '10px', … }
    features: t.features,             // { exportPdf: true, sso: false, … }
  }
})
<!-- app/layouts/default.vue — a téma CSS-változókká válik -->
<script setup lang="ts">
const tenant = useTenant()

const themeVars = computed(() => ({
  '--brand-primary': tenant.value?.theme.primary ?? '#00b96b',
  '--brand-radius':  tenant.value?.theme.radius  ?? '8px',
}))
</script>

<template>
  <div :style="themeVars">
    <AppHeader :logo="tenant?.logoUrl" />
    <slot />
  </div>
</template>
// és a funkció-kapcsolók ugyanabból a forrásból
export const useFeature = (name: FeatureKey) => {
  const tenant = useTenant()
  return computed(() => tenant.value?.features?.[name] === true)
}
Miért ez illik a Cloudflare-hez? Mert így egyetlen Worker szolgálja ki az összes tenantot, és az egyedi domainek a Cloudflare for SaaS custom hostnames-en keresztül érkeznek ugyanoda (4. modul). Egy deploy, egy verzió, egy rollback. Ha tenantonként külön Workert deployolnál, akkor minden hibajavítás N deployt jelentene — és a Cloudflare-anyag 8. moduljának gradual deployment és version-affinity mechanikája N-szeresére bonyolódna.

Mikor van mégis szükség layerre

Akkor, amikor a különbség nem arculati, hanem funkcionális, és nem lehet feature flaggel lefedni:

repo/
├─ layers/
│  └─ base/                 ← minden közös: app/, server/, komponensek
├─ apps/
│  ├─ saas/                 ← a fő, sokbérlős alkalmazás (futásidejű témázással)
│  │  └─ nuxt.config.ts        extends: ['../../layers/base']
│  └─ acme-enterprise/      ← EGY nagy ügyfél saját kiadása
│     ├─ app/pages/onboarding/…   ← felülírja a base-ét
│     └─ nuxt.config.ts        extends: ['../../layers/base']
A gyakorlati szabály: futásidejű témázás a soknak, layer a kevésnek. A tenantjaid 99%-a színt és logót akar — az adatbázisból. Az az egy-két nagy ügyfél, akinek tényleg más az alkalmazása, kaphat saját layert és saját deployt, mert az árában úgyis benne van. Ha fordítva csinálod — layert mindenkinek —, akkor a huszadik tenantnál a CI-d fél óráig fut, és minden hibajavítás húsz deploy.

14.4Amire a layer tényleg jó, tenantoktól függetlenül

14.5Buktatók

BuktatóMi történikMit tegyél
Fordított extends-sorrend A base írja felül a testreszabást Az első elem a legerősebb — a specifikus előre
„Melyik fájl nyer?” Órákig keresed, miért nem érvényesül egy komponens Nuxt DevTools → a komponens forrásának útvonala; illetve .nuxt/components.d.ts
Relatív útvonalak a layerben A layer a saját gyökeréhez képest old fel, nem a tiédhez createResolver(import.meta.url) a layer konfigjában, vagy #layers/… alias
Layer-függőségek A layer node_modules-a nem automatikusan a tiéd npm-csomagként dependencies-ben; monorepóban workspace-szel
Növekvő build-idő Minden layer fájljait be kell olvasni és feloldani Mérd; ne csinálj öt layert három fájlért
Túl korai absztrakció Van egy base layered egyetlen felhasználóval Amíg nincs második fogyasztó, ne emelj ki semmit

14.6Mikor ne layer

Mi jön ezután? A 15. modul a teljesítményről szól — hydration-költség, payload-méret, és a Workers-specifikus rész: a bundle-limit, hogyan mérd (a 11.3-ban látott wrangler check startup kapuval), mi fogyasztja el, és mit tegyél, ha közelítesz hozzá.

14.7Ellenőrizd magad

  1. Mi a különbség egy modul és egy layer között?
    Válasz

    A modul viselkedést oszt meg programozottan (defineNuxtModule, middleware, composable, konfiguráció), és csak azt lehet testre szabni rajta, amit kiajánl. A layer fájlokat oszt meg konvenció szerint — maga is egy Nuxt-projektszerkezet —, és bármelyik fájlját felülírhatod azzal, hogy létrehozod ugyanazt nálad.

  2. extends: ['../base', '../brand'] — miért nem működik ez úgy, ahogy vártad?
    Válasz

    Mert az extends listában az első elem a legerősebb, nem az utolsó. Ebben a sorrendben a base írja felül a brand-et. A helyes sorrend: ['../brand', '../base'] — a specifikus előre. (A saját projekted fájljai amúgy is mindent felülírnak, és a ~~/layers/* ábécésorrendben, Z > A módon rendeződik.)

  3. Mikor válaszd a futásidejű témázást, és mikor a layert?
    Válasz

    Futásidejű témázást, ha a különbség arculati vagy funkció-kapcsolóval lefedhető (szín, logó, feliratok, feature flag) — ez a tenantjaid túlnyomó része, és egyetlen deployjal, adatbázis-sorral kezelhető. Layert akkor, ha a különbség funkcionális: más oldalak, más folyamat, más validáció — jellemzően egy-két nagy ügyfélnél. „Futásidejű a soknak, layer a kevésnek.”

  4. Miért rossz ötlet tenantonként külön Workert deployolni?
    Válasz

    Mert minden hibajavítás N deployt jelent, a CI-idő lineárisan nő, és a Cloudflare gradual deployment / version-affinity mechanikája N-szeresére bonyolódik. Egy Worker + Cloudflare for SaaS custom hostnames mellett egy deploy, egy verzió, egy rollback van — a tenant-különbségek pedig futásidőben, a tenant-rekordból jönnek.

  5. Felülírtál egy komponenst a layerből, de az eredeti funkcióját is szeretnéd használni. Hogyan?
    Válasz

    A layer srcDir-jére automatikusan készül alias, tehát importálhatod közvetlenül: import BaseHeader from '#layers/base/components/AppHeader.vue', és a saját komponensedben körbeveheted vagy kiegészítheted. Így nem kell lemásolnod a base-implementációt — ami a felülírások leggyakoribb, később fájdalmas hibája.

  6. Mikor ne csinálj layert?
    Válasz

    Ha csak témázásról van szó (CSS-változó elég), ha csak egy komponens tér el (props/slot), ha a különbség szerveroldali üzleti logika (feature flag vagy stratégia-minta), és főleg: ha még csak egy projekted van. A layer akkor kezd értéket adni, amikor legalább két fogyasztója van — előtte absztrakciót tervezel egyetlen példa alapján.

Előző13. modul — Modul-ökoszisztéma Workers-szűrővel Következő 15. modul — Teljesítmény: hydration, payload, bundle