Привет, друзья!


Хочу поделиться с вами заметками о Next.js (надеюсь, кому-нибудь пригодится).


Next.js — это основанный на React фреймворк, предназначенный для разработки веб-приложений, обладающих функционалом, выходящим за рамки SPA, т.е. так называемых одностраничных приложений.


Как известно, основным недостатком SPA являются проблемы с индексацией страниц таких приложений поисковыми роботами, что негативно влияет на SEO.


Впрочем, по моим личным наблюдениям, в последнее время ситуация стала меняться к лучшему, по крайней мере, страницы моего небольшого SPA-PWA-приложения нормально индексируются.


Кроме того, существуют специальные инструменты, такие как react-snap, позволяющие превратить React-SPA в многостраничник путем предварительного рендеринга приложения в статическую разметку. Метаинформацию же можно встраивать в head с помощью таких утилит, как react-helmet. Однако Next.js существенно упрощает процесс разработки многостраничных и гибридных приложений (последнего невозможно добиться с помощью того же react-snap). Он также предоставляет множество других интересных возможностей.



Обратите внимание: данная статья предполагает, что вы обладаете некоторым опытом работы с React. Также обратите внимание, что заметки не сделают вас специалистом по Next.js, но позволят получить о нем исчерпывающее представление.


Заметки состоят из 2 частей. Это часть номер два.


Итерация первая


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



Продвинутые возможности


Динамическая маршрутизация


Next.js поддерживает динамический import. Он позволяет загружать модули по необходимости. Это также работает с SSR.


В следующем примере мы реализуем неточный (fuzzy) поиск с помощью fuse.js, загружая этот модуль динамически после того, как пользователь начинает вводить данные в поле для поиска:


import { useState } from 'react'

const names = ['John', 'Jane', 'Alice', 'Bob', 'Harry']

export default function Page() {
  const [results, setResults] = useState([])

  return (
    <div>
      <input
        type="text"
        placeholder="Поиск"
        onChange={async ({ currentTarget: { value } }) => {
          // загружаем модуль динамически
          const Fuse = (await import('fuse.js')).default
          const fuse = new Fuse(names)

          setResults(fuse.search(value))
        }}
      />
      <pre>Результаты поиска: {JSON.stringify(results, null, 2)}</pre>
    </div>
  )
}

Динамический импорт — это способ разделения кода на части (code splitting), позволяющий загружать только тот код, который фактически используется на текущей странице.


Для динамического импорта React-компонентов рекомендуется использовать next/dynamic.


Обычное использование


В следующем примере мы динамически загружаем модель ../components/Hello:


import dynamic from 'next/dynamic'
import Header from '../components/Header'

const DynamicHello = dynamic(() => import('../components/Hello'))

export default function Home() {
  return (
    <div>
      <Header />
      <DynamicHello />
      <h1>Главная страница</h1>
    </div>
  )
}

Динамический импорт именованного компонента


Предположим, что у нас имеется такой компонент:


// components/Hello.js
export const Hello = () => <p>Привет!</p>

Для его динамического импорта его необходимо вернуть из Promise, возвращаемого import:


import dynamic from 'next/dynamic'
import Header from '../components/Header'

const DynamicHello = dynamic(() =>
  import('../components/Hello')
    .then((module) => module.Hello)
)

export default function Home() {
  return (
    <div>
      <Header />
      <DynamicHello />
      <h1>Главная страница</h1>
    </div>
  )
}

Индикатор загрузки


Для рендеринга состояния загрузки можно использовать настройку loading:


import dynamic from 'next/dynamic'
import Header from '../components/Header'

const DynamicHelloWithCustomLoading = dynamic(
  () => import('../components/Hello'),
  { loading: () => <p>Загрузка...</p> }
)

export default function Home() {
  return (
    <div>
      <Header />
      <DynamicHelloWithCustomLoading />
      <h1>Главная страница</h1>
    </div>
  )
}

Отключение SSR


Для отключения импорта модуля на стороне сервера можно использовать настройку ssr:


import dynamic from 'next/dynamic'
import Header from '../components/Header'

const DynamicHelloWithNoSSR = dynamic(
  () => import('../components/Hello'),
  { ssr: false }
)

export default function Home() {
  return (
    <div>
      <Header />
      <DynamicHelloWithNoSSR />
      <h1>Главная страница</h1>
    </div>
  )
}

Настройка suspense позволяет загружать компонент лениво подобно тому, как это делает сочетание React.lazy и <Suspense>. Обратите внимание, что это работает только на клиенте или на сервере с fallback. Полная поддержка SSR в конкурентном режиме находится в процессе разработки:


import dynamic from 'next/dynamic'
import Header from '../components/Header'

const DynamicLazyHello = dynamic(() => import('../components/Hello'), { suspense: true })

export default function Home() {
  return (
    <div>
      <Header />
      <DynamicLazyHello />
      <h1>Главная страница</h1>
    </div>
  )
}

Автоматическая статическая оптимизация


Если на странице присутствуют getServerSideProps, Next.js будет рендерить страницу при каждом запросе (рендеринг на стороне сервера).


В противном случае, Next.js выполняет предварительный рендеринг страницы в статическую разметку.


В процессе предварительного рендеринга объект роутера query будет пустым. После гидратации Next.js запускает обновление приложения для заполнения query параметрами маршрутизации.


next build генерирует HTML-файлы для статически оптимизированных страниц. Например, результатом для pages/about.js будет:


.next/server/pages/about.html

А если на эту страницу добавить getServerSideProps, то результат будет таким:


.next/server/pages/about.js

Заметки


  • при наличии кастомного App оптимизация для страниц без статической генерации отключается
  • при наличии кастомного Document с getInitialProps убедитесь в определении ctx.req перед рендерингом страницы на стороне сервера. Для страниц, которые рендерятся предварительно, ctx.req будет иметь значение undefined

Экспорт статической разметки


next export позволяет экспортировать приложение в статический HTML. Такое приложение может запускаться без сервера.


Такое приложение поддерживает почти все возможности, предоставляемые Next.js, включая динамическую маршрутизацию, предварительный запрос и получение данных и динамический импорт.


next export работает за счет предварительного рендеринга всех страниц в HTML. Для динамических роутов страница может экспортировать функцию getStaticPaths, чтобы Next.js мог определить, какие страницы следует рендерить для данного роута.


next export рассчитан для приложений без рендеринга на стороне сервера или инкрементального рендеринга. Такие фичи, как инкрементальная статическая генерация и регенерация при использовании next export будут отключены.


Для экспорта приложения в статику после разработки приложения следует выполнить команду:


next build && next export

Статическая версия приложения будет записана в директорию out.


По умолчанию next export не требует настройки. HTML-файлы генерируются для всех страниц, определенных в директории pages. Для более продвинутых сценариев можно определить параметр exportPathMap в next.config.js для настройки генерируемых страниц.


