Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Middleware i przechwytywanie żądań

W sercu Metropolii Quantum znajduje się zaawansowany system kontroli dostępu i przekierowań, znany jako Quantum Gateway. Ten system monitoruje każdą próbę dostępu do różnych stref miasta, weryfikuje uprawnienia, przekierowuje do odpowiednich punktów i modyfikuje trasy podróży w czasie rzeczywistym. To niezwykle ważny element infrastruktury, który zapewnia zarówno bezpieczeństwo, jak i optymalizację ruchu w całym mieście.

W świecie Next.js 15, podobną rolę odgrywa Middleware - potężny mechanizm, który pozwala na przechwytywanie i modyfikowanie żądań HTTP zanim zostaną one obsłużone przez stronę lub API, co umożliwia implementację takich funkcji jak autoryzacja, przekierowania, czy modyfikacje odpowiedzi.

Czym jest Middleware w Next.js 15?

Middleware w Next.js to funkcja, która jest wykonywana przed obsługą żądania przez stronę lub API route. Działa ona między żądaniem klienta a odpowiedzią serwera, co daje możliwość:

  • Wykonywania kodu przed załadowaniem strony
  • Modyfikowania obiektu żądania
  • Modyfikowania obiektu odpowiedzi
  • Przekierowywania użytkownika
  • Odrzucania żądań
  • Dodawania nagłówków HTTP
  • Modyfikowania cookies

Podstawowa struktura Middleware

W Next.js 15, middleware jest definiowane w pliku

middleware.ts
(lub
middleware.js
) w katalogu głównym projektu (obok
app
lub
pages
).

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Funkcja middleware jest wykonywana przed każdym żądaniem
6export function middleware(request: NextRequest) {
7  // Tutaj możemy zaimplementować naszą logikę
8  console.log('Middleware uruchomiony dla:', request.nextUrl.pathname);
9
10  // Możemy zmodyfikować obiekt odpowiedzi
11  return NextResponse.next();
12}
13
14// Opcjonalnie możemy ograniczyć, dla których tras middleware będzie uruchamiane
15export const config = {
16  matcher: '/api/:path*',
17};

Dostępne operacje w Middleware

1. Przekierowania i rewrytowanie

Podobnie jak w Metropolii Quantum, gdzie bramy bezpieczeństwa mogą automatycznie przekierować nieautoryzowanych gości do punktów kontrolnych, middleware w Next.js pozwala na przekierowanie żądań:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  // Sprawdzenie, czy użytkownik próbuje dostać się do strefy administracyjnej
7  if (request.nextUrl.pathname.startsWith('/admin')) {
8    // Sprawdzamy, czy użytkownik jest zalogowany (np. sprawdzając cookie)
9    const isLoggedIn = request.cookies.has('auth-token');
10    
11    if (!isLoggedIn) {
12      // Przekierowanie niezalogowanych użytkowników do strony logowania
13      const loginUrl = new URL('/login', request.url);
14      // Dodajemy oryginalny URL jako parametr, aby móc wrócić po zalogowaniu
15      loginUrl.searchParams.set('from', request.nextUrl.pathname);
16      return NextResponse.redirect(loginUrl);
17    }
18  }
19  
20  // W przeciwnym razie kontynuujemy normalne przetwarzanie żądania
21  return NextResponse.next();
22}

2. Modyfikowanie nagłówków żądania lub odpowiedzi

Możemy również modyfikować nagłówki, podobnie jak Quantum Gateway może dodawać specjalne identyfikatory do biletów transportowych:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  // Pobieramy oryginalną odpowiedź
7  const response = NextResponse.next();
8  
9  // Dodajemy własne nagłówki do odpowiedzi
10  response.headers.set('x-quantum-security', 'enabled');
11  response.headers.set('x-quantum-gateway-version', '15.0.1');
12  
13  // Dodanie polityki bezpieczeństwa treści (CSP)
14  response.headers.set(
15    'Content-Security-Policy',
16    "default-src 'self'; script-src 'self' 'unsafe-eval' 'unsafe-inline' quantum-cdn.example.com;"
17  );
18  
19  return response;
20}

3. Manipulowanie cookies

