Kihagyás

Preact kliens

Projekt létrehozása

Nyissunk meg egy tetszőleges mappát (pl. a szerver projekt mellett) a projektnek VS Code-ban, majd a Terminalban (Ctrl+Ö) adjuk ki az alábbi parancsot:

npm init preact

A parancs kiadása után több kérdésre kell válaszolnunk. Projektkönvytárnak (Project directory) válasszunk egy hangzatos nevet (pl. twitter), a projekt nyelvének (Project language) pedig a TypeScript-et. A többi kérdésre nyomjunk ENTER-t, tehát automatikusan nemmel válaszoljunk.

A parancs lefutása után navigáljunk az újonnan létrehozott mappába a Terminal-ban:

cd twitter

Ezután telepítsük a függőségeket a következő parancs kiadásával:

npm install

Az utolsó önálló feladatnál a Bootstrap-re is szükségünk lesz, ezt akár telepíthetjük most is:

npm install bootstrap

Nézzük meg, hogy elindul-e az alkalmazás:

npm run dev

Ha valamilyen hibába futunk a fenti parancsok lefutása során, győződjünk meg róla, hogy friss node.js-t (és npm-et) használunk a node -v és npm --version parancsok kiadásával. Amennyiben nem frissek, próbáljuk meg a node.js-t és / vagy az npm-et (npm install -g npm@latest parancs) frissíteni.

Az alkalmazás alapértelmezetten (ahogy azt a konzolon is olvashatjuk) a http://localhost:5173 címen fut. Hasonlóan a korábbiakhoz, a legtöbb esetben itt sem kell újraindítanunk a futást ha változtatunk a forráskódon. A háttérben a fordító automatikusan figyeli ezeket a változásokat. Indítás után az alábbihoz hasonlót kell, hogy lássunk:

VITE v6.3.5  ready in 555 ms

  ➜  Local:   http://localhost:5173/
  ➜  Network: use --host to expose
  ➜  press h + enter to show help

Opcionálisan a package.json fájlban ("start": "vite""dev": "vite") átírhatjuk, hogy ne az npm run dev, hanem az npm start parancs kiadásával fusson az alkalmazásunk. Ez jobban követi az ipari standardeket, de végső soron bármelyiket használhatjuk.

A jegyzőkönyvben válaszoljuk meg a következő kérdéseket:

  • Mi a Vite szerepe a fejlesztési folyamatban?
  • Miért használunk TypeScript-et a JavaScript helyett?

A projekt struktúrája

Vizsgáljuk meg a létrejött projekt tartalmát (csak a releváns mappák és fájlok vannak kiemelve):

  • node_modules: a Preact és függőségei, melyek a kiinduló projekthez kellenek
  • package.json: az alkalmazás függőségeinek listája, ide tudjuk felvenni az npm csomagjainkat függőségként (vagy az npm install [függőség_neve] paranccsal ebbe a fájlba kerülnek be)
  • tsconfig.json: az alkalmazás TypeScript konfigurációja
  • vite.config.ts: a Vite build tool konfigurációja, ezt használjuk a háttérben
  • index.html: a kiinduló HTML fájl, itt tudjuk átállítani a címet és egyéb metaadatokat
  • a div segítségével van összekötve az index.tsx fájllal
  • src: az alkalmazás forráskódja, elsősorban itt fogunk dolgozni
    • assets: ebben a mappában tárolhatjuk a statikus tartalmakat (pl. képek)
    • style.css: a globális CSS fájl, az egész alkalmazásra érvényes szabályokkal - az egyes komponenseknek külön CSS fájlokat is készíthetünk
    • index.tsx: a fő komponens, ami a tartalom megjelenítéséért felelős

Írjuk át az alkalmazás címét az index.html fájlban valami kifejezőbbre!

<title>Preact labor - Twitter</title>
Formázás

A kódot formázni az Alt+Shift+F billentyűkombinációval tudjuk.

A jegyzőkönyvben válaszoljuk meg a következő kérdéseket: * Mi az a komponens Preact környezetben? * Hogyan tudunk egy komponenshez személyre szabott formázást beállítani?

Modell típusok

Hozzuk létre az src/models.ts fájlt a következő tartalommal:

export type Tweet = {
    text : string,
    userName: string,
    tags?: string[]
}

export type TweetWithId = Tweet & {
    id?: string
}

Ezeket a típusokat fogjuk használni a tweetek leírására.

HTTP kommunikáció a backenddel

A HTTP kérések kezelését érdemes külön service osztályba szervezni, hogy: - Központosítsuk az API hívásokat - Egységes hibakezelést valósítsunk meg - Könnyen módosítható legyen az API alapútvonala

Hozzunk létre egy src/services/api.ts fájlt:

import { Tweet, TweetWithId } from '../models';

const API_BASE = '/api';

export class ApiService {
    private async request<T>(endpoint: string, options?: RequestInit): Promise<T> {
        const response = await fetch(`${API_BASE}${endpoint}`, {
            headers: {
                'Content-Type': 'application/json',
                ...options?.headers
            },
            ...options
        });

        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }

        return response.json() as Promise<T>;
    }

    // Tweetek lekérése
    public async getTweets(): Promise<TweetWithId[]> {
        return this.request<TweetWithId[]>('/tweets');
    }

    // Új tweet létrehozása
    public async createTweet(tweet: Omit<Tweet, 'id'>): Promise<void> {
        return this.request<void>('/tweets', {
            method: 'POST',
            body: JSON.stringify(tweet)
        });
    }

    // Tweet törlése
    public async deleteTweet(id: string): Promise<void> {
        return this.request<void>(`/tweets/${id}`, {
            method: 'DELETE'
        });
    }
}

// Singleton példány
export const apiService = new ApiService();

Proxy beállítás

Ahhoz, hogy a beérkező kérések továbbítódjanak a backend felé (amennyiben a kliens nem tudja kiszolgálni azokat), proxyzást kell beállítanunk, vagyis a localhost:3000 felé kell továbbítanunk a /api útvonalú kéréseket. Ezek azok a kérések, amiket az előzőleg létrehozott service osztály kezel. Módosítsuk a vite.config.ts fájl tartalmát az alábbi módon:

import { defineConfig } from 'vite'; import preact from '@preact/preset-vite';

export default defineConfig({
    plugins: [preact()],
    server: {
        proxy: {
            '/api': {
                target: 'http://localhost:3000',
                changeOrigin: true,
                secure: false
            }
        }
    }
});

Tweetek listája

Hozzunk létre egy src/components/TweetsList.tsx komponenst:

import { useState, useEffect } from 'preact/hooks';
import { apiService } from '../services/api';
import { TweetWithId } from '../models';

export function TweetsList() {
    const [tweets, setTweets] = useState<TweetWithId[]>([]);
    const [error, setError] = useState<string | null>(null);

    const loadTweets = async () => {
        try {
            const data = await apiService.getTweets();
            setTweets(data);
            setError(null);
        } catch (err) {
            setError('Hiba történt a tweetek betöltésekor!');
            console.error(err);
        }
    };

    useEffect(() => {
        loadTweets();
    }, []);

    const getTagsString = (tags?: string[]) => (tags || []).join(', ');

    return (
        <div>
            {error && <div class="alert alert-danger">{error}</div>}

            <button onClick={loadTweets} class="btn btn-primary">
                Frissítés
            </button>

            {tweets.length > 0 && (
                <table>
                    <thead>
                        <tr>
                            <th>Id</th>
                            <th>Text</th>
                            <th>Tags</th>
                        </tr>
                    </thead>
                    <tbody>
                        {tweets.map(tweet => (
                            <tr key={tweet.id}>
                                <td>{tweet.id}</td>
                                <td>{tweet.text}</td>
                                <td>{getTagsString(tweet.tags)}</td>
                            </tr>
                        ))}
                    </tbody>
                </table>
            )}
        </div>
    );
}

A fenti komponens a korábban elkészített service segítségével megjeleníti a tweet-eket egy táblázatban, illetve, ha hibába fut, akkor egy hibaüzenetet jelenít meg az oldalon. A Frissítés gomb megnyomásával újból lekérhetjük az aktuális tweeteket a backendtől.

Frissítsük az index.tsx App függvényét, hogy az új komponensünk jelenjen meg az oldal betöltésekor:

export function App() {
    return (
      <div>
          <TweetsList />
      </div>
    );
}