Заметки


  • с помощью next export мы генерируем HTML-версию приложения. Во время сборки для каждой страницы вызывается функция getStaticProps, результаты выполнения которой передаются компоненту страницы в виде пропов. Вместо getStaticProps можно использовать старый интерфейс getInitialProps, но это имеет некоторые ограничения
  • режим fallback: true в getStaticProps не поддерживается
  • интерфейс маршрутизации не поддерживается
  • getServerSideProps не поддерживается
  • локализованный роутинг не поддерживается
  • дефолтный индикатор загрузки компонента Image не поддерживается (другие настройки loader поддерживаются)

Абсолютный импорт и синонимы путей модулей


Next.js поддерживает настройки baseUrl и paths в файлах tsconfig.json и jsconfig.json.


baseUrl позволяет импортировать модули напрямую из корневой директории.


Пример:


// `tsconfig.json` или `jsconfig.json`
{
  "compilerOptions": {
    "baseUrl": "."
  }
}

// components/button.js
export const Button = () => <button>Click Me</button>

// pages/index.js
import { Button } from 'components/button'

export default function Index() {
  return (
    <>
      <h1>Привет, народ!</h1>
      <Button />
    </>
  )
}

paths позволяет определять синонимы для путей модулей. Например:


// `tsconfig.json` или `jsconfig.json`
{
  "compilerOptions": {
    "baseUrl": ".",
    "paths": {
      "@/*": ["components/*"]
    }
  }
}

// components/button.js
export const Button = () => <button>Click Me</button>

// pages/index.js
import { Button } from '@/button'

export default function Index() {
  return (
    <>
      <h1>Привет, народ!</h1>
      <Button />
    </>
  )
}

Кастомный сервер


Пример кастомного сервера:


// server.js
const { createServer } = require('http')
const { parse } = require('url')
const next = require('next')

const dev = process.env.NODE_ENV !== 'production'
const app = next({ dev })
const handle = app.getRequestHandler()

app.prepare().then(() => {
  createServer((req, res) => {
    // `true` обеспечивает разбор строки запроса
    const parseUrl = parse(req.url, true)
    const { pathname, query } = parseUrl

    switch (pathname) {
      case '/a':
        return app.render(req, res, '/a', query)
      case '/b':
        return app.render(req, res, '/b', query)
      default:
        handle(req, res, parsedUrl)
    }
  }).listen(3000, (err) => {
    if (err) throw err
    console.log('Запущен по адресу: http://localhost:3000')
  })
})

Для запуска этого сервера необходимо обновить раздел scripts в файле package.json следующим образом:


"scripts": {
  "dev": "node server.js",
  "build": "next build",
  "start": "NODE_ENV=production node server.js"
}

Кастомный сервер использует такой импорт для подключения к Next.js-приложению:


const next = require('next')
const app = next({})

next — это функция, которая принимает объект со следующими настройками:


  • dev: boolean — запуск сервера в режиме для разработки
  • dir: string — корневая директория проекта (по умолчанию .)
  • quiet: boolean — если true, ошибки сервера не отображаются (по умолчанию false)
  • conf: object — такой же объект, что используется в next.config.js

После этого app может использоваться для обработки входящих запросов.


По умолчанию Next.js обслуживает каждый файл в директории pages по пути, совпадающему с названием файла. При использовании кастомного сервера это может привести к возвращению одинакового контента для разных путей.


Для отключения маршрутизации, основанной на файлах в pages, используется настройка useFileSystemPublicRoutes в next.config.js:


module.exports = {
  useFileSystemPublicRoutes: false
}

Обратите внимание: это отключает роутинг по названиям файлов только для SSR. Маршрутизация на стороне клиента по-прежнему будет иметь доступ к этим путям.


Кастомное приложение


Next.js использует компонент App для инициализации страниц. Кастомизация этого компонента позволяет делать следующее:


  • распределять макет (layout) между страницами
  • сохранять состояние при переключении между страницами
  • обрабатывать ошибки с помощью componentDidCatch
  • внедрять в страницы дополнительные данные
  • добавлять глобальные стили

Для перезаписи дефолтного App необходимо создать файл pages/_app.js следующего содержания:


// pages/_app.js
import App from 'next/app'

function MyApp({ Component, pageProps }) {
  return <Component {...pageProps} />
}

MyApp.getInitialProps = async (appContext) => {
  const appProps = await App.getInitialProps(appContext)

  return { ...appProps }
}

export default MyApp

Проп Component — это активная страница. pageProps — это объект с начальными пропами, которые предварительно загружаются для страницы с помощью одного из методов для получения данных, в противном случае, данный объект будет пустым.


Заметки


  • при добавлении кастомного App (создании файла pages/_app.js) при запущенном приложении может потребоваться перезапуск сервера для разработки
  • добавление кастомного getInitialProps в App отключает автоматическую статическую оптимизацию страниц без статической генерации
  • при добавлении getInitialProps в кастомное приложение следует import App from 'next/app', вызвать App.getInitialProps(appContext) внутри getInitialProps и объединить объект ответа с возвращаемым значением
  • в настоящее время App не поддерживает такие методы для получения данных, как getStaticProps или getServerSideProps

Кастомные страницы ошибок


Next.js предоставляет дефолтные страницы для ошибок 404 и 500. Для кастомизации этих страниц используются файлы pages/404.js и pages/500.js, соответственно:


// pages/404.js
export default function Custom404() {
  return <h1>404 - Страница не найдена</h1>
}

// pages/500.js
export default function Custom500() {
  return <h1>500 - Ошибка на сервере</h1>
}

Для получения данных во время сборки на этих страницах может использоваться getStaticProps.


Ошибки 500 обслуживаются как на стороне клиента, так и на стороне сервера компонентом Error. Для перезаписи этого компонента необходимо создать файл pages/_error.js и добавить в него код вроде следующего:


export default function Error({ statusCode }) {
  return (
    <p>
      {statusCode
        ? `На сервере возникла ошибка ${statusCode}`
        : `Ошибка возникла на клиенте`}
    </p>
  )
}

Error.getInitialProps = ({ res, err }) => {
  const statusCode = res ? res.statusCode : err ? err.statusCode : 404
  return { statusCode }
}

Обратите внимание: pages/_error.js используется только в продакшне. В режиме для разработки ошибка возвращается вместе со стеком вызовов для обеспечения возможности определения места ее возникновения.


Для рендеринга встроенной страницы ошибки также можно использовать компонент Error:


import Error from 'next/error'

export async function getServerSideProps() {
  const res = await fetch('https://api.github.com/repos/harryheman/react-total')
  const errorCode = res.ok ? false : res.statusCode
  const data = await res.json()

  return {
    props: {
      errorCode,
      stars: data.stergazers_count
    }
  }
}

export default function Page({ errorCode, stars }) {
  if (errorCode) {
    return <Error statusCode={errorCode} />
  }

  return <div>Количество звезд: {stars}</div>
}

