Szerver oldali renderelés (SSR) Nuxt.js nélkül, tisztán Vue.js-szel

A webfejlesztés világában a felhasználói élmény és a keresőoptimalizálás (SEO) kulcsfontosságú szempontok. A modern JavaScript keretrendszerek, mint a Vue.js, fantasztikus interaktív élményt nyújtanak, de alapvetően kliensoldali renderelésre (CSR) épülnek. Ez azt jelenti, hogy a böngésző tölti le a kezdeti HTML-t (ami gyakran csak egy üres <div id="app"></div>), majd a JavaScript tölti be az adatokat és építi fel a teljes felhasználói felületet. Ez azonban hátrányokkal járhat a SEO szempontjából, mivel a keresőrobotok nehezebben indexelhetik a dinamikusan generált tartalmat, és a kezdeti betöltési idő is lassabbnak tűnhet a felhasználók számára.

Itt jön képbe a szerver oldali renderelés (SSR). Az SSR lehetővé teszi, hogy az alkalmazás kezdeti állapotát a szerveren állítsuk elő, és a kliens már egy teljesen renderelt HTML-t kapjon meg. Ez számos előnnyel jár, de sokan azonnal a Nuxt.js keretrendszerhez nyúlnak, ami valóban kiválóan kezeli az SSR komplexitását. De mi van akkor, ha nem akarjuk a Nuxt.js által nyújtott összes funkciót, és teljes kontrollra vágyunk a projektünk felett? Mi van, ha meg akarjuk érteni az SSR alapjait mélyebben, vagy egy már meglévő Vue.js projekthez akarjuk hozzáadni az SSR-t anélkül, hogy egy teljesen új keretrendszert integrálnánk? Ebben a cikkben pontosan ezt fogjuk körüljárni: hogyan valósítható meg a szerver oldali renderelés tisztán Vue.js-szel, Nuxt.js nélkül.

Az SSR alapjai Vue.js-szel: Hogyan működik a gépezet?

Az SSR lényege, hogy a Vue.js alkalmazásunk kódja nem csak a böngészőben, hanem egy Node.js környezetben, a szerveren is futni képes legyen. Amikor egy felhasználó első alkalommal látogatja meg az oldalunkat, a szerver a Vue.js alkalmazást HTML stringgé rendereli. Ezt a HTML stringet aztán elküldi a böngészőnek a szükséges JavaScript és CSS fájlokkal együtt.

A böngésző megkapja a teljes HTML-t, azonnal megjeleníti azt, így a felhasználó sokkal gyorsabbnak érzékeli a betöltést. Amikor a JavaScript kód is letöltődik és elindul, átveszi az irányítást. Ezt a folyamatot hívjuk hydration-nek (hidráció). A hydration során a kliensoldali Vue.js alkalmazás „összekapcsolódik” a szerverről kapott HTML-lel, hozzárendeli az eseménykezelőket, és kezeli a dinamikus frissítéseket, anélkül, hogy újrarenderelné az oldalt. Ha ez a folyamat nem történne meg, az oldal statikus maradna, interakció nélkül.

A fő kihívás az, hogy a kódot úgy kell megírni, hogy az egyaránt működjön a szerveroldali (Node.js) és a kliensoldali (böngésző) környezetben. Ezt nevezzük univerzális vagy izomorf kódnak.

Kihívások és Megoldások: Amit tudnod kell a tiszta Vue.js SSR-ről

A Nuxt.js rengeteg beépített megoldást kínál ezekre a kihívásokra. Tiszta Vue.js-szel nekünk kell gondoskodnunk róluk:

1. Univerzális (izomorf) kód

A kódunknak el kell döntenie, hogy hol fut éppen. A szerveroldali környezetben például nincsenek böngészőspecifikus globális objektumok (pl. window, document). Használhatunk környezetfüggő ellenőrzéseket (pl. typeof window !== 'undefined') vagy külső könyvtárakat, amelyek absztrahálják ezeket a különbségeket. Fontos, hogy a kliensoldali kód csak a kliensoldalon fusson le, és ne próbáljon meg a szerveren böngésző API-kat meghívni.

2. Adatbetöltés (Data Fetching)

Az adatok betöltését a szerveroldali renderelés előtt meg kell tenni, hogy a HTML már tartalmazza a releváns információkat. A Vue.js Composition API-ban az onServerPrefetch életciklus horog (lifecycle hook) tökéletes erre a célra. Ez a hook csak a szerveren fut le, és lehetővé teszi aszinkron adatok lekérdezését, mielőtt a komponens renderelődne. A lekérdezett adatokat aztán a komponens állapotába menthetjük, ahonnan a szerveroldali renderelő eléri, és beépíti a generált HTML-be. Ez a kulcsa annak, hogy a kliensoldalon ne legyen látható „flash of unstyled content” vagy üres adatok. A legacy Options API esetén egy custom asyncData szerű függvényt kell implementálnunk.

