Русский

Изучите продвинутые методы модификации запросов с помощью middleware Next.js. Узнайте, как обрабатывать сложные маршруты, аутентификацию, A/B-тестирование и локализацию.

Next.js Middleware Edge Cases: Освоение паттернов модификации запросов

Middleware Next.js предоставляет мощный механизм для перехвата и модификации запросов перед их достижением маршрутов вашего приложения. Эта возможность открывает широкий спектр возможностей, от простых проверок аутентификации до сложных сценариев A/B-тестирования и стратегий интернационализации. Однако эффективное использование middleware требует глубокого понимания его крайних случаев и потенциальных подводных камней. Это полное руководство исследует продвинутые паттерны модификации запросов, предоставляя практические примеры и действенные идеи, которые помогут вам создавать надежные и производительные приложения Next.js.

Понимание основ Middleware Next.js

Прежде чем углубляться в продвинутые паттерны, давайте кратко вспомним основы middleware Next.js. Функции middleware выполняются до завершения запроса, позволяя вам:

Функции middleware находятся в файле middleware.js или middleware.ts в вашем каталоге /pages или /app (в зависимости от вашей версии Next.js и настройки). Они получают объект NextRequest, представляющий входящий запрос, и могут возвращать объект NextResponse для управления последующим поведением.

Пример: Middleware базовой аутентификации

Этот пример демонстрирует простую проверку аутентификации. Если пользователь не аутентифицирован (например, нет действительного токена в cookie), он перенаправляется на страницу входа.


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*'],
}

Это middleware будет работать только для маршрутов, соответствующих /protected/:path*. Оно проверяет наличие cookie authToken. Если cookie отсутствует, пользователь перенаправляется на страницу /login. В противном случае запрос разрешается продолжить выполнение с помощью NextResponse.next().

Продвинутые паттерны модификации запросов

Теперь давайте рассмотрим некоторые продвинутые паттерны модификации запросов, которые демонстрируют истинную мощь middleware Next.js.

1. A/B-тестирование с использованием Cookies

A/B-тестирование — это важный метод оптимизации пользовательского опыта. Middleware может использоваться для случайного назначения пользователей различным вариантам вашего приложения и отслеживания их поведения. Этот паттерн полагается на cookies для сохранения назначенного варианта пользователя.

Пример: A/B-тестирование целевой страницы


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) {
    // Случайное назначение варианта
    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: ['/'],
}

В этом примере, когда пользователь впервые посещает корневой путь (/), middleware случайным образом назначает его variantA или variantB. Этот вариант сохраняется в cookie. Последующие запросы от того же пользователя будут переписаны на /variant-a или /variant-b, в зависимости от назначенного варианта. Это позволяет вам отображать разные целевые страницы и отслеживать, какая из них работает лучше. Убедитесь, что у вас определены маршруты для /variant-a и /variant-b в вашем приложении Next.js.

Глобальные соображения: При проведении A/B-тестирования учитывайте региональные различия. Дизайн, который хорошо работает в Северной Америке, может быть не столь эффективен в Азии. Вы можете использовать данные геолокации (полученные путем поиска по IP-адресу или предпочтений пользователя) для адаптации A/B-теста к конкретным регионам.

2. Локализация (i18n) с использованием переписывания URL

Интернационализация (i18n) необходима для охвата глобальной аудитории. Middleware может использоваться для автоматического определения предпочтительного языка пользователя и перенаправления его на соответствующую локализованную версию вашего сайта.