Компонент Error также принимает проп title для передачи текстового сообщения в дополнение к статус-коду.


Директория src


Страницы приложения могут помещаться в директорию src/pages вместо корневой директории pages.


Заметки


  • src/pages будет игнорироваться при наличии pages
  • файлы с настройками, такие как next.config.js и tsconfig.json должны находиться в корневой директории проекта. Тоже самое касается директории public

Локализованная маршрутизация (интернационализация)


Next.js имеет встроенную поддержку локализованного роутинга. Достаточно указать список локалей, дефолтную локаль и привязанные к домену локали.


Поддержка роутинга i18n означает интеграцию с такими библиотеками, как react-intl, react-i18next, lingui, rosetta, next-intl и др.


Начало работы


Для начала работы необходимо настроить i18n в файле next.config.js. Идентификатор локали выглядит как язык-регион-скрипт, например:


  • en-US — американский английский
  • nl-NL — нидерландский (голландский)
  • nl — нидерландский без учета региона

// next.config.js
module.exports = {
  i18n: {
    // Локали, поддерживаемые приложением
    locales: ['en-US', 'fr', 'nl-NL'],
    // Дефолтная локаль, которая будет использоваться
    // при посещении пользователем пути без префикса,
    // например, `/hello`
    defaultLocale: 'en-US',
    // Список доменов и привязанных к ним локалей
    // (требуется только в случае настройки маршрутизации на основе доменов)
    // Обратите внимание: поддомены должны включаться в значение домена,
    // например, `fr.example.com`
    domains: [
      {
        domain: 'example.com',
        defaultLocale: 'en-US'
      },
      {
        domain: 'example.nl',
        defaultLocale: 'nl-NL'
      },
      {
        domain: 'example.fr',
        defaultLocale: 'fr',
        // Опциональное поле `http` может использоваться для локального тестирования
        // локали, привязанной к домену (т.е. по `http` вместо `https`)
        http: true
      }
    ]
  }
}

Стратегии локализации


Существует 2 стратегии локализации: маршрутизация на основе субпутей (subpaths) и роутинг на основе доменов.


Роутинг на основе субпутей


В этом случае локаль помещается в url:


// next.config.js
module.exports = {
  i18n: {
    locales: ['en-US', 'fr', 'nl-NL'],
    defaultLocale: 'en-US'
  }
}

Здесь en-US, fr и nl-NL будет доступны для перехода, а en-US будет использоваться по умолчанию. Для страницы pages/blog будут доступны следующие url:


  • /blog
  • /fr/blog
  • /nl-nl/blog

Дефолтная локаль не имеет префикса.


Роутинг на основе доменов


В этом случае локали будут обслуживаться разными доменами:


// next.config.js
module.exports = {
  i18n: {
    locales: ['en-US', 'fr', 'nl-NL', 'nl-BE'],
    defaultLocale: 'en-US',

    domains: [
      {
        domain: 'example.com',
        defaultLocale: 'en-US',
      },
      {
        domain: 'example.fr',
        defaultLocale: 'fr',
      },
      {
        domain: 'example.nl',
        defaultLocale: 'nl-NL',
        // Список указанных локалей будет обслуживаться этим доменом
        locales: ['nl-BE']
      }
    ]
  }
}

Для страницы pages/blog будут доступны следующие url:


  • example.com/blog
  • example.fr/blog
  • example.nl/blog
  • example.nl/nl-BE/blog

Автоматическое определение локали


Когда пользователь запускает приложение, Next.js пытается автоматически определить его локаль на основе заголовка Accept-Language и текущего домена. При обнаружении локали, отличающейся от дефолтной, выполняется перенаправление.


Если при посещении example.com запрос содержит заголовок Accept-Language: fr;q=0.9, в случае роутинга на основе домена выполняется перенаправление на example.fr, а в случае роутинга на основе субпутей — на /fr.


Отключение автоматического определения локали


// next.config.js
module.exports = {
  i18n: {
    localeDetection: false
  }
}

Доступ к информации о локали


Информация о локали содержится в роутере, доступ к которому можно получить с помощью хука useRouter. Доступны следующие свойства:


  • locale — текущая локаль
  • locales — доступные локали
  • defaultLocale — локаль по умолчанию

В случае предварительного рендеринга страниц с помощью getStaticProps или getServerSideProps информация о локали содержится в контексте, передаваемом функции.


При использовании getStaticPaths локали также содержатся в параметре context, передаваемом функции, в свойствах locales и defaultLocale.


Переключение между локалями


Для переключения между локалями можно использовать next/link или next/router.


Для next/link может быть указан проп locale для переключения на другую локаль. Если такой проп не указан, используется текущая локаль:


import Link from 'next/link'

export default function IndexPage(props) {
  return (
    <Link href="/another" locale="fr">
      <a>Перейти к `/fr/another`</a>
    </Link>
  )
}

При использовании next/router локаль указывается в настройках:


import { useRouter } from 'next/router'

export default function IndexPage(props) {
  const router = useRouter()

  return (
    <div
      onClick={() => {
        router.push('/another', '/another', { locale: 'fr' })
      }}
    >
      Перейти к `/fr/another`
    </div>
  )
}

Обратите внимание: для переключения локали с сохранением информации, хранящейся в роутере, такой так значения динамической строки запроса или значения скрытой строки запроса, в качестве значения пропа href можно использовать объект:


import { useRouter } from 'next/router'
const router = useRouter()
const { pathname, asPath, query } = router
// Переключаем локаль с сохранением другой информации
router.push({ pathname, query }, asPath, { locale: nextLocale })

Если href включает локаль, автоматическое добавления префикса можно отключить:


import Link from 'next/link'

export default function IndexPage(props) {
  return (
    <Link href="/fr/another" locale={false}>
      <a>Перейти к `/fr/another`</a>
    </Link>
  )
}

Заметки


  • Next.js позволяет перезаписывать значение заголовка Accept-Language с помощью куки NEXT_LOCALE=локаль. При установке такой куки, Accept-Language будет игнорироваться.
  • Next.js автоматически добавляет атрибут lang к тегу html. Однако, он не знает о возможных вариантах страницы, поэтому добавление мета-тега hreflang — задача разработчика (это можно сделать с помощью next/head).

Статическая генерация


Динамические роуты и getStaticProps


Для страниц, на которых используется динамическая маршрутизация с помощью getStaticProps, все локали, которые должны быть предварительно отрендерены, должны возвращаться из getStaticPaths. Вместе с объектом params возвращается поле locale, определяющее локаль для рендеринга:


// pages/blog/[slug].js
export const getStaticPaths = ({ locales }) => {
  return {
    paths: [
      // При отсутствии `locale` генерируется только локаль по умолчанию
      { params: { slug: 'post-1' }, locale: 'en-US' },
      { params: { slug: 'post-1' }, locale: 'fr' },
    ],
    fallback: true
  }
}

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


