В данной небольшой заметке я бы хотел показать, как можно достаточно быстро развернуть и настроить проект на NextJS 11

Штатным и самым быстрым способом создания проекта является использование штатной утилиты create-next-app, которая, по аналогии со всем известной CRA создаст проект за считанные секунды.

Я же хочу показать другой путь - чуть более сложный, но позволяющий (через некоторое количество ручной работы) намного лучше понять, из чего проект строится и как настраивается, как устанавливается и настраивается компилятор TypeScript и линтер ESLint.

Немного о фреймворке NextJS

NextJS (https://nextjs.org/) - это javascript-фреймворк для создания универсальных веб-приложений (таких, которые могут рендерить html как на клиентской стороне, так и на серверной). Что он дает?

  • Простой, интуитивно понятный роутинг по страницам приложения, основанный на файлах

  • Поддержку SSR - server-side rendering, что позволяет разгрузить клиентские устройства и каналы связи ценой нагрузки на сервер. Клиенты на слабых устройствах (в том числе, и в особенности - мобильных) и клиенты на слабых и нестабильных каналах связи получают намного более высокий user experience за счет в разы более быстрого отображения контента

  • Поддержку SSG - static site generation, что позволяет много всего интересного: корректно индексировать контент сайта без танцев с бубном вокруг nginx, эффективно кэшировать данные и пользоваться сетями доставки контента (CDN)

  • Поддержку фичей Webpack 5 (который во фреймворке используется как бортовой сборщик)

  • Возможность в рамках одного проекта создавать как фронтендовую часть, так и бэкендовую (за счет создания api-эндпоинтов)

  • Поддержку TypeScript "из коробки"

  • Оптимизацию изображений (и использование изображений как React-компонентов)

  • CSS Modules, Sass/SCSS (через установку препроцессора)

  • и много всего интересного, о чем можно прочитать на официальном сайте и в документации

Результаты (с более-менее последовательной и соответствующей тексту пошаговой разбивкой в виде коммитов) - в репозитории

Создаем проект, устанавливаем react и next

Для работы NextJS требует Node.js версии 12.0 и выше, но я рекомендую использовать релиз Node.js 14.17 как наиболее стабильный и вообще LTS.

Первым шагом необходимо инициализировать проект и установить фреймворки

mkdir nextjs-app && cd nextjs-app
npm init -y
npm i react react-dom next

После установки файл package.json проекта будет выглядеть примерно так:

{
  "name": "nextjs-app",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "next": "^11.0.1",
    "react": "^17.0.2",
    "react-dom": "^17.0.2"
  }
}

Далее, необходимо в секции scripts указать все скрипты, необходимые для проекта - запуска dev-сервера, сборки, запуска собранного проекта и запуска линтера (небольшой тюннинг линтера будет показан чуть позже):

"scripts": {
  "dev": "next dev",
  "build": "next build",
  "start": "next start",
  "lint": "next lint"
}

Если вдруг у вас (как и у меня) порт по умолчанию (3000) перманентно чем-то занят, можно переопределить его ключом -p:

"scripts": {
  "dev": "next dev -p 9993",
  "build": "next build",
  "start": "next start",
  "lint": "next lint"
}

Базовая преднастройка закончена, теперь пришло время установить и настроить typescript

TypeScript

Устанавливаем TypeScript и типы для React и его DOM

npm i -D typescript @types/react @types/react-dom

Далее необходимо в корне проекта создать файл конфигурации компилятора - tsconfig.json

Я использую следующую конфигурацию (стоит ли ее использовать - вопрос вкуса):