Middleware może również manipulować cookies, co przydaje się przy zarządzaniu sesjami:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  const response = NextResponse.next();
7  
8  // Odczyt wartości cookie
9  const theme = request.cookies.get('theme')?.value;
10  
11  // Ustawienie nowego cookie
12  response.cookies.set('last-visit', new Date().toISOString());
13  
14  // Zmiana wartości istniejącego cookie
15  if (theme === 'dark') {
16    response.cookies.set('theme-version', 'dark-v2');
17  }
18  
19  // Usunięcie cookie
20  if (request.nextUrl.pathname === '/logout') {
21    response.cookies.delete('auth-token');
22  }
23  
24  return response;
25}

4. Rewrite - podmiana URL bez zmiany widocznego adresu

Rewrite pozwala na podmianę adresu URL wewnętrznie, bez zmiany adresu widocznego dla użytkownika:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  // Przykład: wewnętrzne przekierowanie wszystkich żądań z /docs do /documentation
7  // Adres w pasku przeglądarki pozostaje /docs
8  if (request.nextUrl.pathname.startsWith('/docs')) {
9    const url = request.nextUrl.clone();
10    url.pathname = url.pathname.replace(/^/docs/, '/documentation');
11    return NextResponse.rewrite(url);
12  }
13  
14  return NextResponse.next();
15}

Implementacja Middleware dla aplikacji Quantum Voyages

Przygotujmy kompleksowe middleware dla naszej aplikacji Quantum Voyages, które będzie obsługiwać:

  1. Autoryzację dla stref chronionych
  2. Internacjonalizację i wykrywanie języka
  3. Śledzenie i analitykę
1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { verifyAuthToken } from './lib/auth';
5
6// Lista obsługiwanych języków
7const SUPPORTED_LOCALES = ['pl', 'en', 'de', 'fr'];
8// Domyślny język
9const DEFAULT_LOCALE = 'pl';
10// Ścieżki wymagające autoryzacji
11const PROTECTED_PATHS = ['/dashboard', '/account', '/bookings'];
12// Ścieżki administracyjne wymagające uprawnień admina
13const ADMIN_PATHS = ['/admin'];
14
15export async function middleware(request: NextRequest) {
16  const { pathname } = request.nextUrl;
17  
18  // 1. Obsługa lokalizacji i internacjonalizacji
19  const pathnameHasLocale = SUPPORTED_LOCALES.some(
20    locale => pathname.startsWith(`/\${locale}/`) || pathname === `/\${locale}`
21  );
22  
23  // Jeśli ścieżka nie zawiera jeszcze kodu języka, przekieruj i dodaj kod
24  if (!pathnameHasLocale) {
25    // Próba wykrycia preferowanego języka użytkownika
26    const preferredLocale = getPreferredLocale(request);
27    const url = new URL(request.url);
28    url.pathname = `/\${preferredLocale}\${pathname}`;
29    return NextResponse.redirect(url);
30  }
31  
32  // Wyodrębnienie kodu języka z ścieżki
33  const locale = pathname.split('/')[1];
34  
35  // 2. Autoryzacja dla stref chronionych
36  // Sprawdzenie, czy ścieżka jest chroniona
37  if (isProtectedPath(pathname)) {
38    const authToken = request.cookies.get('auth-token')?.value;
39    
40    // Weryfikacja tokenu autoryzacji
41    const authResult = await verifyAuthToken(authToken);
42    
43    // Jeśli użytkownik nie jest zalogowany lub token jest nieprawidłowy
44    if (!authResult.isValid) {
45      const loginUrl = new URL(`/\${locale}/login`, request.url);
46      loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
47      return NextResponse.redirect(loginUrl);
48    }
49    
50    // Sprawdzenie uprawnień admina dla ścieżek administracyjnych
51    if (isAdminPath(pathname) && !authResult.isAdmin) {
52      const accessDeniedUrl = new URL(`/\${locale}/access-denied`, request.url);
53      return NextResponse.redirect(accessDeniedUrl);
54    }
55  }
56  
57  // 3. Dodanie nagłówków analityki i bezpieczeństwa
58  const response = NextResponse.next();
59  
60  // Dodanie informacji o użytkowniku dla narzędzi analitycznych (nie zawiera danych osobowych)
61  const visitorId = request.cookies.get('visitor-id')?.value || generateVisitorId();
62  response.cookies.set('visitor-id', visitorId, { 
63    httpOnly: true, 
64    sameSite: 'strict',
65    maxAge: 60 * 60 * 24 * 365 // 1 rok
66  });
67  
68  // Nagłówki bezpieczeństwa
69  response.headers.set('X-Frame-Options', 'DENY');
70  response.headers.set('X-Content-Type-Options', 'nosniff');
71  response.headers.set('Referrer-Policy', 'strict-origin-when-cross-origin');
72  
73  return response;
74}
75
76// Funkcje pomocnicze
77
78// Wykrywanie preferowanego języka użytkownika
79function getPreferredLocale(request: NextRequest): string {
80  // Sprawdzenie zapisanego wcześniej preferowanego języka w cookie
81  const savedLocale = request.cookies.get('preferred-locale')?.value;
82  if (savedLocale && SUPPORTED_LOCALES.includes(savedLocale)) {
83    return savedLocale;
84  }
85  
86  // Sprawdzenie nagłówka Accept-Language
87  const acceptLanguage = request.headers.get('accept-language');
88  if (acceptLanguage) {
89    // Analiza nagłówka Accept-Language i wyszukanie najlepszego dopasowania
90    const userLocales = acceptLanguage.split(',')
91      .map(item => {
92        const [locale, priority = 'q=1.0'] = item.trim().split(';');
93        const q = parseFloat(priority.replace('q=', '')) || 1.0;
94        return { locale: locale.split('-')[0], q };
95      })
96      .sort((a, b) => b.q - a.q);
97    
98    // Znajdź pierwszy obsługiwany język
99    for (const { locale } of userLocales) {
100      if (SUPPORTED_LOCALES.includes(locale)) {
101        return locale;
102      }
103    }
104  }
105  
106  // Domyślny język, jeśli nie można określić preferencji
107  return DEFAULT_LOCALE;
108}
109
110// Sprawdzenie, czy ścieżka jest chroniona
111function isProtectedPath(pathname: string): boolean {
112  return PROTECTED_PATHS.some(path => {
113    // Usunięcie kodu języka z ścieżki przed porównaniem
114    const pathWithoutLocale = pathname.split('/').slice(2).join('/');
115    return `/\${pathWithoutLocale}`.startsWith(path);
116  });
117}
118
119// Sprawdzenie, czy ścieżka jest administracyjna
120function isAdminPath(pathname: string): boolean {
121  return ADMIN_PATHS.some(path => {
122    // Usunięcie kodu języka z ścieżki przed porównaniem
123    const pathWithoutLocale = pathname.split('/').slice(2).join('/');
124    return `/\${pathWithoutLocale}`.startsWith(path);
125  });
126}
127
128// Generowanie unikalnego ID dla odwiedzającego
129function generateVisitorId(): string {
130  return `\${Date.now()}-\${Math.random().toString(36).substring(2, 15)}`;
131}
132
133// Określenie, dla których ścieżek middleware będzie uruchamiane
134export const config = {
135  matcher: [
136    // Dopasowanie wszystkich ścieżek
137    '/((?!api|_next/static|_next/image|favicon.ico).*)',
138  ],
139};