Для решения этой проблемы следует использовать режим fallback. Это позволяет возвращать из getStaticPaths только самые популярные пути и локали для предварительного рендеринга во время сборки. Остальные страницы будут рендерится во время выполнения по запросу.


Если из getStaticProps нединамической страницы вернуть notFound: true, то соответствующий вариант страницы сгенерирован не будет:


export async function getStaticProps({ locale }) {
  // Получаем посты из внешнего `API`
  const res = await fetch(`https://example.com/posts?locale=${locale}`)
  const posts = await res.json()

  if (posts.length === 0) {
    return {
      notFound: true
    }
  }

  return {
    props: {
      posts
    }
  }
}

Ограничения


  • locales: максимум 100 локалей
  • domains: максимум 100 доменов

Заголовки безопасности


Шпаргалка и туториал по заголовкам безопасности.


Для улучшения безопасности приложения предназначена настройка headers в next.config.js. Данная настройка позволяет устанавливать HTTP-заголовки ответов для всех роутов в приложении:


// next.config.js
// Список заголовков
const securityHeaders = []

module.exports = {
  async headers() {
    return [
      {
        // Данные заголовки будут применяться ко всем роутам в приложении
        source: '/(.*)',
        headers: securityHeaders
      }
    ]
  }
}

Настройки


X-DNS-Prefetch-Control


Данный заголовок управляет предварительной загрузкой (prefetching) DNS, позволяя браузерам заблаговременно разрешать названия доменов для внешних ссылок, изображений, CSS, JS и т.д. Предварительная загрузка выполняется в фоновом режиме и уменьшает время реакции на клик пользователя по ссылке:


{
  key: 'X-DNS-Prefetch-Control',
  value: 'on'
}

Strict-Transport-Security


Данный заголовок указывает браузеру использовать HTTPS вместо HTTP:


{
  key: 'Strict-Transport-Security',
  // 2 года
  value: 'max-age=63072000; includeSubDomains; preload'
}

X-XSS-Protection


Данный заголовок предназначен для блокировки межсайтового скриптинга в старых браузерах, не поддерживающих заголовок Content-Security-Policy:


{
  key: 'X-XSS-Protection',
  value: '1; mode=block'
}

X-Frame-Options


Данный заголовок определяет, может ли сайт открываться в iframe. Он также предназначен для старых браузеров, не поддерживающих настройку frame-ancestors заголовка CSP:


{
  key: 'X-Frame-Options',
  value: 'SAMEORIGIN'
}

Permissions-Policy


Данный заголовок позволяет определять, какие возможности и API могут использоваться в браузере (раньше он назывался Feature-Policy):


{
  key: 'Permissions-Policy',
  value: 'camera=(), microphone=(), geolocation=()'
}

X-Content-Type-Options


Данный заголовок запрещает браузеру автоматически определять тип содержимого при отсутствии заголовка Content-Type:


{
  key: 'X-Content-Type-Options',
  value: 'nosniff'
}

Referrer-Policy


Данный заголовок управляет тем, какая информация о предыдущем сайте включается в ответ:


{
  key: 'Referrer-Policy',
  value: 'origin-when-cross-origin'
}

Content-Security-Policy


Данный заголовок определяет разрешенные источники для загрузки скриптов, стилей, изображений, шрифтов, медиа и др.:


{
  key: 'Content-Security-Policy',
  value: // Политика `CSP`
}

API


Next CLI


Next CLI позволяет запускать, собирать и экспортировать приложение.


Команда для получения списка доступных команд:


npx next -h

Пример передачи аргументов:


NODE_OPTIONS='--throw-deprecation' next
NODE_OPTIONS='-r esm' next
NODE_OPTIONS='--inspect' next

Сборка


Команда next build создает оптимизированную производственную сборку приложения. Для каждого роута отображается:


  • размер (size) — количество ресурсов, загружаемых при запросе страницы на стороне клиента
  • первоначально загружаемый (first load) JS — количество ресурсов, загружаемых при получении страницы от сервера

Флаг --profile позволяет включить производственный профайлинг:


next build --profile

Флаг --verbose позволяет получить более подробный вывод:


next build --verbose

Разработка


Команда next dev запускает приложение в режиме разработки с быстрой перезагрузкой кода, отчетами об ошибках и т.д.


По умолчанию приложение запускается по адресу http://localhost:3000. Порт может быть изменен с помощью флага -p:


npx next dev -p 4000

Для этого также можно использовать переменную PORT:


PORT=4000 npx next dev

Дефолтный хост (0.0.0.0) можно изменить с помощью флага -H:


npx next dev -H 192.168.1.2

Продакшн


Команда next start запускает приложение в производственном режиме. Перед этим приложение должно быть скомпилировано с помощью next build.


Линтер


Команда next lint запускает ESLint для всех файлов в директориях pages, components и lib. Дополнительные директории для проверки могут быть определены с помощью флага --dir:


next lint --dir utils

Create Next App


CLI create-next-app — это простейший способ создать болванку Next-проекта.


npx create-next-app [app-name]
# or
yarn create next-app [app-name]

Или, если вам требуется поддержка TypeScript:


npx create-next-app [app-name] --typescript
# or
yarn create next-app [app-name] --ts

Настройки


  • --ts | --typescript — инициализация TS-проекта
  • -e, --example [example-name][github-url] — инициализация проекта на основе примера. В URL может быть указана любая ветка и/или поддиректория
  • --example-path [path-to-example]
  • --use-npm

next/router


useRouter


Хук useRouter позволяет получить доступ к объекту router в любом функциональном компоненте:


import { useRouter } from 'next/router'

export const ActiveLink = ({ children, href }) => {
  const router = useRouter()

  const style = {
    marginRight: 10,
    color: router.asPath === href ? 'green' : 'blue',
  }

  const handleClick = (e) => {
    e.preventDefault()
    router.push(href)
  }

  return (
    <a href={href} onClick={handleClick} style={style}>
      {children}
    </a>
  )
}

Объект router


Объект router возвращается useRouter и withRouter и содержит следующие свойства:


  • pathname: string — текущий роут, путь страницы из /pages без basePath или locale
  • query: object — строка запроса, разобранная в объект. По умолчанию имеет значение {}
  • asPath: string — путь (включая строку запроса), отображаемый в браузере, без basePath или locale
  • isFallback: boolean — находится ли текущая страница в резервном режиме?
  • basePath: string — активный базовый путь
  • locale: string — активная локаль
  • locales: string[] — поддерживаемые локали
  • defaultLocale: string — дефолтная локаль
  • domainsLocales: Array<{ domain, defaultLocalem locales }> — локали для доменов
  • isReady: boolean — готовы ли поля роутера к использованию? Может использоваться только в useEffect
  • isPreview: boolean — находится ли приложение в режиме предварительного просмотра?

router.push


Метод router.push предназначен для случаев, когда next/link оказывается недостаточно.