Пример: Перенаправление на основе заголовка `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

  // Проверка наличия существующего локали в 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).*)'
  ],
}

Это middleware извлекает заголовок Accept-Language из запроса и определяет предпочтительный язык пользователя. Если URL еще не содержит префикс языка (например, /en/about), middleware перенаправляет пользователя на соответствующий локализованный URL (например, /fr/about для французского). Убедитесь, что у вас есть соответствующая структура папок в каталоге /pages или /app для разных локалей. Например, вам понадобится файл /pages/en/about.js и /pages/fr/about.js.

Глобальные соображения: Убедитесь, что ваша реализация i18n корректно обрабатывает языки с письмом справа налево (например, арабский, иврит). Также рассмотрите возможность использования сети доставки контента (CDN) для обслуживания локализованных ресурсов с серверов, расположенных ближе к вашим пользователям, для повышения производительности.

3. Флаги функций

Флаги функций позволяют включать или отключать функции в вашем приложении без развертывания нового кода. Это особенно полезно для постепенного выкатывания новых функций или для тестирования функций в производственной среде. Middleware может использоваться для проверки состояния флага функции и соответствующей модификации запроса.

Пример: Включение бета-функции


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()
  }

  // Опционально перенаправить на страницу "функция недоступна"
  return NextResponse.rewrite(new URL('/feature-unavailable', request.url))
}

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

Это middleware проверяет значение переменной окружения BETA_FEATURE_ENABLED. Если она установлена в true и пользователь пытается получить доступ к маршруту в разделе /new-feature, запрос разрешается. В противном случае пользователь перенаправляется на страницу /feature-unavailable. Не забудьте соответствующим образом настроить переменные окружения для разных сред (разработка, тестирование, продакшн).

Глобальные соображения: При использовании флагов функций учитывайте юридические последствия включения функций, которые могут не соответствовать нормативным требованиям во всех регионах. Например, функции, связанные с конфиденциальностью данных, могут потребоваться для отключения в определенных странах.

4. Определение устройств и адаптивная маршрутизация

Современные веб-приложения должны быть отзывчивыми и адаптироваться к различным размерам экрана и возможностям устройств. Middleware может использоваться для определения типа устройства пользователя и перенаправления его на оптимизированные версии вашего сайта.

Пример: Перенаправление мобильных пользователей на поддомен, оптимизированный для мобильных устройств


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: ['/'],
}

Этот пример использует библиотеку detection для определения типа устройства пользователя на основе заголовка User-Agent. Если пользователь находится на мобильном телефоне, он перенаправляется на поддомен m.example.com (при условии, что у вас там размещена версия сайта, оптимизированная для мобильных устройств). Не забудьте установить пакет detection: npm install detection.

Глобальные соображения: Убедитесь, что ваша логика определения устройств учитывает региональные особенности использования устройств. Например, простые телефоны все еще распространены в некоторых развивающихся странах. Рассмотрите возможность использования комбинации определения User-Agent и техник адаптивного дизайна для более надежного решения.

5. Обогащение заголовков запросов

Middleware может добавлять информацию в заголовки запросов перед их обработкой маршрутами вашего приложения. Это полезно для добавления пользовательских метаданных, таких как роли пользователя, статус аутентификации или идентификаторы запросов, которые могут использоваться вашей логикой приложения.

Пример: Добавление идентификатора запроса


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*'], // Применяется только к API-маршрутам
}

Это middleware генерирует уникальный идентификатор запроса с помощью библиотеки uuid и добавляет его в заголовок x-request-id. Этот идентификатор затем может использоваться для целей логирования, отслеживания и отладки. Не забудьте установить пакет uuid: npm install uuid.

Глобальные соображения: При добавлении пользовательских заголовков учитывайте ограничения по размеру заголовков. Превышение этих лимитов может привести к неожиданным ошибкам. Также убедитесь, что любая конфиденциальная информация, добавляемая в заголовки, надежно защищена, особенно если ваше приложение находится за обратным прокси или CDN.

6. Улучшения безопасности: Ограничение скорости

Middleware может служить первой линией защиты от вредоносных атак, реализуя ограничение скорости. Это предотвращает злоупотребления, ограничивая количество запросов, которые клиент может сделать в течение определенного временного интервала.

Пример: Базовое ограничение скорости с использованием простого хранилища


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

const requestCounts: { [ip: string]: number } = {}
const WINDOW_SIZE_MS = 60000; // 1 минута
const MAX_REQUESTS_PER_WINDOW = 100;

export function middleware(request: NextRequest) {
  const clientIP = request.ip || '127.0.0.1' // Получить IP клиента, по умолчанию localhost для локального тестирования

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

  requestCounts[clientIP]++;

  if (requestCounts[clientIP] > MAX_REQUESTS_PER_WINDOW) {
    return new NextResponse(
      JSON.stringify({ message: 'Слишком много запросов' }),
      { status: 429, headers: { 'Content-Type': 'application/json' } }
    );
  }

  // Сбросить счетчик после окна
  setTimeout(() => {
    requestCounts[clientIP]--;
    if (requestCounts[clientIP] <= 0) {
        delete requestCounts[clientIP];
    }
  }, WINDOW_SIZE_MS);

  return NextResponse.next();
}

export const config = {
  matcher: ['/api/:path*'], // Применяется ко всем API-маршрутам
}

Этот пример поддерживает простое хранилище в памяти (requestCounts) для отслеживания количества запросов от каждого IP-адреса. Если клиент превышает MAX_REQUESTS_PER_WINDOW в течение WINDOW_SIZE_MS, middleware возвращает ошибку 429 Too Many Requests. Важно: Это упрощенный пример, который не подходит для производственной среды, так как он не масштабируется и уязвим для атак типа «отказ в обслуживании». Для производственного использования рассмотрите возможность использования более надежного решения для ограничения скорости, такого как Redis или выделенная служба ограничения скорости.

Глобальные соображения: Стратегии ограничения скорости должны быть адаптированы к конкретным характеристикам вашего приложения и географическому распределению ваших пользователей. Рассмотрите возможность использования различных лимитов скорости для разных регионов или сегментов пользователей.

Крайние случаи и потенциальные подводные камни

Хотя middleware является мощным инструментом, важно знать о его ограничениях и потенциальных подводных камнях:

Лучшие практики использования Middleware Next.js

Чтобы максимально использовать преимущества middleware Next.js и избежать потенциальных проблем, следуйте этим лучшим практикам:

Заключение

Middleware Next.js предлагает мощный способ модификации запросов и настройки поведения вашего приложения на краю сети (edge). Понимая продвинутые паттерны модификации запросов, рассмотренные в этом руководстве, вы можете создавать надежные, производительные и глобально осведомленные приложения Next.js. Помните, что необходимо тщательно учитывать крайние случаи и потенциальные подводные камни, а также следовать лучшим практикам, чтобы гарантировать, что ваши функции middleware надежны и легко поддерживаются. Используйте мощь middleware для создания исключительного пользовательского опыта и открытия новых возможностей для ваших веб-приложений.