Készítsen képernyőképet működés közben a felületről és illessze be ezt a jegyzőkönyvbe!

A jegyzőkönyvben válaszoljuk meg a következő kérdéseket: * Mi a useState szerepe egy komponensben? * Mi a useEffect szerepe egy komponensben?

Új Tweet hozzáadása

Az előzőekhez nagyon hasonló módon egy újabb oldalt szeretnénk létrehozni, amellyel új tweeteket tudunk majd létrehozni. Hozzunk létre egy src/components/NewTweet.tsx komponenst:

import { useState } from 'preact/hooks';
import { apiService } from '../services/api';

export function NewTweet() {
    const [text, setText] = useState('');
    const [tagsStr, setTagsStr] = useState('');
    const [userName, setUserName] = useState('');

    const send = async () => {
        if (!text || !userName) return;

        apiService.createTweet({
            text,
            userName,
            tags: tagsStr ? tagsStr.split(',') : undefined
        });

        // route('/tweets');
        window.location.href = '/tweets';
    };

    return (
        <div>
            <label>
                Username:
                <input type="text" value={userName}
                    onInput={(e) => setUserName(e.currentTarget.value)} />
            </label>
            <br />
            <label>
                Text:
                <textarea value={text}
                    onInput={(e) => setText(e.currentTarget.value)} />
            </label>
            <br />
            <label>
                Tags:
                <input type="text" value={tagsStr}
                    onInput={(e) => setTagsStr(e.currentTarget.value)} />
            </label>
            <br />
            <button onClick={send}>Küldés</button>
        </div>
    );
}

A komponens egy form és a korábban létrehozott service segítségével támogatja az új tweet létrehozását. Új tweet felvétele után a komponens automatikusan visszairányít a tweetek listájára. A window.location.href beállítás helyett routing-ot érdemes inkább használni (route függvényhívás, ld. a kikommentezett részt), a következő feladat elvégzése után használjuk is ezt!

Routing

Annak érdekében, hogy az új tweet hozzáadó oldalra szépen el tudjunk navigálni, vezessünk be routing-ot az alkalmazásban! Ehhez először is telepítsük a preact-router csomagot. Előtte érdemes leállítani az alkalmazás futtatását is.

npm install preact-router

Ezután módosítsuk az index.tsx fájlt, felvéve a következő útvonalakat és egy új Header komponenst, mely a navigációt fogja végezni. Az importálásokról se feledkezzünk meg:

export function App() {
    return (
        <div>
            <Header />
            <Router>
                <Route path="/tweets" component={TweetsList} />
                <Route path="/new" component={NewTweet} />
            </Router>
        </div>
    );
}

Ezután hozzunk létre egy src/components/Header.tsx komponenst és importáljuk be az index.tsx fájlba:

import { Link } from 'preact-router';

export function Header() {
    return (
        <header>
            <h1>Twitter</h1>
            <nav>
                <Link href="/tweets">Tweets</Link>
                <Link href="/new">New Tweet</Link>
            </nav>
        </header>
    );
}

A végén ne felejtsük el a végén átírni a NewTweet.tsx komponens kódját, hogy az újonnan bevezetett routing-ot használja az automatikus visszairányításnál!

Készítsen képernyőképet működés közben az új tweet hozzáadásának felületéről, valamint a tweetek listájában látható új tweetről és illessze be ezeket a jegyzőkönyvbe!

Önálló feladatok

Bootstrap stílusozás

A Bootstrap keretrendszer segítségével adjon hozzá néhány stílust az oldalhoz, hogy szebben nézzen ki. Különösen ügyeljen a Header komponens formázására, itt használja a navbar, nav-link stb. Bootstrap osztályokat! Ha korábban nem tette meg, előbb telepítse a bootstrap csomagot:

npm install bootstrap

Illesszen be a tweets és az új tweet oldalakról egy-egy képernyőképet a jegyzőkönyvbe!

Tweetek törlése

Egészítse ki az oldalt úgy, hogy a tweetek listájában minden tweet mellett megjelenjen egy törlés gomb is, amely megnyomására kitöröljük az adott tweetet.

Az idevágó kódrészleteket és egy képernyőképet illesszen be a jegyzőkönybe!


2025-09-02 Szerzők