router.push(url, as, options)

  • url: string | object — путь для навигации
  • as: string | object — опциональный декоратор для адреса страницы при отображении в браузере
  • options — опциональный объект со следующими свойствами:
    • scroll: boolean — выполнять ли прокрутку в верхнюю часть области просмотра при переключении страницы? По умолчанию имеет значение true
    • shallow: boolean — позволяет обновлять путь текущей страницы без использования getStaticProps, getServerSideProps или getInitialProps. По умолчанию имеет значение false
    • locale: string — локаль новой страницы

Использование


Переключение на страницу pages/about.js:


import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button className="link" onClick={() => router.push('/about')}>
      О нас
    </button>
  )
}

Переключение на страницу pages/post/[pid].js:


import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button className="link" onClick={() => router.push('/post/abc')}>
      Подробнее
    </button>
  )
}

Перенаправление пользователя на страницу pages/login.js (может использоваться для реализации защищенных страниц):


import { useEffect } from 'react'
import { useRouter } from 'next/router'

// Получаем данные пользователя
const useUser = () => ({ user: null, loading: false })

export default function Page() {
  const { user, loading } = useUser()
  const router = useRouter()

  useEffect(() => {
    if (!loading && !user) {
      router.push('/login')
    }
  }, [user, loading])

  return user ? <p>Привет, {user.name}!</p> : <p>Загрузка...</p>
}

Пример использования объекта вместо строки:


import { useRouter } from 'next/router'

export default function ReadMore({ post }) {
  const router = useRouter()

  return (
    <button
      className='link'
      onClick={() => {
        router.push({
          pathname: '/post/[pid]',
          query: { pid: post.id }
        })
      }}
    >
      Подробнее
    </button>
  )
}

router.replace


Метод router.replace обновляет путь текущей страницы без добавления URL в стек history.


router.replace(url, as, options)

Использование


import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button className="link" onClick={() => router.replace('/home')}>
      Главная
    </button>
  )
}

router.prefetch


Метод router.prefetch позволяет выполнять предварительную загрузку страниц для ускорения навигации. Обратите внимание: next/link выполняет предварительную загрузку страниц автоматически.


router.prefetch(url, as)

Использование


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


import { useCallback, useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Login() {
  const router = useRouter()

  const handleSubmit = useCallback((e) => {
    e.preventDefault()

    fetch('/api/login', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({
        /* Данные пользователя */
      }),
    }).then((res) => {
      // Выполняем перенаправление на предварительно загруженную страницу профиля
      if (res.ok) router.push('/dashboard')
    })
  }, [])

  useEffect(() => {
    // Предварительно загружаем страницу профиля
    router.prefetch('/dashboard')
  }, [])

  return (
    <form onSubmit={handleSubmit}>
      {/* Поля формы */}
      <button type="submit">Войти</button>
    </form>
  )
}

router.beforePopState


Метод router.beforePopState позволяет реагировать на событие popstate перед обработкой пути роутером.


router.beforePopState(cb)

cb — это функция, которая запускается при возникновении события popstate. Данная функция получает объект события со следующими свойствами:


  • url: string — путь для нового состояния (как правило, название страницы)
  • as: stringURL, который будет отображен в браузере
  • options: object — дополнительные настройки из router.push

Использование


beforePopState может использоваться для модификации запроса или принудительного обновления SSR, как в следующем примере:


import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  useEffect(() => {
    router.beforePopState(({ url, as, options }) => {
      // Разрешены только указанные пути
      if (as !== '/' && as !== '/other') {
        // Заставляем `SSR` рендерить страницу 404
        window.location.href = as
        return false
      }

      return true
    })
  }, [])

  return <p>Добро пожаловать!</p>
}

router.back


Данный метод позволяет вернуться к предыдущей странице. При его использовании выполняется window.history.back():


import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button className="link" onClick={() => router.back()}>
      Назад
    </button>
  )
}

router.reload


Данный метод перезагружает текущий URL. При его использовании выполняется window.location.reload():


import { useRouter } from 'next/router'

export default function Page() {
  const router = useRouter()

  return (
    <button className="link" onClick={() => router.reload()}>
      Обновить
    </button>
  )
}

router.events


Использование роутера приводит к возникновению следующих событий:


  • routeChangeStart(url, { shallow }) — возникает в начале обновления роута
  • routeChangeComplete(url, { shallow }) — возникает в конце обновления роута
  • routeChangeError(err, url, { shallow }) — возникает при провале или отмене обновления роута
  • beforeHistoryChange(url, { shallow }) — возникает перед изменением истории браузера
  • hashChangeStart(url, { shallow }) — возникает в начале изменения хеш-части URL (но не самого URL)
  • hashChangeComplete(url, { shallow }) — возникает в конце изменения хеш-части URL (но не самого URL)

Обратите внимание: здесь url — это адрес страницы, отображаемый в браузере, включая basePath.


Использование


// pages/_app.js
import { useEffect } from 'react'
import { useRouter } from 'next/router'

export default function MyApp({ Component, pageProps }) {
  const router = useRouter()

  useEffect(() => {
    const handleRouteChange = (url, { shallow }) => {
      console.log(
        `Выполняется переключение на страницу ${url} ${
          shallow ? 'с' : 'без'
        } поверхностной маршрутизации`
      )
    }

    router.events.on('routeChangeStart', handleRouteChange)

    // При размонтировании компонента
    // отписываемся от события с помощью метода `off`
    return () => {
      router.events.off('routeChangeStart', handleRouteChange)
    }
  }, [])

  return <Component {...pageProps} />
}

withRouter


Вместо useRouter может использоваться withRouter:


import { withRouter } from 'next/router'

function Page({ router }) {
  return <p>{router.pathname}</p>
}

export default withRouter(Page)

TypeScript


import React from 'react'
import { withRouter, NextRouter } from 'next/router'

interface WithRouterProps {
  router: NextRouter
}

interface MyComponentProps extends WithRouterProps {}

class MyComponent extends React.Component<MyComponentProps> {
  render() {
    return <p>{this.props.router.pathname}</p>
  }
}

export default withRouter(MyComponent)


Компонент Link, экспортируемый из next/link, позволяет переключаться между страницами на стороне клиента.


Предположим, что в директории pages содержатся следующие файлы:


  • pages/index.js
  • pages/about.js
  • pages/blog/[slug].js

Пример навигации по этим страницам:


import Link from 'next/link'

export default function Home() {
  return (
    <ul>
      <li>
        <Link href="/">
          <a>Главная</a>
        </Link>
      </li>
      <li>
        <Link href="/about">
          <a>О нас</a>
        </Link>
      </li>
      <li>
        <Link href="/blog/hello-world">
          <a>Пост</a>
        </Link>
      </li>
    </ul>
  )
}

