Разработка клиент-серверного приложения с помощью Next.js и TypeScript. Часть 2. Разработка клиента

byf9qu-qkjvc3cpega2osikmrcq.png


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

В этой серии из 2 статей-туториалов мы с вами продолжаем разрабатывать клиент-серверное (фуллстек — fullstack) приложение с помощью Next.js и TypeScript.


  1. Наше приложение будет представлять собой блог — относительно полноценную платформу для публикации, редактирования и удаления постов.
  2. Мы реализовали собственный сервис аутентификации на основе JSON Web Tokens и HTTP-куки.
  3. Данные пользователей и постов будут храниться в реляционной базе данных SQLite.

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

Обратите внимание: данный туториал рассчитан на разработчиков, которые имеют некоторый опыт работы с React и Node.js.

Для тех, кого интересует только код, вот соответствующий репозиторий.

Интересно? Тогда прошу под кат.


Настройка проекта


Why Did You Render

Why Did You Render — утилита для отладки React-приложений, позволяющая определить причину повторного рендеринга компонента. Для того, чтобы иметь возможность использовать эту утилиту в Next.js-приложении необходимо сделать 2 вещи:


  • настроить пресет (preset) транспилятора Babel;
  • инициализировать утилиту и импортировать ее в основной компонент приложения.

Настраиваем пресет Babel в файле babel.config.js в корне проекта:

module.exports = function (api) {
  const isServer = api.caller((caller) => caller?.isServer)
  const isCallerDevelopment = api.caller((caller) => caller?.isDev)

  // пресеты
  const presets = [
    [
      'next/babel',
      {
        'preset-react': {
          runtime: 'automatic',
          importSource:
            // код wdyr должен выполняться только на клиенте
            // и только в режиме разработки
            !isServer && isCallerDevelopment
              ? '@welldone-software/why-did-you-render'
              : 'react'
        }
      }
    ]
  ]

  return { presets }
}

Инициализируем WDYR в файле utils/wdyr.ts:

import React from 'react'

// код выполняется только в режиме разработки
// и только на клиенте
if (process.env.NODE_ENV === 'development' && typeof document !== 'undefined') {
  const whyDidYouRender = require('@welldone-software/why-did-you-render')
  whyDidYouRender(React, {
    trackAllPureComponents: true
  })
}

export {}

Импортируем WDYR в файле _app.tsx:

import '@/utils/wdyr'

После этого для отладки в файле компонента достаточно добавить такую строчку:

SomeComponent.whyDidYouRender = true


Material UI

Material UI — самая популярная библиотека компонентов React. Для ее правильного использования в Next.js-приложении необходимо сделать 2 вещи:


  • настроить плагин (plugin) Babel;
  • настроить кэш Emotion — решения CSS-в-JS, которое используется MUI для стилизации компонентов.

Настраиваем плагин Babel в файле babel.config.js:

module.exports = function (api) {
  // пресеты
  // ...

  // плагины
  const plugins = [
    [
      'babel-plugin-import',
      {
        libraryName: '@mui/material',
        libraryDirectory: '',
        camel2DashComponentName: false
      },
      'core'
    ]
  ]

  return { presets, plugins }
}

Для чего нужен этот плагин? Для уменьшения размера клиентской сборки. Проблема в том, что при импорте компонента MUI по названию, например:

import { Button } from '@mui/material'

В сборку попадет весь пакет @mui/material, т.е. все компоненты MUI независимо от того, используются они в приложении или нет. babel-plugin-import преобразует именованный импорт в дефолтный, т.е. на выходе мы получаем, например:

import Button from '@mui/material/Button'

Таким образом, в сборку попадают только компоненты, которые используются в приложении.

Настройка кэша Emotion необходима для предотвращения вспышки нестилизованного контента (flash of unstyled content), например, когда сначала загружаются дефолтные стили браузера и только потом стили MUI, а также для обеспечения возможности легкой перезаписи стилей MUI, т.е. кастомизации компонентов (источник решения).

Определяем утилиту для создания кэша Emotion в файле utils/createEmotionCache.ts:

import createCache from '@emotion/cache'

// Создаем на клиенте тег `meta` с `name="emotion-insertion-point"` в начале  .
// Это позволяет загружать стили MUI в первоочередном порядке.
// Это также позволяет разработчикам легко перезаписывать стили MUI, например, с помощью модулей CSS.
export default function createEmotionCache() {
  let insertionPoint

  if (typeof document !== 'undefined') {
    const emotionInsertionPoint = document.querySelector(
      'meta[name="emotion-insertion-point"]'
    )
    insertionPoint = emotionInsertionPoint ?? undefined
  }

  return createCache({ key: 'mui-style', insertionPoint })
}

Кэш необходимо создавать при запуске приложения как на сервере, так и на клиенте. Настраиваем рендеринг документа в файле _document.tsx (создание кэша на сервере):

import createEmotionCache from '@/utils/createEmotionCache'
import createEmotionServer from '@emotion/server/create-instance'
import Document, {
  DocumentContext,
  Head,
  Html,
  Main,
  NextScript
} from 'next/document'

export default function MyDocument(props: any) {
  return (
    
      
        
        {/* дефолтным шрифтом MUI является Roboto, мы будем использовать Montserrat */}
        
        
        
        {/* ! */}
        
        {props.emotionStyleTags}
      
      
        
) } // `getInitialProps` принадлежит `_document` (а не `_app`), // это совместимо с генерацией статического контента (SSG). MyDocument.getInitialProps = async (docContext: DocumentContext) => { // Порядок разрешения // // На сервере: // 1. app.getInitialProps // 2. page.getInitialProps // 3. document.getInitialProps // 4. app.render // 5. page.render // 6. document.render // // На сервере в случае ошибки: // 1. document.getInitialProps // 2. app.render // 3. page.render // 4. document.render // // На клиенте: // 1. app.getInitialProps // 2. page.getInitialProps // 3. app.render // 4. page.render const originalRenderPage = docContext.renderPage // Кэш Emotion можно распределять между всеми запросами SSR для повышения производительности. // Однако это может привести к глобальным побочным эффектам. const cache = createEmotionCache() const { extractCriticalToChunks } = createEmotionServer(cache) docContext.renderPage = () => originalRenderPage({ enhanceApp: (App: any) => function EnhanceApp(props) { return } }) const docProps = await Document.getInitialProps(docContext) // Важно. Это не позволяет Emotion рендерить невалидный HTML. // См. https://github.com/mui/material-ui/issues/26561#issuecomment-855286153 const emotionStyles = extractCriticalToChunks(docProps.html) const emotionStyleTags = emotionStyles.styles.map((style) => (