Я працював над додатками на Next.js, які масштабується до понад 100 тисяч активних користувачів щомісяця, а також над посадковими сторінками з мільйонами відвідувачів щомісяця. У цій статті я поділюся всіма уроками, які я засвоїв через численні ітерації.
Незалежно від того, чи ви працюєте в маленькій команді з одного, двох чи трьох розробників, чи велику команду на масштабному проекті Next.js з кількома командами, що працюють над одним кодовим базисом, надзвичайно важливо з самого початку правильно побудувати основу вашого додатку.
Навіть якщо ви працюєте над існуючим проектом, ви знайдете деякі корисні уроки, які можна застосувати до вашого додатку вже сьогодні.
Дозвольте провести вас крок за кроком і показати всі налаштування та пакети, які варто встановити, і пояснити, чому вони корисні, щоб ваш додаток масштабувався без проблем.
Ініціалізація вашого проекту
Почніть з створення нового проекту на Next.js.
npx create-next-app@latest
Можливо, вам буде запропоновано встановити останню версію create-next-app
, просто натисніть "так".
Need to install the following packages:
[email protected]
Ok to proceed? (y)
Після цього налаштуйте ваш проект, відповідаючи "так" на всі питання (TypeScript, Tailwind, app router).
✔ **What is your project named?** … reactsquad-production
✔ **Would you like to use** **TypeScript**? … No / Yes
Yes
✔ **Would you like to use** **ESLint**? … No / Yes
Yes
✔ **Would you like to use** **Tailwind CSS**? … No / Yes
Yes
✔ **Would you like to use** **`src/` directory**? … No / Yes
Yes
✔ **Would you like to use** **App Router**? (recommended) … No / Yes
Yes
✔ **Would you like to customize the default** **import alias** **(@/*)?** … No / No
Далі перейдіть до каталогу вашого проекту та відкрийте його у вашому улюбленому редакторі.
$ cd nextjs-for-production
~/dev/nextjs-for-production (main) 🤯
$ cursor .
Запуск сервера для розробки
Вам потрібно перевірити, чи все налаштувалося правильно.
1.
Запустіть команду npm run dev
, щоб стартувати сервер для розробки.
2. Відвідайте http://localhost:3000
, щоб побачити ваш додаток.
Перевірка типів з TypeScript
У вашому проекті вже налаштовано TypeScript, але також варто додати явну команду до вашого файлу package.json
, яка перевіряє всі файли на помилки типів.
"type-check": "tsc -b"
Цю команду ви будете використовувати пізніше разом з іншими автоматизованими перевірками статичного аналізу.
Форматування коду
Коли ви працюєте в команді з великою кількістю розробників над проектом, важливо стандартизувати спосіб, яким кожен пише код.
Обговоріть з вашою командою такі питання, як використання крапок з комою, стиль лапок та табуляція проти пробілів.
Потім використовуйте інструменти для автоматичного застосування вашого стилю коду та форматування.
Є два інструменти для цього: Prettier та ESLint.
Prettier
Prettier — це форматувальник коду з чіткими правилами, який усуває обговорення стилю під час рев’ю коду.
Встановіть Prettier разом з його плагіном для Tailwind.
npm install --save-dev prettier prettier-plugin-tailwindcss
Створіть файл prettier.config.js
з вашими бажаними налаштуваннями.
module.exports = {
arrowParens: 'avoid',
bracketSameLine: false,
bracketSpacing: true,
htmlWhitespaceSensitivity: 'css',
insertPragma: false,
jsxSingleQuote: false,
plugins: ['prettier-plugin-tailwindcss'],
printWidth: 80,
proseWrap: 'always',
quoteProps: 'as-needed',
requirePragma: false,
semi: true,
singleQuote: true,
tabWidth: 2,
trailingComma: 'all',
useTabs: false,
};
Додайте скрипт форматування до вашого файлу package.json
.
"format": "prettier --write .",
Запустіть форматувальник, щоб застосувати ваш стиль.
$ npm run format
\> [email protected] format
\> prettier --write .
next.config.mjs 4ms (без змін)
package-lock.json 54ms (без змін)
package.json 1ms (без змін)
postcss.config.mjs 2ms (без змін)
README.md 20ms (без змін)
src/app/globals.css 17ms (без змін)
src/app/layout.tsx 30ms (без змін)
src/app/page.tsx 11ms (без змін)
tailwind.config.ts 2ms (без змін)
tsconfig.json 2ms (без змін)
Ваші файли тепер «гарніше», але вам також хочеться використовувати ESLint.
ESLint
ESLint може перевіряти ваш код на наявність як стильових, так і логічних проблем.
Встановіть ESLint та його плагіни, такі як unicorn, playwright та import sort.
npm install --save-dev @typescript-eslint/parser eslint-plugin-unicorn eslint-plugin-import eslint-plugin-playwright eslint-config-prettier eslint-plugin-prettier eslint-plugin-simple-import-sort
Оновіть ваш .eslintrc.json
{
"extends": [
"next/core-web-vitals",
"plugin:unicorn/recommended",
"plugin:import/recommended",
"plugin:playwright/recommended",
"plugin:prettier/recommended"
],
"plugins": ["simple-import-sort"],
"rules": {
"simple-import-sort/exports": "error",
"simple-import-sort/imports": "error",
"unicorn/no-array-callback-reference": "off",
"unicorn/no-array-for-each": "off",
"unicorn/no-array-reduce": "off",
"unicorn/prevent-abbreviations": [
"error",
{
"allowList": {
"e2e": true
},
"replacements": {
"props": false,
"ref": false,
"params": false
}
}
]
},
"overrides": [
{
"files": ["\*.js"],
"rules": {
"unicorn/prefer-module": "off"
}
}
]
}
Плагіни цього посібника пропонують різні функції.
Для детальніших описів відвідайте їхні відповідні сторінки на GitHub.
Але коротко, ці інструменти забезпечують дотримання стандартів кодування, організовують імпорти та гарантують правильне використання сучасних можливостей JavaScript. Оскільки ESLint та Prettier можуть конфліктувати, ця конфігурація дозволяє їм працювати разом безперешкодно.
Плагіни також допомагають запобігати помилкам і підтримувати стиль коду консистентним, особливо з такими інструментами, як Vitest та Playwright.
Додайте скрипт для лінтингу до вашого package.json
.
"lint:fix": "next lint --fix",
Запустіть його, щоб відформатувати всі файли відповідно до> [email protected] lint:fix
> next lint --fix
✔ Немає попереджень або помилок ESLint
```
Якщо ви отримаєте попередження про версію TypeScript, можете ігнорувати це.
Примітка: На момент написання цієї статті доступна версія ESLint 9, але цей посібник використовує ESLint 8, оскільки багато плагінів ще не підтримують останню версію.
Commitlint
Коли ви працюєте в великій команді, корисно також впровадити послідовні повідомлення для комітів, щоб історія проекту залишалася зрозумілою. Вибираючи правильні стандарти, ви можете автоматизувати створення changelog та випусків з правильним семантичним версіонуванням.
Встановіть Commitlint та його необхідні конфігурації.
Це включає Husky, який допомагає керувати Git хуками.
npm install --save-dev @commitlint/cli@latest @commitlint/config-conventional@latest husky@latest
Ініціалізуйте Husky у вашому проекті, щоб налаштувати базову конфігурацію.
npx husky-init && npm install
Додайте хуки для автоматизації лінтингу та перевірки типів перед кожним комітом, а також налаштуйте процес написання повідомлень комітів.
npx husky add .husky/pre-commit 'npm run lint && npm run type-check'
npx husky add .husky/prepare-commit-msg 'exec \< /dev/tty && npx cz --hook || true'
Хук pre-commit
виконується після git commit
, але до того, як коміт буде завершено, і запускає лінтинг та перевірку типів вашого коду.
Хук prepare-commit-msg
виконується після ініціації git commit
, але до того, як відкриється редактор повідомлення коміту. Він запускає CLI інструмент commitizen
, щоб дозволити вам створювати звичайні повідомлення комітів.
Ви дізнаєтесь, як використовувати цей хук трохи пізніше.
Видаліть рядок, що містить npm test
, з файлу .husky/_/pre-commit
.
Переконайтесь, що ці скрипти є виконуваними.
chmod a+x .husky/pre-commit
chmod a+x .husky/prepare-commit-msg
Тут chmod
означає "change mode" (зміна режиму). Ця команда дозволяє змінювати права доступу або режими файлів чи директорій в операційних системах на основі Unix. Аргумент a+x
додає права на виконання для всіх користувачів.
Встановіть Commitizen, який надає CLI інтерфейс для створення звичайних повідомлень комітів.
npm install --save-dev commitizen cz-conventional-changelog
Налаштуйте Commitizen у вашому package.json
, щоб використовувати стандарт звичайних змін (conventional changelog).
"config": {
"commitizen": {
"path": "cz-conventional-changelog"
}
},
Стандарт звичайних змін (conventional changelog) — це специфікація для додавання людського та машиночитного змісту до повідомлень комітів.
Це призначено для автоматизації створення changelog-ів та релізів на основі історії Git вашого проєкту.
У майбутньому статті буде докладно пояснено цей стандарт. Він включений у цей посібник, тому що важливо правильно налаштувати його з самого початку. Ви можете використовувати його і отримувати користь без необхідності глибокого розуміння.
Створіть файл commitlint.config.cjs
з правилами, які підходять вашій команді.
Ця конфігурація гарантує, що ваші повідомлення про коміти будуть узгодженими і відповідатимуть змінам, які були внесені.
const config = {
extends: ['@commitlint/config-conventional'],
rules: {
'references-empty': [1, 'never'],
'footer-max-line-length': [0, 'always'],
'body-max-line-length': [0, 'always'],
},
};
module.exports = config;
Запустіть наступну команду, щоб почати створювати повідомлення про коміти за допомогою покрокового CLI.
$ git add --all
$ npx cz
[email protected], [email protected]
? Виберіть тип зміни, яку ви
комітуєте: (Використовуйте стрілки)
❯ feat: Нова функція
fix: Виправлення помилки
docs: Тільки зміни в документації
style: Зміни, які не впливають на
значення коду (пробіли, форматування,
відсутні крапки з комою тощо)
Команда cz
ставить кілька запитань і потім самостійно створює ваше повідомлення про коміт.
? Виберіть тип зміни, яку ви комітуєте: feat: Нова функція
? Який обсяг цієї зміни (наприклад...
назва компонента або файлу): (натисніть Enter, щоб пропустити)
? Напишіть короткий опис зміни в наказовому способі (макс. 63 символи):
(61) налаштування перевірки типів у TS, ESLint, Prettier, Commitlint і Husky
? Напишіть більш детальний опис зміни: (натисніть Enter, щоб пропустити)
Налаштовує скрипт у package.json для перевірки типів TS. Налаштовує Prettier і ESLint з гарними правилами. Конфігурує Husky і налаштовує Commitizen, використовуючи стандарт звичайних комітів.
? Чи є які-небудь зміни, які можуть порушити сумісність? Ні
? Чи впливає ця зміна на якісь відкриті питання? Ні
[main eb69ccd] feat(налаштування перевірок статичного аналізу): налаштування перевірок типів TS, ESLint, Prettier, Commitlint і Husky
7 файлів змінено, 3108 вставок(+), 159 видалень(-)
створено файл 100755 .husky/pre-commit
створено файл 100755 .husky/prepare-commit-msg
створено файл 100644 commitlint.config.cjs
Поки ви відповідаєте на всі питання, хуки Husky автоматично виконуватимуть перевірки типів TypeScript і лінтинг.
Структура папок
Зазвичай є два популярних способи організувати ваш код: групування за типом або групування за функціональністю.
Групування за типом виглядає так:
.
├── components
│ ├── todos
│ └── user
├── reducers
│ ├── todos
│ └── user
└── tests
├── todos
└── user
А групування за функціональністю виглядає так:
.
├── todos
│ ├── component
│ ├── reducer
│ └── test
└── user
├── component
├── reducer
└── test
Групування файлів за функціональністю в проекті організовує всі пов'язані компоненти, редуктори (reducers) та тести разом, що полегшує управління та модифікацію кожної функціональності. Це має такі переваги:
- Масштабованість: Великі додатки легше масштабувати та підтримувати, оскільки кожна функціональність діє як маленький додаток.
Ви уникаєте необхідності прокручувати список файлів вгору та вниз, щоб знайти всі потрібні файли. - Співпраця: Розробники можуть зосереджуватись на конкретних функціональностях, не заважаючи роботі інших.
- Орієнтація нових розробників: Нові розробники швидше розуміють структуру проєкту, оскільки всі файли для однієї функціональності знаходяться в одному місці.
- Рефакторинг: Оновлення функціональності спрощене, оскільки всі її елементи згруповані разом.
- Модульність: Функціональності можна легше використовувати повторно, ділитися ними або перетворювати на окремі пакети.
- Мікрофронтенди: Якщо ваш додаток виросте до 30 або більше розробників, легко здійснити рефакторинг до мікрофронтендів.
Ось більш конкретний приклад.
src/
├── app/
│ ├── ...
│ ├── (group)/
│ │ ├── about/
│ │ │ └── page.tsx
│ │ ├── settings/
│ │ │ └── page.tsx
│ │ └── layout.tsx
│ ├── dashboard/
│ │ ├── page.tsx
│ │ └── layout.tsx
│ ├── layout.tsx
│ └── page.tsx
├── components/
│ ├── ...
│ └── header/
│ ├── header-component.tsx
│ ├── header-component.test.ts
│ └── header.module.css
├── features/
│ ├── ...
│ ├── todos/
│ │ ├── ...
│ │ ├── todos-component.tsx
│ │ ├── todos-component.test.ts
│ │ ├── todos-container.ts
│ │ ├── todos-reducer.ts
│ │ ├── todos-reducer.test.ts
│ │ └── todos-styles.ts
│ └── user/
│ ├── ...
│ ├── user-reducer.ts
│ └── user-reducer.test.ts
├── hocs/
│ ├── ...
│ └── with-layout.tsx
├── hooks/
├── redux/
│ ├── root-reducer.ts
│ ├── root-saga.ts
│ └── store.ts
├── ...
└── middleware.ts
Цей приклад демонструє, як можна організувати директорію src/
. Зазвичай усе групується за функціональністю. Тестові файли знаходяться поруч із відповідними файлами реалізації. Але все, що спільне для кількох функціональностей, згруповано в загальних папках, таких як components/
або HOCs/
або hooks/
.
Спільне налаштування для управління станом — в цьому прикладі для Redux — знаходиться в папці redux/
.
Деякі розробники також люблять групувати за функціональністю в папці app/
. Ось як це може виглядати.
src/
├── app/
│ ├── ...
│ ├── (group)/
│ ├── dashboard/
│ │ ├── components/
│ │ │ ├── dashboard-header.tsx
│ │ │ ├── dashboard-header.test.ts
│ │ │ ├── dashboard-widgets.tsx
│ │ │ └── dashboard-widgets.test.ts
│ │ ├── services/
│ │ │ ├── fetch-data.ts
│ │ │ ├── fetch-data.test.ts
│ │ │ ├── auth-service.ts
│ │ │ └── auth-service.test.ts
│ │ ├── page.tsx
│ │ └── layout.ts
│ ├── layout.tsx
│ └── page.tsx
├── ...
└── middleware.ts
В цьому посібнику ви будете групувати файли за функціональністю першим способом.
Vitest
Якщо ви хочете уникнути помилок і запобігти регресії, вам потрібно писати тести.
Vitest — чудовий вибір, оскільки він має таке ж API, як і найбільш популярний фреймворк, а саме Jest, але працює швидше.
Встановіть Vitest.
npm install -D vitest
І налаштуйте команду для тестування у вашому package.json
.
"test": "vitest --reporter=verbose",
Тепер створіть файл example.test.ts
і напишіть короткий тест, щоб перевірити, чи працює Vitest.
import { describe, expect, test } from 'vitest';
describe('example', () => {
test('given a passing test: passes', () => {
expect(1).toStrictEqual(1);
});
});
Тест має пройти успішно.
$ npm test
\> [email protected] test
\> vitest --reporter=verbose
DEV v2.0.5 /Users/jan/dev/nextjs-for-production
✓ src/example.test.ts (1)
✓ example (1)
✓ given a passing test: passes
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 14:06:24
Duration 203ms (transform 20ms, setup 0ms, collect 17ms, tests 1ms, environment 0ms, prepare 56ms)
PASS Waiting for file changes...
натисніть h для показу довідки, натисніть q для виходу
Якщо ви хочете дізнатися, як писати кращі тести, ознайомтесь з цією статтею, де викладено 12 вічних принципів тестування, які повинен знати кожен старший розробник.
React Testing Library
Вам також потрібно писати тести для ваших компонентів React.
Для цього ви можете використовувати React Testing Library, що зазвичай скорочується до RTL.
npm install --save-dev @testing-library/react @testing-library/dom @testing-library/jest-dom @testing-library/user-event @types/react @types/react-dom happy-dom @vitejs/plugin-react vite-tsconfig-paths
Потім створіть файл vitest.config.ts
.
import react from '@vitejs/plugin-react';
import tsconfigPaths from 'vite-tsconfig-paths';
import { defineConfig } from 'vitest/config';
export default defineConfig({
plugins: [react(), tsconfigPaths()],
server: {
port: 3000,
},
test: {
environment: 'happy-dom',
globals: true,
setupFiles: ['./src/tests/setup-test-environment.ts'],
include: ['./src/\*\*/\*.{spec,test}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
watch: {
ignored: [
String.raw`.*\\/node_modules\\/.*`,
String.raw`.*\\/build\\/.*`,
String.raw`.*\\/postgres-data\\/.*`,
],
},
coverage: {
reporter: ['text', 'json', 'html'],
},
},
});
Це налаштовує сервер розробки, вказує параметри тестування, наприклад, що середовище — це happy-dom
і шляхи до файлів, а також визначає формати звітів про покриття.
І створіть файл під назвою src/tests/setup-test-environment.ts
для налаштування вашого тестового середовища.
import '@testing-library/jest-dom/vitest';
// Дивіться \.
// @ts-ignore
globalThis.IS_REACT_ACT_ENVIRONMENT = true;
Перший рядок імпортує додаткові асерції, які розширюють вбудовані асерції Vitest, що дозволяє вам легше тестувати елементи DOM.
Наприклад, ви можете перевірити, чи елемент видимий, чи має певний текстовий вміст або включає конкретні атрибути.
Глобальний флаг середовища забезпечує, щоб тести, що включають оновлення стану, працювали належним чином без проблем з таймінгом.
Далі потрібно налаштувати кастомний метод рендеру в файлі src/tests/react-test-utils.tsx
.
/* eslint-disable import/export */
import type { RenderOptions } from '@testing-library/react';
import { render } from '@testing-library/react';
import type { ReactElement } from 'react';
const customRender = (
ui: ReactElement,
options?: Omit<RenderOptions, 'wrapper'>,
) =>
render(ui, {
wrapper: ({ children }) => <>{children}</>,
...options,
});
// re-export everything
export * from '@testing-library/react';
// override render method
export { customRender as render };
export { default as userEvent } from '@testing-library/user-event';
Ви можете використовувати це, щоб обгорнути ваш код у провайдери, наприклад, при використанні Redux, або додавати макети та стилі.
Переконайтеся, що ваша конфігурація працює, написавши тест для компонента React.
import { describe, expect, test } from 'vitest';
import { render, screen } from '@/tests/react-test-utils';
function MyReactComponent() {
return 'My React Component';
}
describe('MyReactComponent', () => {
test('given no props: renders a text', () => {
render(<MyReactComponent />);
expect(screen.getByText('My React Component')).toBeInTheDocument();
});
});
Ви імпортуєте render
та screen
з вашого файлу тестових утиліт замість того, щоб брати їх безпосередньо з RTL.
toBeInTheDocument()
— це одна з тих спеціальних асерцій, які ви налаштували раніше у файлі налаштування середовища тестування.
Цей тест також має пройти.
✓ src/features/example.test.tsx (1)
✓ MyReactComponent (1)
✓ given no props: renders a text
Test Files 1 passed (1)
Tests 1 passed (1)
Start at 09:33:32
Duration 402ms (transform 24ms, setup 55ms, collect 87ms, tests 9ms, environment 80ms, prepare 45ms)
PASS Waiting for file changes...
натисніть h, щоб показати допомогу, натисніть q, щоб вийти
Стилізація
Тепер давайте поговоримо про стилізацію. Два найбільш важливі аспекти, які треба правильно налаштувати — це доступність (accessibility) та підтримуваність (maintainability). Однією з найбільш популярних бібліотек, яка добре вирішує ці два аспекти, є Shadcn. Вона використовує Tailwind для спрощеного управління стилями та Radix для покращеної доступності.
Ініціалізуйте Shadcn у вашому проєкті.
$ npx shadcn@latest init
✔ **Який стиль** ви хочете використовувати? › New York
✔ **Який колір ви хочете використати як** основний? › Slate
✔ **Бажаєте використовувати** CSS змінні **для кольорів?** … no / yes
✔ Запис компонентів в components.json...
✔ Ініціалізація проєкту...
✔ Встановлення залежностей...
Успіх! Ініціалізація проєкту завершена.
Тепер ви можете додавати компоненти.
$ npx shadcn@latest init
✔ Який стиль ви хочете використовувати? › New York
✔ Який колір ви хочете використати як основний? › Slate
✔ Бажаєте використовувати CSS змінні для кольорів? … no / yes
✔ Запис компонентів в components.json…
✔ Ініціалізація проєкту…
✔ Встановлення залежностей…
Успіх! Ініціалізація проєкту завершена.
Тепер ви можете додавати компоненти.
npx shadcn@latest add card
Інтернаціоналізація
Якщо ваш додаток масштабуватиметься, вам захочеться перекласти його на кілька мов, щоб охопити більше користувачів.
Додавання інтернаціоналізації — або i18n — на пізніх етапах розробки може бути складним, оскільки вам доведеться знайти всі захардкожені рядки й замінити їх на виклики функцій перекладу.
Встановіть пакет negotiator
і матчер для локалей.
npm install negotiator @formatjs/intl-localematcher
Пакет @formatjs/intl-localematcher
вибирає найкращу мову для контенту вашого додатку на основі мовних переваг користувача.
Пакет negotiator
допомагає вашому додатку визначити, який тип контенту (наприклад, мова чи формат) найкраще підходить для браузера користувача, на основі інформації, яку браузер надсилає.
Також потрібно встановити типи TypeScript для пакету negotiator
.
npm install --save-dev @types/negotiator
Далі створіть конфігурацію i18n у новому файлі @src/features/internationalization/i18n-config.ts
.
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { type NextRequest, NextResponse } from 'next/server';
import { i18n } from './i18n-config';
function getLocale(request: NextRequest) {
const headers = {
'accept-language': request.headers.get('accept-language') ?? '',
};
const languages = new Negotiator({ headers }).languages();
return match(languages, i18n.locales, i18n.defaultLocale);
}
export function localizationMiddleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const pathnameHasLocale = i18n.locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
if (pathnameHasLocale) {
return;
}
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
Використовуйте цей конфігураційний файл i18n
у вашому middleware для локалізації в файлі src/features/internationalization/localization-middleware.ts
.
import { match } from '@formatjs/intl-localematcher';
import Negotiator from 'negotiator';
import { type NextRequest, NextResponse } from 'next/server';
import { i18n } from './i18n-config';
function getLocale(request: NextRequest) {
const headers = {
'accept-language': request.headers.get('accept-language') ?? '',
};
const languages = new Negotiator({ headers }).languages();
return match(languages, i18n.locales, i18n.defaultLocale);
}
export function localizationMiddleware(request: NextRequest) {
const { pathname } = request.nextUrl;
const pathnameHasLocale = i18n.locales.some(
locale => pathname.startsWith(`/${locale}/`) || pathname === `/${locale}`,
);
if (pathnameHasLocale) {
return;
}
const locale = getLocale(request);
request.nextUrl.pathname = `/${locale}${pathname}`;
return NextResponse.redirect(request.nextUrl);
}
Мета цього middleware — автоматично визначати та перенаправляти користувачів на відповідну мовну версію сайту, ґрунтуючись на мовних налаштуваннях їх браузера.
Далі створіть файл middleware src/middleware.ts
у корені вашого проєкту і використовуйте ваш middleware для локалізації там.
import { NextRequest } from 'next/server';
import { localizationMiddleware } from './features/internationalization/localization-middleware';
// Matcher, що ігнорує `/_next/`, `/api/` та svg файли.
експортуйте налаштування, що визначають правило для matcher: ['/((?!api|\_next|.\*.svg$).\*)'] };
export function middleware(request: NextRequest) {
return localizationMiddleware(request);
}
Час додавати ваші переклади. Створіть json
словник для англійських перекладів у файлі src/features/internationalization/dictionaries/en-us.json
.
{
"counter": {
"decrement": "Зменшити",
"increment": "Збільшити"
},
"landing": {
"welcome": "Ласкаво просимо"
}
}
Далі створіть файл src/features/internationalization/get-dictionaries.ts
для вашої функції getDictionary
.
import "server-only";
import type { Locale } from "./i18n-config";
// Тут перераховані всі словники для кращого linting та підтримки TypeScript.
// Ми також отримуємо стандартний імпорт для чистіших типів.
const dictionaries = {
"en-US": () => import("./dictionaries/en-US.json").then((module) => module.default),
};
export const getDictionary = async (locale: Locale) =>
dictionaries[locale]?.() ?? dictionaries["en-US"]();
Це функція, яка приймає параметр locale
(локалізація) і повертає відповідний словник.
Якщо ви хочете додати хук, який дозволяє користувачам обирати свою мову, то він може виглядати так:
import { usePathname } from 'next/navigation';
import { Locale } from './i18n-config';
export function useSwitchLocaleHref() {
const pathName = usePathname();
const getSwitchLocaleHref = (locale: Locale) => {
if (!pathName) return '/';
const segments = pathName.split('/');
segments[1] = locale;
return segments.join('/');
};
return getSwitchLocaleHref;
}
Потім ви можете використовувати цей хук у компоненті і використовувати його повернуте значення в тегу <Link>
ось так:
{locale}
Змініть структуру вашого URL, щоб підтримувати i18n.
Створіть нову папку [lang]
у вашій папці app/
, що створює динамічний сегмент для мови.
Перемістіть файли page.tsx
та layout.tsx
у цю папку і змініть макет, щоб встановити правильну мову на тегу <html>
.
import '../globals.css';
import type { Metadata } from 'next';
import { Inter } from 'next/font/google';
import { Locale } from '@/features/internationalization/i18n-config';
const inter = Inter({ subsets: ['latin'] });
export const metadata: Metadata = {
title: "Jan Hesters' Next.js для продакшн-оточення",
description: 'Пропонується ReactSquad.io',
};
export default function RootLayout({
children,
params,
}: Readonly<{
children: React.ReactNode;
params: { lang: Locale };
}\>) {
return (
<html lang={params.lang}>
{children}
</html>
);
}
В подальшому ви можете використовувати функцію getDictionary
в будь-якому серверному компоненті.
import { getDictionary } from '@/features/internationalization/get-dictionaries';
import { Locale } from '@/features/internationalization/i18n-config';
import { CounterComponent } from './counter-component';
export default async function IndexPage({
params: { lang },
}: {
params: { lang: Locale };
}) {
const dictionary = await getDictionary(lang);
return (
<div>
<p>Поточна мова: {lang}</p>
<p>Цей текст рендериться на сервері: {dictionary.landing.welcome}</p>
</div>
);
}
Для клієнтських компонентів вам слід передавати відповідний словник.
'use client';
import { useState } from 'react';
import type { getDictionary } from '@/features/internationalization/get-dictionaries';
export function CounterComponent({
dictionary,
}: {
dictionary: Awaited<ReturnType<typeof getDictionary>>['counter'];
}) {
const [count, setCount] = useState(0);
return (
<div>
<p>Цей компонент рендериться на клієнті:</p>
<button onClick={() => setCount(n => n - 1)}>{dictionary.decrement}</button>
<p>{count}</p>
<button onClick={() => setCount(n => n + 1)}>{dictionary.increment}</button>
</div>
);
}
База даних
Цей посібник використовує Postgres як базу даних, оскільки вона добре перевірена на практиці, але для абстрагування роботи з базою даних ви будете використовувати Prisma ORM.
Це дає вам гнучкість у використанні різних баз даних і спрощує API, яке ви використовуєте для взаємодії з нею.
npm install prisma --save-dev
Вам також потрібно встановити Prisma клієнт.
npm install @prisma/client
Ініціалізуйте Prisma.
npx prisma init
Це автоматично створює файл prisma/schema.prisma
.
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
// Використовує пул з'єднань
url = env("DATABASE_URL")
}
model UserProfile {
id String @id @default(cuid())
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
email String @unique
name String @default("")
acceptedTermsAndConditions Boolean @default(false)
}
Додайте модель UserProfile
, яка має електронну пошту, ім'я та булеве значення, яке вказує, чи погодилися користувачі з вашими умовами використання.
Якщо ви виконали команду npx prisma init
, перейменуйте ваш файл .env
на .env.local
, інакше створіть його і переконайтесь, що він містить облікові дані для вашої бази даних Prisma.
DATABASE_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
Тепер створіть файл src/lib/prisma.ts
, в якому буде зберігатися з'єднання вашого Prisma клієнта.
import { PrismaClient } from '@prisma/client';
declare global {
var __database__: PrismaClient;
}
let prisma: PrismaClient;
if (process.env.NODE_ENV === 'production') {
prisma = new PrismaClient();
} else {
if (!global.__database__) {
global.__database__ = new PrismaClient();
}
prisma = global.__database__;
}
export default prisma;
Переназначення змінної гарантує, що в не-продакшн середовищах (наприклад, під час розробки або тестування) буде створюватися і повторно використовуватись лише один екземпляр Prisma клієнта через всю програму.
Цей підхід запобігає витратам на створення нових з'єднань з базою даних щоразу, коли імпортується модуль, який потребує доступу до бази даних.
Змініть ваш файл package.json
, щоб додати наступні допоміжні команди для Prisma.
"prisma:deploy": "npx prisma migrate deploy && npx prisma generate",
"prisma:migrate": "npx prisma migrate dev --name",
"prisma:push": "npx prisma db push && npx prisma generate",
"prisma:reset-dev": "run-s prisma:wipe prisma:seed dev",
"prisma:seed": "tsx ./prisma/seed.ts",
"prisma:setup": "prisma generate && prisma migrate deploy && prisma db push",
"prisma:studio": "npx prisma studio",
"prisma:wipe": "npx prisma migrate reset --force && npx prisma db push",
Деякі з цих команд використовують пакети run-s
, tsx
та dotenv
, які потрібно встановити.
npm install --save-dev npm-run-all tsx dotenv
Ось пояснення кожної з команд Prisma:
- “prisma:deploy”: Розгортає міграції бази даних та генерує Prisma Client.
- “prisma:deploy”: Виконує міграції на продуктивних базах даних і оновлює API клієнта.
- “prisma:migrate”: Створює нову міграцію на основі змін у схемі Prisma, застосовуючи її в середовищі розробки. Потрібно вказати ім'я міграції після команди.
- “prisma:push”: Безпосередньо виштовхує зміни схеми до бази даних і оновлює Prisma Client.
- “prisma:deploy”: Виконує міграції на продуктивних базах даних і оновлює API клієнта.
- “prisma:migrate”: Створює нову міграцію на основі змін у схемі Prisma, застосовуючи її в середовищі розробки. Потрібно вказати ім'я міграції після команди.
- “prisma:push”: Безпосередньо виштовхує зміни схеми до бази даних і оновлює Prisma Client.
- “prisma:reset-dev”: Скидає базу даних для розробки, очищаючи її, повторно ініціалізуючи дані та застосовуючи міграції в режимі розробки.
- “prisma:seed”: Виконує TypeScript-скрипт для наповнення бази даних початковими даними.
- “prisma:setup”: Генерує Prisma Client, застосовує міграції до бази даних і пушить схему до бази даних.
- “prisma:studio”: Відкриває Prisma Studio, графічний інтерфейс для перегляду та редагування записів бази даних.
- “prisma:wipe”: Скидає базу даних, примусово видаляючи всі дані та міграції, після чого застосовує зміни схеми.
npm run prisma:setup
Якщо ваш Prisma не розпізнає файл .env.local
, вручну встановіть вашу змінну середовища в терміналі. На Mac це можна зробити за допомогою команди export
.
export DATABASE\_URL="postgresql://johndoe:randompassword@localhost:5432/mydb?schema=public"
Створіть файл prisma/seed.ts
.
Ви можете використовувати його для наповнення вашої бази даних даними для розробки.
import { exit } from 'node:process';
import { PrismaClient } from '@prisma/client';
import dotenv from 'dotenv';
dotenv.config({ path: '.env.local' });
const prisma = new PrismaClient();
const prettyPrint = (object: any) =>
console.log(JSON.stringify(object, undefined, 2));
async function seed() {
const user = await prisma.userProfile.create({
data: {
email: '[email protected]',
name: 'Jan Hesters',
acceptedTermsAndConditions: true,
},
});
console.log('========= 🌱 результат ініціалізації: =========');
prettyPrint({ user });
}
seed()
.then(async () => {
await prisma.$disconnect();
})
// eslint-disable-next-line unicorn/prefer-top-level-await
.catch(async error => {
console.error(error);
await prisma.$disconnect();
exit(1);
});
Якщо ви запустите його, то буде створено користувача.
$ npm run prisma:seed
> tsx ./prisma/seed.ts
========= 🌱 результат ініціалізації: =========
{
"user": {
"id": "clzekb5sp0000ock9gsp72p33",
"createdAt": "2024-08-03T20:04:27.289Z",
"updatedAt": "2024-08-03T20:04:27.289Z",
"email": "[email protected]",
"name": "Jan Hesters",
"acceptedTermsAndConditions": true
}
}
У ваших серверних компонентах ви можете використовувати prisma для отримання будь-яких даних.
import prisma from '@/lib/prisma';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
export default async function Dashboard() {
const user = await prisma.userProfile.findUnique({
where: { email: '[email protected]' },
});
return (
\
\
\Профіль корист
{user ? (
\
\Ім'я: {user.name}\
\Електронна пошта: {user.email}\
\
) : (
\Користувача не знайдено.\
)}
\
\
);
}
Використання фасадів
Добре мати абстракцію для ваших викликів до бази даних за допомогою фасадів (facades).
Фасад (Facade) — це шаблон проєктування, який надає спрощений інтерфейс до складної підсистеми.
Створіть файл, що міститиме всі фасади, пов'язані з моделлю профілю користувача, наприклад, features/user-profile/user-profile-model.ts
.
import { UserProfile } from '@prisma/client';
import prisma from '@/lib/prisma';
export async function retrieveUserProfileFromDatabaseByEmail(
email: UserProfile['email'],
) {
return await prisma.userProfile.findUnique({ where: { email } });
}
Існують дві основні причини для використання фасадів.
- Збільшення стійкості до змін постачальників — Ви можете легко змінити стороннього постачальника. Наприклад, ви можете перейти з Firebase на Supabase або навпаки. Замість того, щоб оновлювати весь код проєкту, щоб відобразити зміни, ви оновлюєте лише фасад.
- Спрощення коду — Фасад може зменшити обсяг коду, який потрібно писати у вашому додатку, оскільки він зменшує API до специфічних потреб вашого додатку.
Одночасно це спрощує розуміння вашого коду, оскільки ви можете надавати фасадам описові назви.
Тепер використовуйте ваш фасад у серверному компоненті.
import { retrieveUserProfileFromDatabaseByEmail } from '@/features/user-profiles/user-profiles-model';
import {
Card,
CardContent,
CardHeader,
CardTitle,
} from '@/components/ui/card';
export default async function Dashboard() {
const user =
await retrieveUserProfileFromDatabaseByEmail('[email protected]');
return (
\
\
\User Profile
\Name: {user.name}\
\Email: {user.email}\
\
) : (
\User not found.\
)}
\
\
);
}
Vercel Postgres
Ви можете використовувати Vercel Postgres для вашого продакшн деплою.
Вони мають просту для розуміння інструкцію, яку ви можете переглянути в документації Vercel. Але ось кроки для вашої зручності.
Щоб налаштувати базу даних у вашому проекті на Vercel, дотримуйтесь цих кроків:
- Перейдіть на вкладку Storage та натисніть кнопку Create Database.
- Коли відкриється модальне вікно Browse Storage, виберіть Postgres та натисніть Continue.
Для створення нової бази даних:
- У діалоговому вікні введіть
sample_postgres_db
(або бажану назву) у поле Store Name. Переконайтесь, що ім’я складається лише з алфавітно-цифрових символів, "_" або "-", та не перевищує 32 символи. - Виберіть регіон. Для зменшення затримки виберіть регіон, близький до вашого функціонального регіону, який за замовчуванням є US East.
3.
Натисніть Create.
Потім вам потрібно додати POSTGRES_URL_NON_POOLING
до datasource
у вашій схемі Prisma.
datasource db {
provider = "postgresql"
// Використовує пул підключень
url = env("DATABASE\_URL")
// Використовує пряме підключення, ⚠️ переконайтесь, що залишаєте це значення `POSTGRES_URL_NON_POOLING`
// інакше у вас будуть "висячі_URL\_NON\_POOLING")
}
Vercel використовує пула підключень, які керують пулом підключень до бази даних, що можуть бути повторно використані різними частинами програми, замість того, щоб створювати нове підключення для кожного запиту до бази даних.
Властивість directUrl
використовується для того, щоб операції, які потребують прямого доступу до бази даних, наприклад міграції, могли обходити пул підключень для надійного виконання.
Ви можете отримати змінні середовища для вашої бази даних Vercel, витягнувши їх із Vercel.
vercel env pull .env
Playwright
Ви також повинні використовувати E2E (End-to-End) тести, оскільки вони надають найбільшу впевненість, що ваш додаток працює як очікується. E2E тести занадто часто пропускають, але вам справді варто зробити звичкою їх писати.
Переваги цих підходів нарощуються.
Ініціалізуйте Playwright.
$ npm init playwright@latest
Початок роботи з написанням end-to-end тестів за допомогою Playwright:
Ініціалізація проєкту в '.'
✔ Куди додавати ваші end-to-end тести? · playwright
✔ Додати GitHub Actions workflow? (y/N) · false
✔ Встановити браузери Playwright (можна зробити вручну через 'npx playwright install')? (Y/n) · true
Якщо ви вперше використовуєте Playwright, ви можете переглянути папку test-examples/
, яку створює скрипт ініціалізації, а потім видалити її, оскільки вам вона не знадобиться.
Модифікуйте ключ webServer
у вашому файлі playwright.config.ts
.
webServer: {
command: process.env.CI ? 'npm run build && npm run start' : 'npm run dev',
port: 3000,
},
Додайте два скрипти для ваших E2E тестів до вашого файлу package.json
.
"test:e2e": "npx playwright test",
"test:e2e:ui": "npx playwright test --ui",
Перший скрипт запускає ваш Playwright тест у безголовому режимі (headless mode), а другий — у режимі UI, який дає можливість налагодження з подорожжю в часі, режим спостереження та інші функції.
import { expect, test } from '@playwright/test';
test.describe('landing page', () => {
test('given any user: shows the test user', async ({ page }) => {
await page.goto('/');
await expect(page.getByText('Jan Hesters')).toBeVisible();
await expect(page.getByText('[email protected]')).toBeVisible();
});
});
Запустіть ваші тести, щоб перевірити, чи працює налаштування Playwright.
$ npm run test:e2e
\> [email protected] test:e2e
\> npx playwright test
Запуск 3 тестів за допомогою 3 робітників
3 пройшли (3.9с)
Було запущено три тести, оскільки за замовчуванням Playwright налаштований на запуск в браузерах Chrome, Safari та Firefox.
GitHub Actions
Доброю практикою є налаштування CI/CD для вашого додатку.
CI/CD означає безперервну доставку та безперервне розгортання.
Додайте секрет для вашого URL бази даних до налаштувань вашого репозиторію на GitHub.
DATABASE\_URL="postgresql://postgres:postgres@localhost:5432/testdb"
Далі створіть файл конфігурації GitHub Actions YAML у .github/workflows/pull-request.yml
для комплексного CI/CD процесу, який включає лінтинг, перевірку типів, тести та інше.
name: Pull Request
on: [pull\_request]
jobs:
lint:
name: ⬣ ESLint
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
- name: 🔬 Lint
run: npm run lint
type-check:
name: ʦ TypeScript
runs-on: ubuntu-latest
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
- name: 🔎 Type check
run: npm run type-check --if-present
commitlint:
name: ⚙️ commitlint
runs-on: ubuntu-latest
if: github.actor != 'dependabot[bot]'
env:
GITHUB\_TOKEN: ${{ secrets.GITHUB\_TOKEN }}
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
with:
fetch-depth: 0
- name: ⚙️ commitlint
uses: wagoid/commitlint-github-action@v4
vitest:
name: ⚡ Vitest
runs-on: ubuntu-latest
services:
postgres:
image:_PASSWORD: postgres
POSTGRES\_DB: testdb
ports:
- 5432:5432
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
- name: 🛠 Setup Database
run: npm run prisma:wipe
env:
DATABASE\_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: ⚡ Run vitest
run: npm run test -- --coverage
env:
DATABASE\_URL: postgresql://postgres:postgres@localhost:5432/testdb
playwright:
name: 🎭 Playwright
runs-on: ubuntu-latest
services:
postgres:
image: postgres:12
env:
POSTGRES_DB: testdb
ports:
- 5432:5432
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@v3
- name: ⎔ Setup node
uses: actions/setup-node@v3
with:
node-version: 20
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
- name: 🌐 Install Playwright Browsers
run: npx playwright install --with-deps
- name: 🛠 Setup Database
run: npm run prisma:wipe
env:
DATABASE\_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: 🎭 Playwright Run
run: npx playwright test
env:
DATABASE\_URL: postgresql://postgres:postgres@localhost:5432/testdb
- name: 📸 Playwright Screenshots
uses: actions/upload-artifact@v4
if: failure()
with:
name: playwright-report
path: playwright-report/
retention-days: 30
Тепер кожного разу, коли ви створюєте pull request до вашого додатку, автоматично запускаються тести для перевірки, чи працює додаток, перевіряються типи TypeScript і лінтиться код, щоб усі розробники використовували однакове форматування коду.
Перекладено з: How to Set Up Next.js 15 for Production in 2024