{
  "compilerOptions": {
    "target": "es5",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": false,
    "skipLibCheck": true,
    "strict": true,
    "strictPropertyInitialization": false,
    "forceConsistentCasingInFileNames": true,
    "noEmit": true,
    "esModuleInterop": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "jsx": "preserve"
  },
  "include": [
    "**/*.ts",
    "**/*.tsx"
  ],
  "exclude": [
    "node_modules"
  ]
}
Немного про опции файла конфигурации
  • target - целевая версия ECMAScript. Для поддержки более старых браузеров можно использовать версию ES5 (как в примере). Подробнее

  • lib - набор библиотек определений типов, нужных для работы приложения. Наше приложение работает в браузере, поэтому нам нужны dom (основные типы DOM браузера), dom.iterable (итерационные типы DOM) и esnext (современные API ECMAScript). Подробнее

  • allowJs - разрешен ли импорт JS-файлов в TS- и TSX-файлы. Подробнее

  • skipLibCheck - пропускать ли полную проверку *.d.ts-файлов (файлов определений). Если true, компилятор будет проверять только библиотеки, непосредственно импортируемые в проекте. Подробнее

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

  • strictPropertyInitialization - отключает проверку объявленного, но не установленного в конструкторе класса свойства. Подробнее

  • forceConsistentCasingInFileNames - включает форсированную чувствительность к регистру имени файла. Актуально, если разработчики работают в разных операционных системах. Подробнее

  • noEmit - не создавать результирующие файлы компиляции. У нас итоговой сборкой рулит NextJS и Webpack, поэтому промежуточные файлы нам не нужны. Подробнее

  • esModuleInterop - позволяет импортировать файлы по стандартам ES6. Подробнее

  • module - используемая модульная система. Подробнее

  • moduleResolution - используемая стратегия разрешения импортов. Подробнее

  • resolveJsonModule - разрешен ли импорт JSON-файлов. Подробнее

  • isolatedModules - нужно ли компилятору обрабатывать каждый файл как изолированный модуль. Если в файле не хватает импортов и определений (или по какой-то еще причине компилятор не будет понимать, что это за класс/тип/ ит.д.) - при компиляции будет выдаваться ошибка. Подробнее

  • jsx - какой стратегии будет придерживаться компилятор, когда встретит JSX-код. Он, например, может пытаться заменять JSX на эквивалентные конструкции вида React.createElement или, как в нашем случае - выдавать JSX-код без изменений. Подробнее

  • include - какие файлы будут использоваться при компиляции

  • exclude - а какие - не будут

Конфигурация NextJS, структура директорий и запуск

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

Для начала в корне проекта создадим файл определений - next-env.d.ts, содержащий референсы на определения типов NextJS

/// <reference types="next" />
/// <reference types="next/types/global" />
/// <reference types="next/image-types/global" />

и конфигурационный файл - next.config.js

module.exports = {
  reactStrictMode: true,
}

в котором мы указываем, что React должен находиться в строгом режиме (в этом режиме любые небезопасные и некорректные с точки зрения React моменты вроде сайд-эффектов будут считаться ошибками)

Теперь необходимо воссоздать для NextJS его привычную структуру директорий. По умолчанию он требует наличия директории pages/ в корне проекта. Данная директория используется для работы роутера: каждый файл в ней является обработчиком какого-то роута (файл index.tsx, например, будет индексным файлом и отрабатывать на роуте /).

Директория pages в корне проекта - это удобно, но только пока проект пуст. Постепенно к ней добавится куча других директорий - components, utils, helpers и прочего. Их нельзя прятать внутрь директории pages - там NextJS ожидает увидеть только обработчики, и любой файл/директорию будет интерпретировать именно так, рождая ошибки. Поэтому NextJS позволяет создать (но при работе create-next-app, что характерно, сам не создает) директорию src и в нее уже спрятать весь код приложения. Сделаем именно так:

mkdir -p src/pages

В директории pages создадим файл _app.tsx (дефолтный компонент приложения). В нем мы создаем базовую структуру компонента приложения и используем встроенный компонент NextJS - Head, чтобы передать title в заголовок страницы

import { AppProps } from 'next/dist/next-server/lib/router/router';
import React from 'react';
import Head from 'next/head';

const MyApp = ({ Component, pageProps }: AppProps): JSX.Element => {
  return (
    <>
      <Head>
        <title>NextJS App From Scratch</title>
      </Head>
      <Component {...pageProps} />
    </>
  );
};

export default MyApp;

А теперь - создаем индексный модуль index.tsx с компонентом домашней страницы

const Home = (): JSX.Element => {
  return (
    <div>
      Hello, NextJS!
    </div>
  );
};

export default Home;

Теперь можно запустить проект командой

npm run dev

и радоваться "Hello, NextJS!" в браузере

Установка, настройка и запуск линтера

Линтинг - это хорошо. Линтинг позволяет нам не забывать про импорты, типы и точки с запятой. Давайте настроим линтинг.

Под капотом NextJS использует в качестве линтера, как это ни странно, ESLint. Для корректной работы ESlint с typescript-кодом необходимо установить несколько плагинов и создать файл конфигурации .eslintrc

Для начала - установка плагинов и парсеров

npm i -D eslint eslint-config-next
npm i -D @typescript-eslint/eslint-plugin @typescript-eslint/parser