Link принимает следующие пропы:


  • href — путь или URL для навигации. Единственный обязательный проп
  • as — опциональный декоратор пути, отображаемый в адресной строке браузера
  • passHref — указывает Link передать проп href дочернему компоненту. По умолчанию имеет значение false
  • prefetch — предварительная загрузка страницы в фоновом режиме. По умолчанию — true. Предварительная загрузка страницы выполняется для любого Link, находящегося в области просмотра (изначально или при прокрутке). Предварительная загрузка может быть отключена с помощью prefetch={false}. Однако даже в этом случае предварительная загрузка выполняется при наведении курсора на ссылку. Для страниц со статической генерацией загружается JSON-файл для более быстрого переключения страницы. Предварительная загрузка выполняется только в производственном режиме
  • replace — заменяет текущее состояние истории вместо добавления нового URL в стек. По умолчанию — false
  • scroll — выполнение прокрутки в верхнюю часть области просмотра. По умолчанию — true
  • shallow — обновление пути текущей страницы без перезапуска getStaticProps, getServerSideProps или getInitialProps. По умолчанию — false
  • locale — по умолчанию к пути добавляется активная локаль. locale позволяет определить другую локаль. Когда имеет значение false, проп href должен включать локаль

Роут с динамическими сегментами


Пример динамического роута для pages/blog/[slug].js:


import Link from 'next/link'

export default function Posts({ posts }) {
  return (
    <ul>
      {posts.map((post) => (
        <li key={post.id}>
          <Link href={`/blog/${encodeURIComponent(post.slug)}`}>
            <a>{post.title}</a>
          </Link>
        </li>
      ))}
    </ul>
  )
}

Кастомный компонент — обертка для тега a


Если дочерним компонентом Link является кастомный компонент, оборачивающий a, Link должен иметь проп passHref. Это необходимо при использовании таких библиотек как styled-components. Без этого тег a не получит атрибут href, что негативно скажется на доступности и SEO.


import Link from 'next/link'
import styled from 'styled-components'

// Создаем кастомный компонент, оборачивающий `a`
const RedLink = styled.a`
  color: red;
`

export default function NavLink({ href, name }) {
  // Добавляем `passHref`
  return (
    <Link href={href} passHref>
      <RedLink>{name}</RedLink>
    </Link>
  )
}

Функциональный компонент


Если дочерним компонентом Link является функция, то кроме passHref, необходимо обернуть ее в React.forwardRef:


import Link from 'next/link'
import { forwardRef } from 'react'

// `onClick`, `href` и `ref` должны быть переданы DOM-элементу
const MyButton = forwardRef(({ onClick, href }, ref) => {
  return (
    <a href={href} onClick={onClick} ref={ref}>
      Кликни
    </a>
  )
})

export default function Home() {
  return (
    <Link href="/about" passHref>
      <MyButton />
    </Link>
  )
}

Объект URL


Link также принимает объект URL. Данный объект автоматически преобразуется в строку:


import Link from 'next/link'

export default function Home() {
  return (
    <ul>
      <li>
        <Link
          href={{
            pathname: '/about',
            query: { name: 'test' },
          }}
        >
          <a>О нас</a>
        </Link>
      </li>
      <li>
        <Link
          href={{
            pathname: '/blog/[slug]',
            query: { slug: 'my-post' },
          }}
        >
          <a>Пост</a>
        </Link>
      </li>
    </ul>
  )
}

В приведенном примере у нас имеются ссылки на:


  • предопределенный роут: /about?name=test
  • динамический роут: /blog/my-post

Замена URL


<Link href="/about" replace>
  <a>О нас</a>
</Link>

Отключение прокрутки


<Link href="/?page=2" scroll={false}>
  <a>Загрузить еще</a>
</Link>

next/image


Обязательные пропы


Компонент Image принимает следующие обязательные пропы:


  • src — статически импортированный файл или строка, которая может быть абсолютной ссылкой или относительным путем в зависимости от пропа loader или настроек загрузки. При использовании ссылок на внешние ресурсы, эти ресурсы должны быть указаны в разделе domains файла next.config.js
  • width — ширина изображения в пикселях: целое число без единицы измерения
  • height — высота изображения в пикселях: целое число без единицы измерения

Опциональные пропы


  • layoutintrinsic | fixed | responsive | fill. Значением по умолчанию является intrinsic
  • loader — кастомная функция для разрешения URL. Установка этого пропа перезаписывает настройки из раздела images в next.config.js. loader принимает параметры src, width и quality

import Image from 'next/image'

const myLoader = ({ src, width, quality }) => `https://example.com/${src}?w=${width}&q=${quality || 75}`

const MyImage = (props) => (
  <Image
    loader={myLoader}
    src="me.png"
    alt=""
    role="presentation"
    width={500}
    height={500}
  />
)

  • sizes — строка, содержащая информацию о ширине изображения на различных контрольных точках. По умолчанию имеет значение 100vw при использовании layout="responsive" или layout="fill"
  • quality — качество оптимизированного изображения: целое число от 1 до 100, где 100 — лучшее качество. Значением по умолчанию является 75
  • priority — если true, изображение будет считаться приоритетным и загружаться предварительно. Ленивая загрузка для такого изображения будет отключена
  • placeholder — заменитель изображения. Возможными значениями являются blur и empty. Значением по умолчанию является empty. Когда значением является blur, в качестве заменителя используется значение пропа blurDataURL. Если значением src является объект из статического импорта и импортированное изображение имеет формат JPG, PNG, WebP или AVIF, blurDataURL заполняется автоматически

Пропы для продвинутого использования


  • objectFit — определяет, как изображение заполняет родительский контейнер при использовании layout="fill"
  • objectPosition — определяет, как изображение позиционируется внутри родительского контейнера при использовании layout="fill"
  • onLoadingComplete — функция, которая вызывается после полной загрузки изображения и удаления заменителя
  • lazyBoundary — строка, определяющая ограничительную рамку для определения пересечения области просмотра с изображением для запуска его ленивой загрузки. По умолчанию имеет значение 200px
  • unoptimized — если true, источник изображения будет использоваться как есть, без изменения качества, размера или формата. Значением по умолчанию является false

Другие пропы


Любые другие пропы компонента Image передаются дочернему элементу img, кроме следующих:


  • style — для стилизации изображения следует использовать className
  • srcSet — следует использовать размеры устройства
  • ref — следует использовать onLoadingComplete
  • decoding — всегда async

Настройки


Настройки для обработки изображений определяются в файле next.config.js.


Домены


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


module.exports = {
  images: {
    domains: ['assets.acme.com']
  }
}

Размеры устройств


Настройка deviceSizes позволяет определить список контрольных точек для ширины устройств потенциальных пользователей. Эти контрольные точки предназначены для предоставления подходящего изображения при использовании layout="responsive" или layout="fill".


// настройки по умолчанию
module.exports = {
  images: {
    deviceSizes: [640, 750, 828, 1080, 1200, 1920, 2048, 3840]
  }
}

Размеры изображений


Настройка imageSizes позволяет определить размеры изображений в виде списка. Этот список объединяется с массивом размеров устройств для формирования полного перечня размеров для генерации набора источников (srcset) изображения.