Selektywne uruchamianie Middleware z Matcher

Podobnie jak w Metropolii Quantum, gdzie nie każde przemieszczenie wymaga pełnej kontroli bezpieczeństwa, tak w Next.js 15 możemy określić, dla których ścieżek middleware będzie uruchamiane, używając konfiguracji

matcher
:

1// middleware.ts
2export const config = {
3  matcher: [
4    // Ścieżki, dla których middleware będzie uruchamiane
5    '/dashboard/:path*',
6    '/api/:path*',
7    '/((?!_next/static|_next/image|favicon.ico).*)',
8  ],
9};

Dostępne są różne wzorce matcherów:

  1. Dopasowanie dokładne:
    '/about'
  2. Dopasowanie z parametrami:
    '/blog/:path'
  3. Dopasowanie z parametrami catch-all:
    '/blog/:path*'
  4. Dopasowanie grup:
    /dashboard/:path*|/admin/:path*
  5. Dopasowanie z negacją:
    /((?!api|_next/static|_next/image|favicon.ico).*)

Implementacja autoryzacji JWT z Middleware

W Metropolii Quantum, identyfikatory mieszkańców są weryfikowane przy użyciu zaawansowanej technologii kryptograficznej. Zaimplementujmy podobny system autoryzacji JWT w naszej aplikacji:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4import { jwtVerify } from 'jose';
5
6interface UserPayload {
7  id: string;
8  email: string;
9  role: string;
10  exp: number;
11}
12
13export async function middleware(request: NextRequest) {
14  // Sprawdź, czy ścieżka wymaga autoryzacji
15  if (request.nextUrl.pathname.startsWith('/dashboard') || 
16      request.nextUrl.pathname.startsWith('/api/protected')) {
17    
18    // Pobierz token JWT z cookie lub nagłówka Authorization
19    const authToken = request.cookies.get('auth-token')?.value || 
20                     request.headers.get('Authorization')?.split(' ')[1];
21    
22    if (!authToken) {
23      // Brak tokenu - przekierowanie do logowania
24      return redirectToLogin(request);
25    }
26    
27    try {
28      // Weryfikacja tokenu JWT
29      const secretKey = new TextEncoder().encode(process.env.JWT_SECRET!);
30      const { payload } = await jwtVerify<UserPayload>(authToken, secretKey);
31      
32      // Sprawdzenie, czy token nie wygasł
33      const currentTime = Math.floor(Date.now() / 1000);
34      if (payload.exp < currentTime) {
35        // Token wygasł - przekierowanie do logowania
36        return redirectToLogin(request);
37      }
38      
39      // Sprawdzenie uprawnień admina dla ścieżek administracyjnych
40      if (request.nextUrl.pathname.startsWith('/admin') && payload.role !== 'admin') {
41        return NextResponse.json(
42          { message: 'Insufficient permissions' },
43          { status: 403 }
44        );
45      }
46      
47      // Dodanie informacji o użytkowniku do nagłówków dla kolejnych obsług
48      const response = NextResponse.next();
49      response.headers.set('x-user-id', payload.id);
50      response.headers.set('x-user-role', payload.role);
51      
52      return response;
53    } catch (error) {
54      // Błąd weryfikacji tokenu - przekierowanie do logowania
55      return redirectToLogin(request);
56    }
57  }
58  
59  return NextResponse.next();
60}
61
62// Funkcja przekierowująca do logowania
63function redirectToLogin(request: NextRequest) {
64  const loginUrl = new URL('/login', request.url);
65  loginUrl.searchParams.set('returnTo', request.nextUrl.pathname);
66  return NextResponse.redirect(loginUrl);
67}
68
69export const config = {
70  matcher: [
71    '/dashboard/:path*',
72    '/admin/:path*',
73    '/api/protected/:path*',
74  ],
75};