3. Állapotkezelés (State Management)

Ha Vuex-et vagy Pinia-t használunk, az állapotot a szerveroldalon kell inicializálni és feltölteni az előre betöltött adatokkal. Ezután ezt az állapotot szerializálni kell JSON formátumba, és be kell injektálni a generált HTML-be egy globális változóként (pl. window.__INITIAL_STATE__). A kliensoldalon, a hydration előtt, a Vuex/Pinia store-t ezzel a kezdeti állapottal kell feltölteni. Így a kliensoldali app azonnal tudja, milyen adatokkal dolgozzon, elkerülve az újabb adatlekérést.

4. Útválasztás (Routing)

A Vue Router-t is konfigurálni kell, hogy a szerveren is működjön. Amikor egy kérés érkezik a szerverre egy adott URL-re, a szerveroldali Vue Router-nek meg kell találnia a megfelelő komponenst, és le kell futtatnia annak adatbetöltő logikáját. Ehhez általában a router.push() metódussal kell beállítani az aktuális URL-t, majd meg kell várni a router.isReady() hívást, ami biztosítja, hogy minden aszinkron útvonal-komponens betöltődött.

5. Build Folyamat

Ez az egyik legösszetettebb rész. Két különálló bundle-t kell létrehoznunk:

  • Kliensoldali bundle: Ez a szokásos böngészőben futó JavaScript kódunk, amely a hydration-ért felel.
  • Szerveroldali bundle: Ez egy Node.js környezetben futó kód, amely a Vue.js alkalmazást HTML stringgé rendereli. Ennek a bundle-nek nem szabad tartalmaznia böngészőspecifikus függőségeket, és a külső modulokat általában nem is kell belefordítani (externals).

A build eszközök, mint a Vite vagy a Webpack, konfigurálhatók erre a két kimenetre, speciális pluginok (pl. vite-plugin-ssr vagy saját Webpack konfiguráció) segítségével.

A gyakorlati megvalósítás lépései (áttekintés)

Lássuk, milyen főbb lépésekből áll egy tiszta Vue.js SSR projekt felépítése:

1. Projekt inicializálás

Hozzuk létre az alap Vue.js projektünket. A Vite rendkívül gyors fejlesztési élményt nyújt, és egyszerűbbé teszi az SSR konfigurálását, mint a Webpack. Például:

npm init vue@latest
cd my-ssr-app
npm install

2. Belépési pontok (Entry Points)

Szükségünk lesz két fő belépési pontra az alkalmazásunkhoz:

  • src/entry-client.js: Ez a fájl felelős a kliensoldali alkalmazás indításáért és a hydration-ért.
  • src/entry-server.js: Ez a fájl exportál egy függvényt, amely a szerveroldali renderelést végzi.

3. Az univerzális Vue app létrehozása

Létrehozunk egy src/main.js (vagy src/app.js) fájlt, ami egy függvényt exportál, amely visszaadja a Vue.js app, a Vue Router és az állapotkezelő (pl. Vuex) példányait. Ez a függvény lesz az, amit mind a kliens-, mind a szerveroldalon meghívunk.

// src/app.js
import { createApp } from 'vue'
import App from './App.vue'
import { createRouter } from './router'
import { createStore } from './store'

export function createSSRApp() {
  const app = createApp(App)
  const router = createRouter()
  const store = createStore()

  app.use(router)
  app.use(store)

  return { app, router, store }
}

4. Kliensoldali belépési pont (entry-client.js)

Ez a fájl hidratálja az alkalmazást, és mountolja a DOM-hoz.

// src/entry-client.js
import { createSSRApp } from './app'

const { app, router, store } = createSSRApp()

if (window.__INITIAL_STATE__) {
  store.replaceState(window.__INITIAL_STATE__)
}

router.isReady().then(() => {
  app.mount('#app')
})

5. Szerveroldali belépési pont (entry-server.js)

Ez a fájl rendereli az alkalmazást HTML stringgé a vue/server-renderer segítségével.

// src/entry-server.js
import { renderToString } from 'vue/server-renderer'
import { createSSRApp } from './app'

export async function render(url, manifest) {
  const { app, router, store } = createSSRApp()

  router.push(url)
  await router.isReady()

  // Adatbetöltés a szerveroldalon (pl. onServerPrefetch)
  // ...

  const appHtml = await renderToString(app)
  const initialState = JSON.stringify(store.state)

  return { appHtml, initialState }
}

6. Node.js szerver felállítása

Egy Express vagy Koa szerver fogadja a bejövő kéréseket. Ez a szerver betölti a szerveroldali bundle-t, meghívja a renderelő függvényt, és beilleszti a generált HTML-t egy alap sablonba.

// server.js (példa Express-szel)
const express = require('express')
const { render } = require('./dist/server/entry-server.js') // Betöltjük a szerveroldali bundle-t
const fs = require('node:fs/promises')
const path = require('node:path')