module.exports = {
  images: {
    imageSizes: [16, 32, 48, 64, 96, 128, 256, 384]
  }
}

next/head


Компонент Head позволяет добавлять элементы в head страницы:


import Head from 'next/head'

export default function IndexPage() {
  return (
    <div>
      <Head>
        <title>Заголовок страницы</title>
        <meta name="viewport" content="initial-scale=1.0, width=device-width" />
      </Head>
      <p>Привет, народ!</p>
    </div>
  )
}

Проп key позволяет дедуплицировать теги:


import Head from 'next/head'

export default function IndexPage() {
  return (
    <div>
      <Head>
        <title>Заголовок страницы</title>
        <meta property="og:title" content="Заголовок страницы" key="title" />
      </Head>
      <Head>
        <meta property="og:title" content="Новый заголовок страницы" key="title" />
      </Head>
      <p>Привет, народ!</p>
    </div>
  )
}

В данном случае будет отрендерен только <meta property="og:title" content="Новый заголовок страницы" key="title" />.


Контент head удаляется при размонтировании компонента.


title, meta и другие элементы должны быть прямыми потомками Head. Они могут быть обернуты в один <React.Fragment> или рендериться из массива.


next/server


Посредники создаются с помощью функции middleware, находящейся в файле _middleware. Интерфейс посредников основан на нативных объектах FetchEvent, Response и Request.


Эти нативные объекты расширены для предоставления большего контроля над формированием ответов на основе входящих запросов.


Сигнатура функции:


import type { NextRequest, NextFetchEvent } from 'next/server'

export type Middleware = (
  request: NextRequest,
  event: NextFetchEvent
) => Promise<Response | undefined> | Response | undefined

Функция не обязательно должна называться middleware. Это всего лишь соглашение. Функция также не обязательно должна быть асинхронной.


NextRequest


Объект NextRequest расширяет нативный интерфейс Request следующими методами и свойствами:


  • cookies — куки запроса
  • nextUrl — расширенный, разобранный объект URL, предоставляющий доступ к таким свойствам, как pathname, basePath, trailingSlash и i18n
  • geo — геоинформация о запросе
    • country — код страны
    • region — код региона
    • city — город
    • latitude — долгота
    • longitude — широта
  • ip — IP-адрес запроса
  • ua — агент пользователя

NextRequest может использоваться вместо Request.


import type { NextRequest } from 'next/server'

NextFetchEvent


NextFetchEvent расширяет объект FetchEvent методом waitUntil.


Данный метод позволяет продолжить выполнение функции после отправки ответа.


Например, waitUntil может использоваться для интеграции с такими системами мониторинга ошибок, как Sentry.


import type { NextFetchEvent } from 'next/server'

NextResponse


NextResponse расширяет Response следующими методами и свойствами:


  • cookies — куки ответа
  • redirect() — возвращает NextResponse с набором перенаправлений (redirects)
  • rewrite() — возвращает NextResponse с набором перезаписей (rewrites)
  • next() — возвращает NextResponse, который является частью цепочки посредников

import { NextResponse } from 'next/server'

next.config.js


Файл next.config.js или next.config.mjs предназначен для кастомной продвинутой настройки Next.js.


Пример next.config.js:


/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  /* настройки */
}

module.exports = nextConfig

Пример next.config.mjs:


/**
 * @type {import('next').NextConfig}
 */
const nextConfig = {
  /* настройки */
}

export default nextConfig

Можно использовать функцию:


module.exports = (phase, { defaultConfig }) => {
  /**
   * @type {import('next').NextConfig}
   */
  const nextConfig = {
    /* настройки */
  }
  return nextConfig
}

phase — это текущий контекст, для которого загружаются настройки. Доступными фазами являются:


  • PHASE_EXPORT
  • PHASE_PRODUCTION_BUILD
  • PHASE_PRODUCTION_SERVER
  • PHASE_DEVELOPMENT_SERVER

Фазы импортируются из next/constants:


const { PHASE_DEVELOPMENT_SERVER } = require('next/constants')

module.exports = (phase, { defaultConfig }) => {
  if (phase === PHASE_DEVELOPMENT_SERVER) {
    return {
      /* настройки для разработки */
    }
  }

  return {
    /* другие настройки */
  }
}

Переменные среды окружения


Пример


module.exports = {
  env: {
    someKey: 'some-value'
  }
}

Использование


export default function Page() {
  return <h1>Значением `someKey` является `{process.env.someKey}`</h1>
}

Результат


export default function Page() {
  return <h1>Значением `someKey` является `some-value`</h1>
}

Заголовки


Настройка headers позволяет устанавливать кастомные HTTP-заголовки для входящих запросов:


module.exports = {
  async headers() {
    return [
      {
        source: '/about',
        headers: [
          {
            key: 'x-custom-header',
            value: 'кастомное значение заголовка',
          },
          {
            key: 'x-another-custom-header',
            value: 'еще одно кастомное значение заголовка'
          }
        ]
      }
    ]
  }
}

headers — асинхронная функция, возвращающая массив объектов со свойствами source и headers:


  • source — адрес входящего запроса
  • headers — массив объектов со свойствами key и value

Дополнительные параметры:


  • basePath: false | undefined — если false, basePath не будет учитываться при совпадении, может использоваться только для внешних перезаписей
  • locale: false | undefined — определяет, должна ли при совпадении включаться локаль
  • has — массив объектов со свойствами type, key и value

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


Поиск совпадения путей


Разрешен поиск совпадения путей. Например, путь /blog/:slug будет совпадать с /blog/hello-world (без вложенных путей):


module.exports = {
  async headers() {
    return [
      {
        source: '/blog/:slug',
        headers: [
          {
            key: 'x-slug',
            value: ':slug', // Совпавшие параметры могут использоваться в качестве значений
          },
          {
            key: 'x-slug-:slug', // Совпавшие параметры могут использоваться в качестве ключей
            value: 'кастомное значение заголовка'
          }
        ]
      }
    ]
  }
}

Поиск всех совпадений путей


Для поиска всех совпадений можно использовать * после параметра, например, /blog/:slug* будет совпадать с /blog/a/b/c/d/hello-world:


module.exports = {
  async headers() {
    return [
      {
        source: '/blog/:slug*',
        headers: [
          {
            key: 'x-slug',
            value: ':slug*',
          },
          {
            key: 'x-slug-:slug*',
            value: 'кастомное значение заголовка'
          }
        ]
      }
    ]
  }
}

Поиск совпадений путей с помощью регулярных выражений


Для поиска совпадений с помощью регулярок используются круглые скобки после параметра, например, /blog/:slug(\\d{1,}) будет совпадать с /blog/123, но не с /blog/abc:


module.exports = {
  async headers() {
    return [
      {
        source: '/blog/:post(\\d{1,})',
        headers: [
          {
            key: 'x-post',
            value: ':post'
          }
        ]
      }
    ]
  }
}