Implementacja przełączania motywów z Middleware

Quantum City oferuje swoim mieszkańcom możliwość dostosowania wyglądu interfejsów miejskich. Zaimplementujmy podobny system przełączania między jasnymi i ciemnymi motywami:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  // Pobierz preferowany motyw
7  const theme = request.cookies.get('theme')?.value || 'light';
8  
9  // Utwórz odpowiedź
10  const response = NextResponse.next();
11  
12  // Ustaw zmienną preferencji motywu jako cookie
13  response.cookies.set('theme', theme, {
14    maxAge: 60 * 60 * 24 * 365, // ważne przez rok
15    path: '/',
16  });
17  
18  // Dodaj niestandardowy nagłówek, który może być odczytany przez klienta
19  response.headers.set('x-theme', theme);
20  
21  return response;
22}
23
24export const config = {
25  matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
26};

Obsługa wielu domen i subdomen z Middleware

W Metropolii Quantum, różne dzielnice mogą mieć swoje własne systemy dostępu. Podobnie w Next.js możemy obsługiwać różne domeny i subdomeny:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  const { pathname, hostname } = request.nextUrl;
7  
8  // Obsługa różnych subdomen
9  if (hostname === 'admin.quantumvoyages.com') {
10    // Przekierowanie wszystkich żądań do panelu administracyjnego
11    const url = new URL('/admin' + pathname, request.url);
12    return NextResponse.rewrite(url);
13  }
14  
15  if (hostname === 'api.quantumvoyages.com') {
16    // Przekierowanie żądań do API
17    const url = new URL('/api' + pathname, request.url);
18    return NextResponse.rewrite(url);
19  }
20  
21  if (hostname.startsWith('user-')) {
22    // Obsługa subdomen użytkowników (np. user-john.quantumvoyages.com)
23    const username = hostname.replace('user-', '').split('.')[0];
24    const url = new URL(`/profile/\${username}\${pathname}`, request.url);
25    return NextResponse.rewrite(url);
26  }
27  
28  return NextResponse.next();
29}
30
31export const config = {
32  matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
33};

Cachowanie i modyfikacja nagłówków odpowiedzi