И теперь сконфигурируем его

{
	"root": true,
	"parser": "@typescript-eslint/parser",
	"plugins": [
		"@typescript-eslint"
	],
	"rules": {
		"semi": "off",
		"@typescript-eslint/semi": [
			"error"
		],
		"@typescript-eslint/no-empty-interface": [
			"error",
			{
				"allowSingleExtends": true
			}
		]
	},
	"extends": [
		"next",
		"next/core-web-vitals",
		"eslint:recommended",
		"plugin:@next/next/recommended",
		"plugin:@typescript-eslint/eslint-recommended",
		"plugin:@typescript-eslint/recommended"
	]
}

Здесь мы устанавливаем в качестве парсера typescript-eslint/parser, в правилах отключаем стандартную обработку точек с запятой и назначаем свою (мне нравится, когда линтер ругается на отсутствие точки с запятой с помощью error, но вы можете поменять typescript-eslint/semi на warn, например). Кроме того, в секции extends мы подключаем много разных конфигураций как из NextJS, так и из плагинов - так мы будем получать рекомендации по созданию качественного кода. На данном этапе IDE (особенно если у вас VSCode, как у меня) уже должна отображать результаты линтинга корректно.

Для запуска проверки проекта линтером (например, перед сборкой) есть два пути: можно пользоваться штатной оберткой next lint (которая есть просто запуск eslint с некоторыми предопределенными в глубинах NextJS настройками), либо - запускать eslint напрямую, указав директорию, в которой он должен анализировать код. Я предпочитаю второй вариант (одной из причин является то, что next lint по умолчанию проверяет только директории pages/, components/ и lib/ проект, а чтобы добавить к ним, например, utils/, необходимо писать длинную портянку ключей next lint -d pages -d utils ....)

Посему, необходимо обновить скрипт линтинга в файле package.json на следующий:

"scripts": {
  "dev": "next dev -p 9993",
  "build": "next build",
  "start": "next start",
  "lint": "eslint src/**/*.{ts,tsx}"
},

Этим мы говорим, что хотим натравить eslint на все директории внутри src/, в которых лежат файлы ts и tsx.

После этого запуск npm run lint будет выводить нам все ошибки во всех typescript-файлах проекта

Настройка использования SVG

По умолчанию NextJS умеет работать с изображениями, как с компонентами. Что, надо признать, достаточно удобно. Однако, если возникнет необходимость использовать векторные изображения в формате SVG, потребуется некоторый тюннинг конфигурационных файлов. Сделаем это загодя.

Благодаря Webpack в качестве бортового сборщика подключение загрузчика SVG не представляет серьезной проблемы. Сначала необходимо установить пакет загрузчика в dev-зависимости:

npm i -D @svgr/webpack

и затем - добавить webpack-правило для этого загрузчика в конфигурационный файл сборщика (next.config.js)

module.exports = {
  reactStrictMode: true,
  webpack(config, options) {
    config.module.rules.push({
      test: /\.svg$/i,
      issuer: { and: [/\.(ts)x?$/] },
      use: [
        {
          loader: "@svgr/webpack",
          options: {
            svgoConfig: { plugins: [{ removeViewBox: false }] },
          },
        },
      ],
    });

    return config;
  }
}

Если бы мы использовали JS вместо TypeScript, на этом настройка SVG бы завершилась. Но нет, за типизацию надо платить.

Для того, чтобы компилятор подтягивал к компоненту SVG-изображения корректные типы, необходимо внести изменения в файл определений проекта - next-env.d.ts. И в NextJS 10 мы бы так и сделали. Но в 11 версии данный файл стал автогенерируемым и сборщик перетирает его каждый раз при сборке, что несколько усложняет дело. Нужно создавать отдельный файл определений.

Создаем в корне проекта директорию @types, а в ней - файл images.d.ts следующего содержания:

declare module "*.svg" {
  const component: React.FC<React.SVGProps<SVGAElement>>;
  
  export default component;
}

Этим мы сообщаем компилятору, что модули с расширением *.svg - это не объекты типа any (как в типах по-умолчанию), а вполне себе функциональные React-компоненты.

После этого компилятор подтянет для компонентов-изображений корректный тип

Итоги

Следуя данной заметке, можно достаточно быстро (ну, второй раз выходит и правда быстро!) создать полностью настроенный NextJS-проект с подключенным и настроенным TypeScript, реализованной поддержкой SVG и линтингом.

Спасибо за внимание!

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