Символы ( ) { } : * + ? считаются частью регулярного выражения, поэтому при использовании в source в качестве обычных символов они должны быть экранированы с помощью \\:


module.exports = {
  async headers() {
    return [
      {
        // Это будет совпадать с `/english(default)/something`
        source: '/english\\(default\\)/:slug',
        headers: [
          {
            key: 'x-header',
            value: 'some-value'
          }
        ]
      }
    ]
  }
}

Поиск совпадения на основе заголовка, куки и строки запроса


Для этого используется поле has. Заголовок будет установлен только при совпадении полей source и has.


Значением has является массив объектов со следующими свойствами:


  • type: stringheader | cookie | host | query
  • key: string — ключ для поиска совпадения
  • value: string | undefined — значение для проверки. Если undefined, любое значение будет совпадать. Для захвата определенной части значения может использоваться регулярное выражение. Например, если для hello-world используется значение hello-(?<param>.*), world можно использовать в качестве значения destination как :param:

module.exports = {
  async headers() {
    return [
      // Если имеется заголовок `x-add-header`,
      // будет установлен заголовок `x-another-header`
      {
        source: '/:path*',
        has: [
          {
            type: 'header',
            key: 'x-add-header',
          },
        ],
        headers: [
          {
            key: 'x-another-header',
            value: 'hello'
          }
        ]
      },
      // Если совпадают `source`, `query` и `cookie`,
      // будет установлен заголовок `x-authorized`
      {
        source: '/specific/:path*',
        has: [
          {
            type: 'query',
            key: 'page',
            // Значение `page` будет недоступно в заголовке,
            // поскольку значение указано и при этом
            // не используются именованная группа захвата, например `(?<page>home)`
            value: 'home',
          },
          {
            type: 'cookie',
            key: 'authorized',
            value: 'true',
          },
        ],
        headers: [
          {
            key: 'x-authorized',
            value: ':authorized'
          }
        ]
      },
      // Если имеется заголовок `x-authorized` и он
      // содержит искомое значение, будет установлен заголовок `x-another-header`
      {
        source: '/:path*',
        has: [
          {
            type: 'header',
            key: 'x-authorized',
            value: '(?<authorized>yes|true)',
          },
        ],
        headers: [
          {
            key: 'x-another-header',
            value: ':authorized'
          }
        ]
      },
      // Если значением хоста является `example.com`,
      // будет установлен данный заголовок
      {
        source: '/:path*',
        has: [
          {
            type: 'host',
            value: 'example.com',
          },
        ],
        headers: [
          {
            key: 'x-another-header',
            value: ':authorized'
          }
        ]
      }
    ]
  }
}

basePath и i18n


При наличии basePath, его значение автоматически добавляется к значению source, если не определено basePath: false:


module.exports = {
  basePath: '/docs',

  async headers() {
    return [
      {
        source: '/with-basePath', // становится /docs/with-basePath
        headers: [
          {
            key: 'x-hello',
            value: 'world'
          }
        ]
      },
      {
        source: '/without-basePath', // не модифицируется
        headers: [
          {
            key: 'x-hello',
            value: 'world'
          }
        ],
        basePath: false
      }
    ]
  }
}

При наличии i18n к значению source автоматически добавляются значения locales, если не определено locale: false — в этом случае значение source должно включать префикс локали:


module.exports = {
  i18n: {
    locales: ['en', 'fr', 'de'],
    defaultLocale: 'en',
  },

  async headers() {
    return [
      {
        source: '/with-locale', // автоматическая обработка всех локалей
        headers: [
          {
            key: 'x-hello',
            value: 'world'
          }
        ]
      },
      {
        // ручная установка локали
        source: '/nl/with-locale-manual',
        locale: false,
        headers: [
          {
            key: 'x-hello',
            value: 'world'
          }
        ]
      },
      {
        // это совпадает с `/`, поскольку `en` является `defaultLocale`
        source: '/en',
        locale: false,
        headers: [
          {
            key: 'x-hello',
            value: 'world'
          }
        ]
      },
      {
        // это преобразуется в /(en|fr|de)/(.*), поэтому не будет совпадать с роутами верхнего уровня, такими как
        // `/` или `/fr`, а с `/:path*` будет
        source: '/(.*)',
        headers: [
          {
            key: 'x-hello',
            value: 'worlld'
          }
        ]
      }
    ]
  }
}

Дополнительная настройка Webpack


Перед кастомизацией Вебпака убедитесь, что Next.js не имеет поддержки необходимого функционала.


Пример определения функции для настройки webpack:


module.exports = {
  webpack: (config, { buildId, dev, isServer, defaultLoaders, webpack }) => {
    // не забудьте вернуть модифицированный конфиг
    return config
  }
}

Данная функция выполняется дважды: один раз для сервера и еще один для клиента. Это позволяет разделять настройки с помощью свойства isServer.


Вторым аргументом, передаваемым функции является объект со следующими свойствами:


  • buildId: string — уникальный идентификатор сборки
  • dev: boolean — индикатор компиляции в режиме разработки
  • isServer: boolean — если true, значит, выполняется компиляция для сервера
  • defaultLocales: object — дефолтные лоадеры:
    • babel: object — дефолтные настройки babel-loader

Пример использования defaultLoaders.babel:


module.exports = {
  webpack: (config, options) => {
    config.module.rules.push({
      test: /\.mdx/,
      use: [
        options.defaultLoaders.babel,
        {
          loader: '@mdx-js/loader',
          options: pluginOptions.options
        }
      ]
    })

    return config
  }
}

Настройка distDir позволяет определять директорию для сборки:


module.exports = {
  distDir: 'build'
}

Настройка generateBuildId позволяет определять идентификатор сборки:


module.exports = {
  generateBuildId: async () => {
    // Здесь можно использовать, например, хеш последнего гит-коммита
    return 'my-build-id'
  }
}

Пример отключения проверки кода с помощью eslint при сборке проекта:


module.exports = {
  eslint: {
    ignoreDuringBuilds: true
  }
}

Пример отключения typescript при сборке проекта:


module.exports = {
  typescript: {
    ignoreBuildErrors: true
  }
}

Пожалуй, на сегодня это все.


Благодарю за внимание и хорошего дня!




Комментарии (1)


  1. noodles
    20.11.2021 14:08
    +2

    Как известно, основным недостатком SPA являются проблемы с индексацией страниц таких приложений поисковыми роботами, что негативно влияет на SEO.

    Это не проблемы SPA. Это проблемы тех, кто принял решение делать контентную seo-чувствительную чать продукта по технологии spa.

    SPA - это как правило приложения, которые спрятаны за аутентификацией.
    Или же ЗА кнопками "начать", "запустить", "войти", "заказать", "создать" и т.д.
    ДО этих кнопок как правило находится seo-важный контент, который продаёт/рекламирует/рассказывает про продукт. Его не нужно делать как spa.