const app = express()
const resolve = (p) => path.resolve(__dirname, p)

// Serve static assets
app.use(express.static(resolve('dist/client')))

app.get('*', async (req, res) => {
  try {
    const template = await fs.readFile(resolve('dist/client/index.html'), 'utf-8')
    const { appHtml, initialState } = await render(req.url)

    const html = template
      .replace('', appHtml)
      .replace('', `window.__INITIAL_STATE__=${initialState}`)

    res.status(200).set({ 'Content-Type': 'text/html' }).end(html)
  } catch (e) {
    console.error(e)
    res.status(500).end(e.message)
  }
})

app.listen(3000, () => {
  console.log('Server running on http://localhost:3000')
})

7. Build konfiguráció

A Vite (vagy Webpack) konfigurációját úgy kell módosítani, hogy két különálló build-et hozzon létre: egy kliensoldalit és egy szerveroldalit. A szerveroldali build-hez a target: 'node' beállítást kell használni, és konfigurálni kell az externals opciót, hogy a node_modules-ban található függőségeket ne csomagolja be, hanem futásidőben töltse be a Node.js.

// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    ssr: 'src/entry-server.js', // szerveroldali belépési pont
    outDir: 'dist/server', // szerveroldali kimenet
    rollupOptions: {
      input: {
        client: 'src/entry-client.js',
        server: 'src/entry-server.js'
      }
    }
  }
})

Természetesen ez csak egy leegyszerűsített példa, a valóságban sokkal összetettebb konfigurációra van szükség, beleértve a manifest fájlok generálását a kliensoldali assetek helyes hivatkozásához.

8. Stílusok és egyéb assetek kezelése

A CSS és egyéb assetek kezelése is fontos. A szerveroldali rendereléskor a stílusokat általában bele kell injektálni a HTML-be (inline vagy <link> tag-ként), hogy a kezdeti renderelés stílusos legyen. A build eszközök általában támogatják ezt a folyamatot.

Előnyök és Hátrányok mérlegelése: Mikor éri meg a tiszta Vue.js SSR?

Előnyök:

  • Teljes kontroll: Nincs keretrendszer, ami korlátozna. Pontosan tudod, hogyan működik minden.
  • Mélyebb megértés: Az SSR alapjainak megismerése elengedhetetlen a haladó webfejlesztéshez.
  • Kisebb függőségi fa: Nincs szükség a Nuxt.js összes függőségére, ami kisebb csomagméretet és gyorsabb build időt eredményezhet.
  • Optimalizálási lehetőségek: Személyre szabott optimalizációk a build folyamatban és a szerveroldalon.
  • Integráció meglévő projektekkel: Könnyebb lehet egy meglévő, egyszerű Vue.js projektbe bevezetni az SSR-t, mint átállni a Nuxt.js-re.

Hátrányok:

  • Komplexebb beállítás: Sok manuális konfigurációra van szükség, különösen a build folyamat és a szerveroldali logikák terén.
  • Több manuális munka: Azok a funkciók, amiket a Nuxt.js automatikusan kezel (pl. router konfiguráció, meta tag-ek, adatbetöltés életciklusok), manuálisan kell implementálni.
  • Hosszabb fejlesztési idő: A kezdeti beállítás és a hibakeresés időigényesebb lehet.
  • Karbantartási terhek: A rendszer fenntartása és frissítése több erőfeszítést igényel.
  • Kisebb közösségi támogatás: Kevesebb kész példa és megoldás áll rendelkezésre, mint a Nuxt.js esetében.

Következtetés: A szabadság ára és jutalma

A szerver oldali renderelés (SSR) tisztán Vue.js-szel, Nuxt.js nélkül, egy mélyreható és tanulságos utazás a webfejlesztés komplex világába. Bár jelentős erőfeszítést és mély technikai ismereteket igényel, a jutalom a teljes kontroll, a szabadság, és az SSR alapjainak alapos megértése.

Ez a megközelítés ideális azoknak a fejlesztőknek, akik:

  • Saját egyedi igényeik vannak, és nem szeretnék, ha egy keretrendszer korlátozná őket.
  • Szeretnék mélyebben megismerni az SSR működését.
  • Már létező, kisebb Vue.js alkalmazásukat szeretnék SSR képességekkel felruházni anélkül, hogy áttérnének egy nagyobb keretrendszerre.

Azonban, ha a sebesség és az egyszerűség a legfontosabb, és nem bánja, ha egy keretrendszer bizonyos döntéseket hoz Ön helyett, akkor a Nuxt.js valószínűleg a jobb választás. Ez a cikk azonban bebizonyította, hogy a tiszta Vue.js SSR nem csak lehetséges, hanem egy értékes tudásbázis is, ami bármelyik fejlesztő számára hasznos lehet.

Leave a Reply

Az e-mail címet nem tesszük közzé. A kötelező mezőket * karakterrel jelöltük