W Metropolii Quantum, system Gateway optymalizuje przepływ danych przez swoje systemy. Podobnie możemy zoptymalizować naszą aplikację:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  const response = NextResponse.next();
7  
8  // Ustawienie nagłówków cachowania dla zasobów statycznych
9  if (request.nextUrl.pathname.match(/\.(jpg|jpeg|png|webp|svg|gif|ico)$/)) {
10    response.headers.set('Cache-Control', 'public, max-age=31536000, immutable');
11  }
12  
13  // Ustawienie nagłówków cachowania dla stron HTML
14  else if (!request.nextUrl.pathname.startsWith('/api/')) {
15    response.headers.set('Cache-Control', 'public, max-age=300, s-maxage=3600');
16  }
17  
18  // Dodanie nagłówków bezpieczeństwa
19  response.headers.set('Strict-Transport-Security', 'max-age=63072000');
20  response.headers.set('X-Content-Type-Options', 'nosniff');
21  response.headers.set('X-Frame-Options', 'DENY');
22  response.headers.set('X-XSS-Protection', '1; mode=block');
23  
24  return response;
25}
26
27export const config = {
28  matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
29};

Implementacja geolokalizacji i personalizacji treści

Metropolia Quantum dostosowuje dostępne usługi w zależności od położenia mieszkańców. Zaimplementujmy podobny system w naszej aplikacji, używając middleware do określenia lokalizacji użytkownika:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Tabela regionów i ich dostępnych destynacji
6const REGION_DESTINATIONS = {
7  'EU': ['mars', 'europa', 'titan'],
8  'NA': ['mars', 'luna', 'titan', 'ganymede'],
9  'ASIA': ['mars', 'venus-orbiter', 'luna'],
10  'default': ['mars'] // domyślne destynacje dostępne globalnie
11};
12
13export function middleware(request: NextRequest) {
14  // Pobierz region użytkownika (w rzeczywistości można użyć zewnętrznego API do geolokalizacji)
15  const countryCode = request.headers.get('x-country') || 'default';
16  const region = getRegionFromCountry(countryCode);
17  
18  // Utwórz odpowiedź
19  const response = NextResponse.next();
20  
21  // Dodanie informacji o regionie i dostępnych destynacjach jako nagłówki
22  response.headers.set('x-user-region', region);
23  response.headers.set('x-available-destinations', JSON.stringify(REGION_DESTINATIONS[region] || REGION_DESTINATIONS.default));
24  
25  // Ustawienie cookie z regionem (będzie dostępne w przeglądarce)
26  response.cookies.set('user-region', region, {
27    maxAge: 60 * 60 * 24 * 30, // 30 dni
28    path: '/',
29  });
30  
31  return response;
32}
33
34// Funkcja mapująca kody krajów na regiony
35function getRegionFromCountry(countryCode: string): keyof typeof REGION_DESTINATIONS {
36  const EU_COUNTRIES = ['DE', 'FR', 'IT', 'ES', 'PL', 'NL', 'BE', 'SE', 'AT', 'DK'];
37  const NA_COUNTRIES = ['US', 'CA', 'MX'];
38  const ASIA_COUNTRIES = ['JP', 'CN', 'KR', 'IN', 'SG', 'TH', 'MY', 'PH', 'VN'];
39  
40  if (EU_COUNTRIES.includes(countryCode)) return 'EU';
41  if (NA_COUNTRIES.includes(countryCode)) return 'NA';
42  if (ASIA_COUNTRIES.includes(countryCode)) return 'ASIA';
43  
44  return 'default';
45}
46
47export const config = {
48  matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
49};

Ograniczanie liczby żądań (Rate limiting)

