Polski

Poznaj zaawansowane techniki modyfikacji żądań za pomocą middleware w Next.js. Naucz się obsługi złożonego routingu, uwierzytelniania, testów A/B i strategii lokalizacji dla solidnych aplikacji.

Przypadki brzegowe middleware w Next.js: Opanowanie wzorców modyfikacji żądań

Middleware w Next.js dostarcza potężny mechanizm do przechwytywania i modyfikowania żądań, zanim dotrą one do tras (routes) aplikacji. Ta zdolność otwiera szeroki wachlarz możliwości, od prostych kontroli uwierzytelniania po złożone scenariusze testów A/B i strategie internacjonalizacji. Jednak skuteczne wykorzystanie middleware wymaga głębokiego zrozumienia jego przypadków brzegowych i potencjalnych pułapek. Ten kompleksowy przewodnik zgłębia zaawansowane wzorce modyfikacji żądań, dostarczając praktycznych przykładów i użytecznych wskazówek, które pomogą Ci budować solidne i wydajne aplikacje w Next.js.

Zrozumienie podstaw middleware w Next.js

Zanim zagłębimy się w zaawansowane wzorce, przypomnijmy sobie podstawy middleware w Next.js. Funkcje middleware są wykonywane przed zakończeniem obsługi żądania, co pozwala na:

Funkcje middleware znajdują się w pliku middleware.js lub middleware.ts w katalogu /pages lub /app (w zależności od wersji i konfiguracji Next.js). Otrzymują one obiekt NextRequest reprezentujący przychodzące żądanie i mogą zwrócić obiekt NextResponse, aby kontrolować dalsze zachowanie.

Przykład: Podstawowe middleware uwierzytelniające

Ten przykład demonstruje prostą kontrolę uwierzytelniania. Jeśli użytkownik nie jest uwierzytelniony (np. brak ważnego tokena w ciasteczku), jest przekierowywany na stronę logowania.


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