W Metropolii Quantum, system Gateway kontroluje ilość zapytań do krytycznych zasobów, aby zapobiec przeciążeniu. Zaimplementujmy podobny mechanizm rate limitingu:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Prosty system rate limitingu oparty na pamięci (w produkcji lepiej użyć Redis lub podobnej usługi)
6const RATE_LIMIT_DURATION = 60 * 1000; // 1 minuta
7const MAX_REQUESTS_PER_MINUTE = 60; // Maksymalna liczba żądań na minutę
8
9// Przechowywanie liczników żądań dla różnych IP (w rzeczywistej aplikacji lepiej użyć Redis)
10const ipRequestCounts = new Map<string, { count: number, resetAt: number }>();
11
12export function middleware(request: NextRequest) {
13  // Ograniczamy tylko żądania API
14  if (request.nextUrl.pathname.startsWith('/api/')) {
15    const ip = request.ip || 'unknown';
16    const now = Date.now();
17    
18    // Pobierz lub utwórz licznik dla tego IP
19    if (!ipRequestCounts.has(ip) || ipRequestCounts.get(ip)!.resetAt < now) {
20      // Zresetuj licznik, jeśli upłynął czas
21      ipRequestCounts.set(ip, { count: 1, resetAt: now + RATE_LIMIT_DURATION });
22    } else {
23      // Zwiększ licznik
24      const record = ipRequestCounts.get(ip)!;
25      record.count++;
26      
27      // Sprawdź, czy przekroczono limit
28      if (record.count > MAX_REQUESTS_PER_MINUTE) {
29        // Oblicz czas, po którym można ponownie wysyłać żądania
30        const resetTime = Math.ceil((record.resetAt - now) / 1000);
31        
32        // Zwróć odpowiedź z kodem 429 Too Many Requests
33        return new NextResponse('Rate limit exceeded', {
34          status: 429,
35          headers: {
36            'Retry-After': `\${resetTime}`,
37            'X-RateLimit-Limit': `\${MAX_REQUESTS_PER_MINUTE}`,
38            'X-RateLimit-Remaining': '0',
39            'X-RateLimit-Reset': `\${resetTime}`,
40          },
41        });
42      }
43    }
44  }
45  
46  return NextResponse.next();
47}
48
49export const config = {
50  matcher: '/api/:path*',
51};

Obsługa A/B testów z Middleware

W Metropolii Quantum, niektóre nowe funkcje są testowane na wybranych grupach mieszkańców. Zaimplementujmy podobny system A/B testów:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5// Definicje testów A/B
6const AB_TESTS = {
7  'new-landing-page': {
8    variants: ['control', 'variant-a', 'variant-b'],
9    weights: [0.5, 0.25, 0.25], // 50% użytkowników zobaczy control, 25% variant-a, 25% variant-b
10  },
11  'checkout-flow': {
12    variants: ['standard', 'simplified'],
13    weights: [0.5, 0.5], // 50% użytkowników zobaczy każdy wariant
14  },
15};
16
17export function middleware(request: NextRequest) {
18  const response = NextResponse.next();
19  
20  // Przypisz użytkownika do grup testów
21  Object.entries(AB_TESTS).forEach(([testName, test]) => {
22    // Pobierz istniejący wariant z cookie lub przypisz nowy
23    let variant = request.cookies.get(`ab-\${testName}`)?.value;
24    
25    if (!variant || !test.variants.includes(variant)) {
26      // Przypisz wariant na podstawie wag
27      variant = assignVariant(test.variants, test.weights);
28      
29      // Zapisz wariant w cookie
30      response.cookies.set(`ab-\${testName}`, variant, {
31        maxAge: 60 * 60 * 24 * 30, // 30 dni
32        path: '/',
33      });
34    }
35    
36    // Dodaj informację o wariancie jako nagłówek dla klienta
37    response.headers.set(`x-ab-\${testName}`, variant);
38  });
39  
40  return response;
41}
42
43// Funkcja przypisująca wariant na podstawie wag
44function assignVariant(variants: string[], weights: number[]): string {
45  const random = Math.random();
46  let cumulativeWeight = 0;
47  
48  for (let i = 0; i < variants.length; i++) {
49    cumulativeWeight += weights[i];
50    if (random < cumulativeWeight) {
51      return variants[i];
52    }
53  }
54  
55  // Domyślnie zwróć pierwszy wariant
56  return variants[0];
57}
58
59export const config = {
60  matcher: '/((?!api|_next/static|_next/image|favicon.ico).*)',
61};

Monitorowanie i debugowanie Middleware

W zastosowaniach produkcyjnych, ważne jest monitorowanie działania middleware. Możemy zaimplementować prosty system logowania:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export function middleware(request: NextRequest) {
6  const startTime = Date.now();
7  
8  // Utwórz identyfikator żądania
9  const requestId = generateRequestId();
10  
11  // Utwórz odpowiedź
12  const response = NextResponse.next();
13  
14  // Dodaj identyfikator żądania do nagłówków
15  response.headers.set('x-request-id', requestId);
16  
17  // Oblicz czas wykonania
18  const duration = Date.now() - startTime;
19  response.headers.set('x-response-time', `\${duration}ms`);
20  
21  // W środowisku produkcyjnym można wysłać te dane do systemu monitorowania
22  // logRequest(requestId, request.method, request.url, response.status, duration);
23  
24  return response;
25}
26
27// Generowanie unikalnego identyfikatora żądania
28function generateRequestId(): string {
29  return `\${Date.now()}-\${Math.random().toString(36).substring(2, 15)}`;
30}
31
32export const config = {
33  matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
34};

Integracja z systemami zewnętrznymi

Middleware może również integrować się z zewnętrznymi systemami, takimi jak systemy uwierzytelniania lub analityki:

1// middleware.ts
2import { NextResponse } from 'next/server';
3import type { NextRequest } from 'next/server';
4
5export async function middleware(request: NextRequest) {
6  // Pobierz informacje o sesji z zewnętrznego serwisu
7  const sessionId = request.cookies.get('session-id')?.value;
8  let userInfo = null;
9  
10  if (sessionId) {
11    try {
12      // W rzeczywistej aplikacji, to mogłoby być wywołanie API
13      userInfo = await fetchUserInfo(sessionId);
14    } catch (error) {
15      console.error('Error fetching user info:', error);
16    }
17  }
18  
19  // Utwórz odpowiedź
20  const response = NextResponse.next();
21  
22  // Jeśli mamy informacje o użytkowniku, dodaj je do nagłówków
23  if (userInfo) {
24    response.headers.set('x-user-id', userInfo.id);
25    response.headers.set('x-user-plan', userInfo.plan);
26    
27    // Wysyłanie danych do systemu analityki
28    trackUserActivity(userInfo.id, request.url, request.method);
29  }
30  
31  return response;
32}
33
34// Funkcja pobierająca informacje o użytkowniku (symulacja)
35async function fetchUserInfo(sessionId: string) {
36  // W rzeczywistej aplikacji, mogłoby to być wywołanie API
37  return new Promise(resolve => {
38    setTimeout(() => {
39      resolve({
40        id: 'user-123',
41        plan: 'premium',
42        name: 'John Doe'
43      });
44    }, 50);
45  });
46}
47
48// Funkcja śledząca aktywność użytkownika (symulacja)
49function trackUserActivity(userId: string, url: string, method: string) {
50  // W rzeczywistej aplikacji, mogłoby to wysyłać dane do Google Analytics, Mixpanel, itp.
51  console.log(`User \${userId} accessed \${url} with method \${method}`);
52}
53
54export const config = {
55  matcher: '/((?!_next/static|_next/image|favicon.ico).*)',
56};

Podsumowanie

Middleware w Next.js 15 jest potężnym narzędziem, które, podobnie jak Quantum Gateway w Metropolii Quantum, umożliwia kontrolę i modyfikację ruchu w aplikacji na samym początku cyklu żądanie-odpowiedź. Dzięki temu możemy:

  1. Implementować zaawansowane systemy autoryzacji - weryfikować tokeny JWT, przekierowywać niezalogowanych użytkowników, kontrolować dostęp do chronionych zasobów.

  2. Dodawać funkcje internacjonalizacji - wykrywać preferowany język użytkownika, przekierowywać do odpowiednich wersji językowych.

  3. Personalizować doświadczenie użytkownika - dostosowywać treści na podstawie lokalizacji, preferencji i historii użytkownika.

  4. Optymalizować wydajność - kontrolować nagłówki cachowania, modyfikować odpowiedzi, implementować rate limiting.

  5. Integrować się z zewnętrznymi systemami - łączyć się z systemami analityki, uwierzytelniania i innymi usługami.

  6. Przeprowadzać testy A/B - przydzielać użytkowników do grup testowych i personalizować doświadczenie.

  7. Monitorować i debugować - śledzić żądania, mierzyć wydajność, zapisywać metryki.

Wykorzystując middleware, możemy znacząco rozszerzyć możliwości naszej aplikacji Next.js 15, dodając zaawansowane funkcje, które są wykonywane na wczesnym etapie cyklu życia żądania, co pozwala na większą kontrolę i lepszą wydajność.

W następnym rozdziale, zgłębimy techniki obsługi formularz i walidacji danych w Next.js 15, co pozwoli nam tworzyć interaktywne i przyjazne dla użytkownika interfejsy zbierania danych.

Vai a CodeWorlds