export function middleware(request: NextRequest) {
  const authToken = request.cookies.get('authToken')

  if (!authToken) {
    return NextResponse.redirect(new URL('/login', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/protected/:path*'],
}

To middleware zostanie uruchomione tylko dla tras pasujących do /protected/:path*. Sprawdza ono obecność ciasteczka authToken. Jeśli ciasteczko jest nieobecne, użytkownik jest przekierowywany na stronę /login. W przeciwnym razie, żądanie jest normalnie kontynuowane za pomocą NextResponse.next().

Zaawansowane wzorce modyfikacji żądań

Teraz przeanalizujmy kilka zaawansowanych wzorców modyfikacji żądań, które pokazują prawdziwą moc middleware w Next.js.

1. Testy A/B z użyciem ciasteczek

Testy A/B są kluczową techniką optymalizacji doświadczeń użytkownika. Middleware może być używane do losowego przypisywania użytkowników do różnych wariantów aplikacji i śledzenia ich zachowania. Ten wzorzec opiera się na ciasteczkach, aby utrwalić przypisany wariant użytkownika.

Przykład: Testy A/B strony docelowej


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const VARIANT_A = 'variantA'
const VARIANT_B = 'variantB'

export function middleware(request: NextRequest) {
  let variant = request.cookies.get('variant')?.value

  if (!variant) {
    // Randomly assign a variant
    variant = Math.random() < 0.5 ? VARIANT_A : VARIANT_B
    const response = NextResponse.next()
    response.cookies.set('variant', variant)
    return response
  }

  if (variant === VARIANT_A) {
    return NextResponse.rewrite(new URL('/variant-a', request.url))
  } else if (variant === VARIANT_B) {
    return NextResponse.rewrite(new URL('/variant-b', request.url))
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/'],
}

W tym przykładzie, gdy użytkownik odwiedza ścieżkę główną (/) po raz pierwszy, middleware losowo przypisuje go do wariantu variantA lub variantB. Ten wariant jest przechowywany w ciasteczku. Kolejne żądania od tego samego użytkownika będą przepisywane na /variant-a lub /variant-b, w zależności od przypisanego wariantu. Pozwala to na serwowanie różnych stron docelowych i śledzenie, która z nich ma lepsze wyniki. Upewnij się, że masz zdefiniowane trasy dla /variant-a i /variant-b w swojej aplikacji Next.js.

Uwagi globalne: Przeprowadzając testy A/B, weź pod uwagę regionalne różnice. Projekt, który dobrze rezonuje w Ameryce Północnej, może nie być tak skuteczny w Azji. Możesz użyć danych geolokalizacyjnych (uzyskanych poprzez wyszukiwanie adresu IP lub preferencje użytkownika), aby dostosować test A/B do konkretnych regionów.

2. Lokalizacja (i18n) z przepisywaniem URL-i

Internacjonalizacja (i18n) jest niezbędna do dotarcia do globalnej publiczności. Middleware może być używane do automatycznego wykrywania preferowanego języka użytkownika i przekierowywania go do odpowiedniej zlokalizowanej wersji Twojej witryny.

Przykład: Przekierowanie na podstawie nagłówka `Accept-Language`


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const SUPPORTED_LANGUAGES = ['en', 'fr', 'es', 'de']
const DEFAULT_LANGUAGE = 'en'

function getPreferredLanguage(request: NextRequest): string {
  const acceptLanguage = request.headers.get('accept-language')
  if (!acceptLanguage) {
    return DEFAULT_LANGUAGE
  }

  const languages = acceptLanguage.split(',').map((lang) => lang.split(';')[0].trim())

  for (const lang of languages) {
    if (SUPPORTED_LANGUAGES.includes(lang)) {
      return lang
    }
  }

  return DEFAULT_LANGUAGE
}

export function middleware(request: NextRequest) {
  const pathname = request.nextUrl.pathname

  // Check if there's an existing locale in the pathname
  if (
    SUPPORTED_LANGUAGES.some(
      (locale) => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`
    )
  ) {
    return NextResponse.next()
  }

  const preferredLanguage = getPreferredLanguage(request)

  return NextResponse.redirect(
    new URL(`/${preferredLanguage}${pathname}`, request.url)
  )
}

export const config = {
  matcher: [
    '/((?!api|_next/static|_next/image|favicon.ico).*)'
  ],
}

To middleware wyodrębnia nagłówek Accept-Language z żądania i określa preferowany język użytkownika. Jeśli adres URL nie zawiera jeszcze prefiksu językowego (np. /en/about), middleware przekierowuje użytkownika na odpowiedni zlokalizowany URL (np. /fr/about dla języka francuskiego). Upewnij się, że masz odpowiednią strukturę folderów w katalogu `/pages` lub `/app` dla różnych lokalizacji. Na przykład, będziesz potrzebować plików `/pages/en/about.js` i `/pages/fr/about.js`.

Uwagi globalne: Upewnij się, że Twoja implementacja i18n poprawnie obsługuje języki pisane od prawej do lewej (np. arabski, hebrajski). Rozważ także użycie sieci dostarczania treści (CDN), aby serwować zlokalizowane zasoby z serwerów bliższych użytkownikom, co poprawi wydajność.

3. Flagi funkcyjne (Feature Flags)

Flagi funkcyjne pozwalają włączać lub wyłączać funkcje w aplikacji bez wdrażania nowego kodu. Jest to szczególnie przydatne do stopniowego wprowadzania nowych funkcji lub testowania ich w środowisku produkcyjnym. Middleware może być używane do sprawdzania statusu flagi funkcyjnej i odpowiedniego modyfikowania żądania.

Przykład: Włączanie funkcji beta


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const BETA_FEATURE_ENABLED = process.env.BETA_FEATURE_ENABLED === 'true'

export function middleware(request: NextRequest) {
  if (BETA_FEATURE_ENABLED && request.nextUrl.pathname.startsWith('/new-feature')) {
    return NextResponse.next()
  }

  // Optionally redirect to a "feature unavailable" page
  return NextResponse.rewrite(new URL('/feature-unavailable', request.url))
}

export const config = {
  matcher: ['/new-feature/:path*'],
}

To middleware sprawdza wartość zmiennej środowiskowej BETA_FEATURE_ENABLED. Jeśli jest ustawiona na true, a użytkownik próbuje uzyskać dostęp do trasy pod /new-feature, żądanie jest przepuszczane. W przeciwnym razie użytkownik jest przekierowywany na stronę /feature-unavailable. Pamiętaj o odpowiedniej konfiguracji zmiennych środowiskowych dla różnych środowisk (deweloperskiego, stagingowego, produkcyjnego).

Uwagi globalne: Używając flag funkcyjnych, weź pod uwagę prawne implikacje włączania funkcji, które mogą nie być zgodne z przepisami we wszystkich regionach. Na przykład, funkcje związane z prywatnością danych mogą wymagać wyłączenia w niektórych krajach.

4. Wykrywanie urządzeń i routing adaptacyjny

Nowoczesne aplikacje internetowe muszą być responsywne i dostosowywać się do różnych rozmiarów ekranów i możliwości urządzeń. Middleware może być używane do wykrywania typu urządzenia użytkownika i przekierowywania go do zoptymalizowanych wersji Twojej witryny.

Przykład: Przekierowanie użytkowników mobilnych na subdomenę zoptymalizowaną dla urządzeń mobilnych


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { device } from 'detection'

export function middleware(request: NextRequest) {
  const userAgent = request.headers.get('user-agent')

  if (userAgent) {
    const deviceType = device(userAgent)

    if (deviceType.type === 'phone') {
      const mobileUrl = new URL(request.url)
      mobileUrl.hostname = 'm.example.com'
      return NextResponse.redirect(mobileUrl)
    }
  }

  return NextResponse.next()
}

export const config = {
  matcher: ['/'],
}

Ten przykład używa biblioteki `detection` do określenia typu urządzenia użytkownika na podstawie nagłówka User-Agent. Jeśli użytkownik korzysta z telefonu komórkowego, jest przekierowywany na subdomenę m.example.com (zakładając, że masz tam hostowaną wersję witryny zoptymalizowaną dla urządzeń mobilnych). Pamiętaj o zainstalowaniu pakietu `detection`: `npm install detection`.

Uwagi globalne: Upewnij się, że Twoja logika wykrywania urządzeń uwzględnia regionalne różnice w użytkowaniu urządzeń. Na przykład, w niektórych krajach rozwijających się wciąż popularne są telefony z podstawowymi funkcjami (feature phones). Rozważ użycie kombinacji wykrywania User-Agent i technik responsywnego projektowania dla bardziej niezawodnego rozwiązania.

5. Wzbogacanie nagłówków żądania

Middleware może dodawać informacje do nagłówków żądania, zanim zostaną one przetworzone przez trasy aplikacji. Jest to przydatne do dodawania niestandardowych metadanych, takich jak role użytkowników, status uwierzytelnienia czy identyfikatory żądań, które mogą być wykorzystane przez logikę aplikacji.

Przykład: Dodawanie identyfikatora żądania


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
import { v4 as uuidv4 } from 'uuid'

export function middleware(request: NextRequest) {
  const requestId = uuidv4()
  const response = NextResponse.next()
  response.headers.set('x-request-id', requestId)
  return response
}

export const config = {
  matcher: ['/api/:path*'], // Only apply to API routes
}

To middleware generuje unikalny identyfikator żądania za pomocą biblioteki uuid i dodaje go do nagłówka x-request-id. Ten identyfikator może być następnie używany do celów logowania, śledzenia i debugowania. Pamiętaj o zainstalowaniu pakietu uuid: `npm install uuid`.

Uwagi globalne: Dodając niestandardowe nagłówki, pamiętaj o limitach ich rozmiaru. Przekroczenie tych limitów może prowadzić do nieoczekiwanych błędów. Upewnij się również, że wszelkie wrażliwe informacje dodawane do nagłówków są odpowiednio chronione, zwłaszcza jeśli Twoja aplikacja znajduje się za odwrotnym proxy lub CDN.

6. Ulepszenia bezpieczeństwa: Ograniczanie liczby zapytań (Rate Limiting)

Middleware może działać jako pierwsza linia obrony przed złośliwymi atakami poprzez implementację ograniczania liczby zapytań. Zapobiega to nadużyciom, ograniczając liczbę żądań, które klient może wysłać w określonym oknie czasowym.

Przykład: Podstawowe ograniczanie liczby zapytań z użyciem prostego magazynu


import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'

const requestCounts: { [ip: string]: number } = {}
const WINDOW_SIZE_MS = 60000; // 1 minute
const MAX_REQUESTS_PER_WINDOW = 100;

export function middleware(request: NextRequest) {
  const clientIP = request.ip || '127.0.0.1' // Get client IP, default to localhost for local testing

  if (!requestCounts[clientIP]) {
    requestCounts[clientIP] = 0;
  }

  requestCounts[clientIP]++;

  if (requestCounts[clientIP] > MAX_REQUESTS_PER_WINDOW) {
    return new NextResponse(
      JSON.stringify({ message: 'Too many requests' }),
      { status: 429, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // Reset count after window
  setTimeout(() => {
    requestCounts[clientIP]--;
    if (requestCounts[clientIP] <= 0) {
        delete requestCounts[clientIP];
    }
  }, WINDOW_SIZE_MS);

  return NextResponse.next();
}

export const config = {
  matcher: ['/api/:path*'], // Apply to all API routes
}

Ten przykład utrzymuje prosty magazyn w pamięci (requestCounts) do śledzenia liczby żądań z każdego adresu IP. Jeśli klient przekroczy MAX_REQUESTS_PER_WINDOW w ciągu WINDOW_SIZE_MS, middleware zwraca błąd 429 Too Many Requests. Ważne: To jest uproszczony przykład i nie nadaje się do środowisk produkcyjnych, ponieważ nie skaluje się i jest podatny na ataki typu denial-of-service. Do użytku produkcyjnego rozważ użycie bardziej solidnego rozwiązania do ograniczania liczby zapytań, takiego jak Redis lub dedykowana usługa rate-limiting.

Uwagi globalne: Strategie ograniczania liczby zapytań powinny być dostosowane do specyfiki Twojej aplikacji i geograficznego rozkładu użytkowników. Rozważ użycie różnych limitów dla różnych regionów lub segmentów użytkowników.

Przypadki brzegowe i potencjalne pułapki

Chociaż middleware jest potężnym narzędziem, należy być świadomym jego ograniczeń i potencjalnych pułapek:

Najlepsze praktyki korzystania z middleware w Next.js

Aby zmaksymalizować korzyści płynące z middleware w Next.js i uniknąć potencjalnych problemów, postępuj zgodnie z tymi najlepszymi praktykami:

Podsumowanie

Middleware w Next.js oferuje potężny sposób modyfikowania żądań i dostosowywania zachowania aplikacji na brzegu sieci (at the edge). Dzięki zrozumieniu zaawansowanych wzorców modyfikacji żądań omówionych w tym przewodniku, możesz budować solidne, wydajne i globalnie świadome aplikacje w Next.js. Pamiętaj, aby starannie rozważyć przypadki brzegowe i potencjalne pułapki oraz postępować zgodnie z powyższymi najlepszymi praktykami, aby zapewnić, że Twoje funkcje middleware są niezawodne i łatwe w utrzymaniu. Wykorzystaj moc middleware, aby tworzyć wyjątkowe doświadczenia użytkownika i odblokować nowe możliwości dla swoich aplikacji internetowych.