diff --git a/.gitignore b/.gitignore index 028147e..45bbe30 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ site node_modules .history .cache +.venv-migration diff --git a/.vscode/settings.json b/.vscode/settings.json index e478364..24b1c2d 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "svn.ignoreMissingSvnWarning": true, "editor.tabCompletion": "on", "diffEditor.codeLens": true, - "editor.defaultFormatter": "esbenp.prettier-vscode" + "editor.defaultFormatter": "esbenp.prettier-vscode", + "editor.formatOnSave": true } diff --git a/README.md b/README.md index 4db4e54..4827aa6 100644 --- a/README.md +++ b/README.md @@ -12,6 +12,11 @@ Для сборки справочника нужно [установить MKDocs](https://www.mkdocs.org/#installation), [расширения PyMdown](https://facelessuser.github.io/pymdown-extensions/installation/) и тему [Material for MkDocs](https://squidfunk.github.io/mkdocs-material/): +```sh +python -m venv venv +source venv/bin/activate +``` + ``` pip3 install -r ./requirements.txt ``` diff --git a/docs/archive/react16/context.md b/docs/archive/react16/context.md index cdac57a..b7e8974 100644 --- a/docs/archive/react16/context.md +++ b/docs/archive/react16/context.md @@ -20,7 +20,6 @@ class App extends React.Component { } function Toolbar(props) { - // highlight-range{1-5,8} // Компонент Toolbar должен передать проп "theme" ниже, // фактически не используя его. Учитывая, что у вас в приложении // могут быть десятки компонентов, использующих UI-тему, @@ -43,7 +42,6 @@ class ThemedButton extends React.Component { Контекст позволяет избежать передачи пропсов в промежуточные компоненты: ```js -// highlight-range{1-5} // Контекст позволяет передавать значение глубоко // в дерево компонентов без явной передачи пропсов // на каждом уровне. Создадим контекст для текущей @@ -52,7 +50,6 @@ const ThemeContext = React.createContext('light'); class App extends React.Component { render() { - // highlight-range{1-4,6} // Компонент Provider используется для передачи текущей // UI-темы вниз по дереву. Любой компонент может использовать // этот контекст и не важно, как глубоко он находится. @@ -65,7 +62,6 @@ class App extends React.Component { } } -// highlight-range{1,2} // Компонент, который находится в середине, // больше не должен явно передавать тему вниз. function Toolbar() { @@ -77,7 +73,6 @@ function Toolbar() { } class ThemedButton extends React.Component { - // highlight-range{1-4,7} // Определяем contextType, чтобы получить значение контекста. // React найдёт (выше по дереву) ближайший Provider-компонент, // предоставляющий этот контекст, и использует его значение. @@ -182,7 +177,7 @@ const MyContext = React.createContext(defaultValue); Каждый объект Контекста используется вместе с `Provider` компонентом, который позволяет дочерним компонентам, использующим этот контекст, подписаться на его изменения. -Принимает проп `value`, который будут передан во все компоненты, использующие этот контекст и являющиеся потомками этого Provider компонента. Один Provider может быть связан с несколькими компонентами, потребляющими контекст. Так же Provider компоненты могут быть вложены друг в друга, переопределяя значение контекста глубже в дереве. +Принимает проп `value`, который будет передан во все компоненты, использующие этот контекст и являющиеся потомками этого Provider компонента. Один Provider может быть связан с несколькими компонентами, потребляющими контекст. Так же Provider компоненты могут быть вложены друг в друга, переопределяя значение контекста глубже в дереве. Все потребители, которые являются потомками Provider, будут повторно рендериться, как только проп `value` у Provider изменится. Потребитель перерендерится при изменении контекста, даже если его родитель, не использующий данный контекст, блокирует повторные рендеры с помощью `shouldComponentUpdate`. @@ -270,7 +265,6 @@ export const themes = { }, }; -// highlight-range{1-3} export const ThemeContext = React.createContext( themes.dark // значение по умолчанию ); @@ -282,7 +276,6 @@ export const ThemeContext = React.createContext( import { ThemeContext } from './theme-context'; class ThemedButton extends React.Component { - // highlight-range{3,12} render() { let props = this.props; let theme = this.context; @@ -334,12 +327,10 @@ class App extends React.Component { } render() { - //highlight-range{1-4} // ThemedButton внутри ThemeProvider использует // значение светлой UI-темы из состояния, в то время как // ThemedButton, который находится вне ThemeProvider, // использует тёмную UI-тему из значения по умолчанию - //highlight-range{3-5,7} return ( , document.root); // Убедитесь, что форма значения по умолчанию, // передаваемого в createContext, совпадает с формой объекта, // которую ожидают потребители контекста. -// highlight-range{2-3} export const ThemeContext = React.createContext({ theme: themes.dark, toggleTheme: () => {}, @@ -383,7 +373,6 @@ export const ThemeContext = React.createContext({ import { ThemeContext } from './theme-context'; function ThemeTogglerButton() { - // highlight-range{1-2,5} // ThemeTogglerButton получает из контекста // не только значение UI-темы, но и функцию toggleTheme. return ( @@ -424,7 +413,6 @@ class App extends React.Component { })); }; - // highlight-range{1-2,5} // Состояние хранит функцию для обновления контекста, // которая будет также передана в Provider-компонент. this.state = { @@ -434,7 +422,6 @@ class App extends React.Component { } render() { - // highlight-range{1,3} // Всё состояние передаётся в качестве значения контекста return ( @@ -473,7 +460,6 @@ class App extends React.Component { const { signedInUser, theme } = this.props; // Компонент App, который предоставляет начальные значения контекстов - // highlight-range{2-3,5-6} return ( @@ -495,7 +481,6 @@ function Layout() { // Компонент, который может использовать несколько контекстов function Content() { - // highlight-range{2-10} return ( {(theme) => ( @@ -522,7 +507,6 @@ function Content() { ```js class App extends React.Component { render() { - // highlight-range{2} return ( diff --git a/docs/archive/react16/fragments.md b/docs/archive/react16/fragments.md index 6378c12..8a9a802 100644 --- a/docs/archive/react16/fragments.md +++ b/docs/archive/react16/fragments.md @@ -22,15 +22,15 @@ render() { ```js class Table extends React.Component { - render() { - return ( - - - - -
- ) - } + render() { + return ( + + + + +
+ ); + } } ``` @@ -38,14 +38,14 @@ class Table extends React.Component { ```js class Columns extends React.Component { - render() { - return ( -
- Привет - Мир -
- ) - } + render() { + return ( +
+ Привет + Мир +
+ ); + } } ``` @@ -53,12 +53,12 @@ class Columns extends React.Component { ```js - -
-
- - - + +
+
+ + +
ПриветМир
ПриветМир
``` @@ -68,14 +68,14 @@ class Columns extends React.Component { ```js class Columns extends React.Component { - render() { - return ( - - Привет - Мир - - ) - } + render() { + return ( + + Привет + Мир + + ); + } } ``` @@ -83,10 +83,10 @@ class Columns extends React.Component { ```js - - - - + + + +
ПриветМир
ПриветМир
``` @@ -96,18 +96,18 @@ class Columns extends React.Component { ```js class Columns extends React.Component { - render() { - return ( - <> - Привет - Мир - - ) - } + render() { + return ( + <> + Привет + Мир + + ); + } } ``` -Можно использовать `<>` так же, как используется любой другой элемент. Однако такая запись не поддерживает ключи или атрибуты. +Можно использовать `<></>` так же, как используется любой другой элемент. Однако такая запись не поддерживает ключи или атрибуты. Обратите внимание, что **большинство инструментов ещё не поддерживают сокращённую запись**, поэтому можно явно указывать ``, пока не появится поддержка. @@ -117,17 +117,17 @@ class Columns extends React.Component { ```js function Glossary(props) { - return ( -
- {props.items.map((item) => ( - // Без указания атрибута `key`, React выдаст предупреждение об его отсутствии - -
{item.term}
-
{item.description}
-
- ))} -
- ) + return ( +
+ {props.items.map((item) => ( + // Без указания атрибута `key`, React выдаст предупреждение об его отсутствии + +
{item.term}
+
{item.description}
+
+ ))} +
+ ); } ``` diff --git a/docs/guides/index.md b/docs/guides/index.md index 6008c36..e256629 100644 --- a/docs/guides/index.md +++ b/docs/guides/index.md @@ -4,6 +4,8 @@ description: Раздел с гайдами и статьями про React # Гайды и статьи +:date: 09.09.2025
**[Как использовать React Compiler – Полное руководство](./react-compiler/index.md)**
_В этом руководстве вы узнаете, как React Compiler может помочь вам писать более оптимизированные React-приложения._ + :date: 15.02.2024
**[История о рефакторинге](./tale-of-a-refactor/index.md)**
_Укрощение сложности кодовой базы с помощью абстракции, издержки и преимущества_ :date: 15.08.2023
**[Опыт модернизации пакетов в ESM](./esm-modernization-lessons/index.md)**
_Подробности о болезненном опыте и извлеченных уроках, которые я получил, перенося пакеты Redux на ESM_ diff --git a/docs/guides/react-compiler/.pages b/docs/guides/react-compiler/.pages new file mode 100644 index 0000000..e25ace5 --- /dev/null +++ b/docs/guides/react-compiler/.pages @@ -0,0 +1,3 @@ +title: React Compiler +nav: + - index.md diff --git a/docs/guides/react-compiler/index.md b/docs/guides/react-compiler/index.md new file mode 100644 index 0000000..76ed800 --- /dev/null +++ b/docs/guides/react-compiler/index.md @@ -0,0 +1,474 @@ +--- +description: Полное руководство по использованию React Compiler для оптимизации React-приложений +--- + +# Как использовать React Compiler – Полное руководство + + +В этом руководстве вы узнаете, как React Compiler может помочь вам писать более оптимизированные React-приложения. + + +React — это библиотека пользовательского интерфейса, которая отлично справляется со своей работой уже более десяти лет. Архитектура компонентов, однонаправленный поток данных и декларативная природа выделяются в помощи разработчикам создавать готовые к продакшену, масштабируемые программные приложения. + +В течение релизов (даже вплоть до последнего стабильного релиза v18.x) React предоставлял различные техники и методологии для улучшения производительности приложений. + +Например, вся парадигма мемоизации поддерживалась с использованием компонента высшего порядка `React.memo` или с помощью хуков вроде `useMemo` и `useCallback`. + +В программировании _мемоизация_ — это техника оптимизации, которая заставляет ваши программы выполняться быстрее путем кэширования результата дорогих вычислений. + +Хотя техники _мемоизации_ React отлично подходят для применения оптимизаций, как однажды сказал дядя Бен (помните дядю Человека-Паука?), "С большой силой приходит большая ответственность". Поэтому мы, как разработчики, должны быть немного более ответственными в применении их. Оптимизация — это здорово, но чрезмерная оптимизация может стать убийцей производительности вашего приложения. + +С React 19 сообщество разработчиков получило список улучшений и функций, которыми можно похвастаться: + +- Экспериментальный компилятор с открытым исходным кодом. Мы будем сосредотачиваться в основном на нем в этой статье. +- React Server Components. +- Server Actions. +- Более простой и естественный способ обработки метаданных документа. +- Улучшенные хуки и API. +- `ref` можно передавать как пропсы. +- Улучшения в загрузке ассетов для стилей, изображений и шрифтов. +- Более плавная интеграция с Web Components. + +Если это вас взволновало, я рекомендую посмотреть это [видео](https://www.youtube.com/watch?v=hiiGUjEkzbM), которое объясняет, как каждая функция повлияет на вас как на React-разработчика. Надеюсь, вам понравится 😊. + +Введение компилятора с React 19 призвано стать настоящим переворотом. Отныне мы можем позволить компилятору справляться с головной болью оптимизации вместо того, чтобы держать это на себе. + +Означает ли это, что нам больше не нужно использовать `React.memo`, `useMemo`, `useCallback` и так далее? Нет — мы в основном не должны. Компилятор может позаботиться об этих вещах автоматически, если вы понимаете и следуете Правилам React для компонентов и хуков. + +Как он это сделает? Ну, мы к этому подойдем. Но сначала давайте разберемся, что такое компилятор и оправдано ли называть этот новый оптимизатор для React-кода React Compiler. + +Если вам нравится учиться по видеоруководствам, эта статья также доступна как видеоруководство здесь: + +![type:video](https://www.youtube.com/watch?v=bdWUVp0TbTU) + +## Что такое компилятор в традиционном понимании? + +Проще говоря, компилятор — это программное обеспечение/инструмент, который переводит код высокоуровневого языка программирования (исходный код) в машинный код. Существует несколько шагов, которые нужно выполнить для компиляции исходного кода и генерации машинного кода: + +- _Лексический анализатор_ токенизирует исходный код и генерирует токены. +- _Синтаксический анализатор_ создает абстрактное синтаксическое дерево (AST) для логической структуризации токенов исходного кода. +- _Семантический анализатор_ проверяет семантическую (или синтаксическую) корректность кода. +- После всех трех типов анализа соответствующими анализаторами генерируется некоторый _промежуточный код_. Он также известен как IR-код. +- Затем выполняется _оптимизация_ IR-кода. +- Наконец, _машинный код_ генерируется компилятором из оптимизированного IR-кода. + +![Шаги для компиляции исходного кода и генерации машинного кода](./react-compiler-1.webp) + +Теперь, когда вы понимаете основы того, как работает компилятор, давайте узнаем о **React Compiler** и разберемся, как он работает. + +## Архитектура React Compiler + +React Compiler — это инструмент времени сборки, который вам нужно явно настроить с вашим React 19 проектом, используя параметры конфигурации, предоставляемые экосистемой инструментов React. + +Например, если вы используете _Vite_ для создания вашего React-приложения, конфигурация компилятора будет происходить в файле `vite.config.js`. + +React Compiler имеет три основных компонента: + +1. _Babel Plugin_: помогает трансформировать код во время процесса компиляции. +2. _ESLint Plugin_: помогает ловить и сообщать о любых нарушениях Правил React. +3. _Compiler Core_: основная логика компилятора, которая выполняет анализ кода и оптимизации. И Babel, и ESLint плагины используют основную логику компилятора. + +Процесс компиляции происходит следующим образом: + +- _Babel Plugin_ идентифицирует, какие функции (компоненты или хуки) компилировать. Мы увидим некоторые конфигурации позже, чтобы узнать, как включать и отключать процесс компиляции. Плагин вызывает основную логику компилятора для каждой из функций и в итоге создает Абстрактное Синтаксическое Дерево. +- Затем ядро компилятора конвертирует Babel AST в IR-код, анализирует его и запускает различные валидации, чтобы убедиться, что ни одно из правил не нарушено. +- Далее оно пытается уменьшить количество кода для оптимизации, выполняя различные проходы для устранения мертвого кода. Код дополнительно оптимизируется с использованием мемоизации. +- Наконец, на этапе генерации кода трансформированное AST конвертируется обратно в оптимизированный JavaScript код. + +## React Compiler в действии + +Теперь, когда вы знаете, как работает React Compiler, давайте погрузимся в его настройку с React 19 проектом, чтобы вы могли начать изучать различные оптимизации. + +### Понимание проблемы: Без React Compiler + +Давайте создадим простую страницу продукта с React. Страница продукта показывает заголовок с количеством продуктов на странице, список продуктов и рекомендуемые продукты. + +![простая страница продукта](./react-compiler-2.webp) + +Иерархия компонентов и передача данных между компонентами выглядит следующим образом: + +![иерархия](./react-compiler-3.webp) + +Как вы можете видеть на изображении выше, + +- Компонент `ProductPage` имеет три дочерних компонента: `Heading`, `ProductList` и `FeaturedProducts`. +- Компонент `ProductPage` получает два пропса: `products` и `heading`. +- Компонент `ProductPage` вычисляет общее количество продуктов и передает значение вместе со значением текста заголовка в компонент `Heading`. +- Компонент `ProductPage` передает пропс `products` дочернему компоненту `ProductList`. +- Аналогично, он вычисляет рекомендуемые продукты и передает пропс `featuredProducts` компоненту `FeaturedProducts`. + +Вот как может выглядеть исходный код компонента `ProductPage`: + +```jsx +import React from 'react'; + +import Heading from './Heading'; +import FeaturedProducts from './FeaturedProducts'; +import ProductList from './ProductList'; + +const ProductPage = ({ products, heading }) => { + const featuredProducts = products.filter( + (product) => product.featured + ); + const totalProducts = products.length; + + return ( +
+ + + + + +
+ ); +}; + +export default ProductPage; +``` + +Также предположим, что мы используем компонент `ProductPage` в файле `App.js` следующим образом: + +```jsx +import ProductPage from './components/compiler/ProductPage'; + +function App() { + // Список пищевых продуктов + const foodProducts = [ + { + id: '001', + name: 'Hamburger', + image: '🍔', + featured: true, + }, + { + id: '002', + name: 'French Fries', + image: '🍟', + featured: false, + }, + { + id: '003', + name: 'Taco', + image: '🌮', + featured: false, + }, + { + id: '004', + name: 'Hot Dog', + image: '🌭', + featured: true, + }, + ]; + + return ( + + ); +} + +export default App; +``` + +Все хорошо — так в чем же проблема? Проблема в том, что React активно перерисовывает дочерний компонент, когда перерисовывается родительский компонент. Ненужная перерисовка требует оптимизаций. Давайте сначала полностью разберемся с проблемой. + +Мы добавим текущую временную метку в каждый из дочерних компонентов. Теперь отрендеренный пользовательский интерфейс будет выглядеть следующим образом: + +![отрендеренный пользовательский интерфейс](./react-compiler-4.webp) + +Большое число, которое вы видите рядом с заголовками — это временная метка (используя простую функцию `Date.now()` из JavaScript Date API), которую мы добавили в код компонента. Теперь что произойдет, если мы изменим значение пропса `heading` компонента `ProductPage`? + +До: + +```jsx + +``` + +И после (обратите внимание, что мы сделали его множественным числом для продуктов, добавив s в конце значения заголовка): + +```jsx + +``` + +Теперь вы заметите немедленное изменение в пользовательском интерфейсе. Все три временные метки обновились. Это потому, что все три компонента были перерисованы, когда родительский компонент был перерисован из-за изменения пропсов. + +![Теперь вы заметите немедленное изменение в пользовательском интерфейсе](./react-compiler-5.webp) + +Если вы заметили, пропс `heading` был передан только компоненту `Heading`, и даже тогда другие два дочерних компонента перерисовались. Здесь нам нужны оптимизации. + +### Решение проблемы: Без React Compiler + +Как обсуждалось ранее, React предоставляет нам различные хуки и API для _мемоизации_. Мы можем использовать `React.memo()` или `useMemo()` для защиты компонентов, которые перерисовываются ненужно. + +Например, мы можем использовать `React.memo()` для мемоизации компонента ProductList, чтобы гарантировать, что компонент `ProductList` не будет перерисовываться, если не изменится пропс `products`. + +Мы можем использовать хук `useMemo()` для мемоизации вычисления рекомендуемых продуктов. Обе реализации указаны на изображении ниже. + +![Применение мемоизации](./react-compiler-6.webp) + +Но опять же, вспоминая мудрые слова великого дяди Бена, за последние несколько лет мы начали чрезмерно использовать эти техники оптимизации. Эти чрезмерные оптимизации могут негативно повлиять на производительность ваших приложений. Поэтому наличие компилятора — это благо для React-разработчиков, поскольку оно позволяет делегировать многие такие оптимизации компилятору. + +Давайте теперь исправим проблему с помощью React Compiler. + +### Решение проблемы: С использованием React Compiler + +Опять же, React Compiler — это инструмент времени сборки с явным включением. Он не поставляется в комплекте с React 19 RC. Вам нужно установить необходимые зависимости и настроить компилятор с вашим React 19 проектом. + +Перед настройкой компилятора вы можете проверить совместимость вашей кодовой базы, выполнив эту команду в директории вашего проекта: + +```sh +npx react-compiler-healthcheck@experimental +``` + +Она проверит и сообщит: + +- Сколько компонентов может быть оптимизировано компилятором +- Соблюдаются ли Правила React. +- Есть ли несовместимые библиотеки. + +![d7866215-5cda-4a64-b0d6-ecedb100a428](./react-compiler-7.webp) + +Если вы обнаружите, что все совместимо, пришло время установить ESLint плагин, работающий на React Compiler. Этот плагин поможет вам ловить любые нарушения правил React в вашем коде. Нарушающий код будет пропущен React Compiler и на нем не будут выполняться оптимизации. + +```sh +npm install eslint-plugin-react-compiler@experimental +``` + +Затем откройте файл конфигурации ESLint (например, .eslintrc.cjs для Vite) и добавьте эти конфигурации: + +```js +module.exports = { + plugins: ['eslint-plugin-react-compiler'], + rules: { + 'react-compiler/react-compiler': 'error', + }, +}; +``` + +Далее вы будете использовать Babel плагин для React Compiler, чтобы включить компилятор для всего вашего проекта. Если вы начинаете новый проект с React 19, я рекомендую включить React Compiler для всего проекта. Давайте установим Babel плагин для React Compiler: + +```sh +npm install babel-plugin-react-compiler@experimental +``` + +После установки вам нужно завершить конфигурацию, добавив опции в файл конфигурации Babel. Поскольку мы используем Vite, откройте файл `vite.config.js` и замените его содержимое следующим фрагментом кода: + +```jsx +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +const ReactCompilerConfig = { + /* ... */ +}; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [ + react({ + babel: { + plugins: [ + [ + 'babel-plugin-react-compiler', + ReactCompilerConfig, + ], + ], + }, + }), + ], +}); +``` + +Здесь вы добавили `babel-plugin-react-compiler` в конфигурацию. `ReactCompilerConfig` требуется для предоставления любой расширенной конфигурации, например, если вы хотите предоставить какой-либо пользовательский модуль времени выполнения или другие конфигурации. В этом случае это пустой объект без расширенных конфигураций. + +Вот и все. Вы завершили настройку React Compiler с вашей кодовой базой для использования его возможностей. Отныне React Compiler будет просматривать каждый компонент и хук в вашем проекте, пытаясь применить к ним оптимизации. + +Если вы хотите настроить React Compiler с Next.js, Remix, Webpack и так далее, вы можете [следовать этому руководству](https://react.dev/learn/react-compiler#installation). + +### Оптимизированное React приложение с React Compiler + +Теперь у вас должно быть оптимизированное React приложение с включением React Compiler. Итак, давайте проведем те же тесты, что и раньше. Опять измените значение пропса `heading` компонента `ProductPage`. + +На этот раз вы не увидите перерисовку дочерних компонентов. Поэтому временная метка тоже не обновится. Но вы увидите ту часть компонента, где данные изменились, поскольку она отразит изменения сама по себе. Также вам больше не нужно будет использовать `memo`, `useMemo()` или `useCallback()` в вашем коде. + +Вы можете увидеть это в работе визуально [здесь](https://youtu.be/bdWUVp0TbTU?t=1326). + +## React Compiler в React DevTools + +[React DevTools](https://react.dev/learn/react-developer-tools) версии 5.0+ имеет встроенную поддержку React Compiler. Вы увидите значок с текстом _Memo ✨_ рядом с компонентами, оптимизированными React Compiler. Это фантастично! + +![React DevTools](./react-compiler-8.png) + +## Погружение в детали – Как работает React Compiler? + +Теперь, когда вы увидели, как React Compiler работает с кодом React 19, давайте глубоко погрузимся в понимание того, что происходит в фоне. Мы будем использовать [React Compiler Playground](https://playground.react.dev/) для изучения переведенного кода и шагов оптимизации. + +![React Compiler Playground](./react-compiler-9.png) + +Мы будем использовать компонент `Heading` в качестве примера. Скопируйте и вставьте следующий код в левую секцию playground: + +```jsx +const Heading = ({ heading, totalProducts }) => { + return ( + + ); +}; +``` + +Вы увидите, что некоторый JavaScript код генерируется немедленно внутри вкладки `_JS` playground. React Compiler генерирует этот JavaScript код как часть процесса компиляции. Давайте разберем его шаг за шагом: + +```jsx +function anonymous_0(t0) { + const $ = _c(4); + const { heading, totalProducts } = t0; + let t1; + if ($[0] === Symbol.for('react.memo_cache_sentinel')) { + t1 = Date.now(); + $[0] = t1; + } else { + t1 = $[0]; + } + let t2; + if ($[1] !== heading || $[2] !== totalProducts) { + t2 = ( + + ); + $[1] = heading; + $[2] = totalProducts; + $[3] = t2; + } else { + t2 = $[3]; + } + return t2; +} +``` + +The compiler uses a hook called `_c()` to create an array of items to cache. In the code above, an array of four elements has been created to cache four items. + +```js +const $ = _c(4); +``` + +But, what are the things to cache? + +- The component takes two props, `heading` and `totalProducts`. The compiler needs to cache them. So, it needs two elements in the array of cacheable items. +- The `Date.now()` part in the header should be cached. +- The JSX itself should be cached. There is no point in computing JSX unless either of the above changes. + +So there are a total of four items to cache. + +The compiler creates memoization blocks using the `if-block`. The final return value from the compiler is the JSX which depends on three dependencies: + +- The `Date.now()` value. +- Two props, a `heading` and `totalProducts` + +The output JSX needs re-computation when any of the above changes. This means that the compiler needs to create two memoization blocks for each of the above. + +The first memoization block looks like this: + +```jsx +if ($[0] === Symbol.for('react.memo_cache_sentinel')) { + t1 = Date.now(); + $[0] = t1; +} else { + t1 = $[0]; +} +``` + +The if-block stores the value of the `Date.now()` into the first index of the cacheable array. It re-uses the same every time unless it is changed. + +Similarly, in the second memoization block: + +```jsx +if ($[1] !== heading || $[2] !== totalProducts) { + t2 = ( + + ); + $[1] = heading; + $[2] = totalProducts; + $[3] = t2; +} else { + t2 = $[3]; +} +``` + +Here, the check is for the value changes for either `heading` or `totalProducts` props. If either of these changes, the JSX needs to be recomputed. All the values are then stored in the cacheable array. If there are no changes in the value, the previously computed JSX is returned from the cache. + +You can now paste any other component source code into the left side and look into the generated JavaScript code to help you understand what's going on as we did above. This will help you to get a better grip on how the compiler performs the memoization techniques in the compilation process. + +## How Do You Opt in and Out of the React Compiler? + +Once you've configured the React compiler the way we have done with our Vite project here, it's enabled for all the compilers and hooks of the project. + +But in some cases, you may want to selectively opt-in for the React compiler. In that case, you can run the compiler in “opt-in” mode using the `compilationMode: "annotation"` option. + +```jsx +// Specify the option in the ReactCompilerConfig +const ReactCompilerConfig = { + compilationMode: 'annotation', +}; +``` + +Then annotate the components and hooks you want to opt-in for compilation with the `"use memo"` directive. + +```jsx +// src/ProductPage.jsx +export default function ProductPage() { + 'use memo'; + // ... +} +``` + +Note that there is a `"use no memo"` directive as well. There might be some rare cases where your component may not be working as expected after compilation, and you want to opt out of the compilation temporarily until the issue is identified and fixed. In that case, you can use this directive: + +```jsx +function AComponent() { + 'use no memo'; + // ... +} +``` + +## Can We Use the React Compiler with React 18.x? + +It is recommended to use the React compiler with React 19 as there are required compatibilities. If you can't upgrade your application to React 19, you'll need to have a custom implementation of the cache function. You can go over [this thread](https://github.com/reactwg/react-compiler/discussions/6) describing the workaround. + +### Repositories to Look Into + +- All the source code used in this article is in [this repository](https://github.com/tapascript/react-compiler-lesson). +- If you want to start coding with React 19 and its features, [here is a template repository](https://github.com/atapas/code-in-react-19) configured with React 19 RC, Vite, and TailwindCSS. You may want to try it out. + +## What's Next? + +To learn further, + +- Check out the official documentation of React Compiler [from here](https://react.dev/learn/react-compiler). +- Check out the [discussions](https://github.com/reactwg/react-compiler/discussions) in the Working Group. + +Up next, if you are willing to learn React and its ecosystem-like Next.js with both fundamental concepts and projects, I have great news for you: you can check out this [playlist](https://www.youtube.com/watch?v=VSB2h7mVhPg&list=PLIJrr73KDmRwz_7QUvQ9Az82aDM9I8L_8) on my YouTube channel with 22+ video tutorials and 12+ hours of engaging content so far, for free. I hope you like them as well. + +That's all for now. Did you enjoy reading this article and have you learned something new? If so, I would love to know if the content was helpful. + +:material-information-outline: Источник — diff --git a/docs/guides/react-compiler/react-compiler-1.webp b/docs/guides/react-compiler/react-compiler-1.webp new file mode 100644 index 0000000..3335e90 Binary files /dev/null and b/docs/guides/react-compiler/react-compiler-1.webp differ diff --git a/docs/guides/react-compiler/react-compiler-2.webp b/docs/guides/react-compiler/react-compiler-2.webp new file mode 100644 index 0000000..be5f5fa Binary files /dev/null and b/docs/guides/react-compiler/react-compiler-2.webp differ diff --git a/docs/guides/react-compiler/react-compiler-3.webp b/docs/guides/react-compiler/react-compiler-3.webp new file mode 100644 index 0000000..3016b57 Binary files /dev/null and b/docs/guides/react-compiler/react-compiler-3.webp differ diff --git a/docs/guides/react-compiler/react-compiler-4.webp b/docs/guides/react-compiler/react-compiler-4.webp new file mode 100644 index 0000000..0f4b915 Binary files /dev/null and b/docs/guides/react-compiler/react-compiler-4.webp differ diff --git a/docs/guides/react-compiler/react-compiler-5.webp b/docs/guides/react-compiler/react-compiler-5.webp new file mode 100644 index 0000000..8afb409 Binary files /dev/null and b/docs/guides/react-compiler/react-compiler-5.webp differ diff --git a/docs/guides/react-compiler/react-compiler-6.webp b/docs/guides/react-compiler/react-compiler-6.webp new file mode 100644 index 0000000..5d68312 Binary files /dev/null and b/docs/guides/react-compiler/react-compiler-6.webp differ diff --git a/docs/guides/react-compiler/react-compiler-7.webp b/docs/guides/react-compiler/react-compiler-7.webp new file mode 100644 index 0000000..bc09fa3 Binary files /dev/null and b/docs/guides/react-compiler/react-compiler-7.webp differ diff --git a/docs/guides/react-compiler/react-compiler-8.png b/docs/guides/react-compiler/react-compiler-8.png new file mode 100644 index 0000000..179bbad Binary files /dev/null and b/docs/guides/react-compiler/react-compiler-8.png differ diff --git a/docs/guides/react-compiler/react-compiler-9.png b/docs/guides/react-compiler/react-compiler-9.png new file mode 100644 index 0000000..467b528 Binary files /dev/null and b/docs/guides/react-compiler/react-compiler-9.png differ diff --git a/docs/index.md b/docs/index.md index 9f93bca..eaae098 100644 --- a/docs/index.md +++ b/docs/index.md @@ -40,7 +40,7 @@ hide: Фреймворки для старта нового проекта - [:octicons-arrow-right-24: Next.js](libs/nextjs/index.md) + **[:octicons-arrow-right-24: Next.js](libs/nextjs/index.md)** v14+ [:octicons-arrow-right-24: Create React App](libs/cra.md) @@ -60,27 +60,21 @@ hide: [:octicons-arrow-right-24: Redux Toolkit](libs/redux-toolkit.md) -- :simple-apollographql:{ .lg .middle } **Apollo** - - *** - - Библиотеки GraphQL API - [:octicons-arrow-right-24: GraphQL](libs/graphql/index.md) - :octicons-arrow-right-24: React Apollo Client v3 - - :octicons-arrow-right-24: Apollo Server :octicons-link-external-16: - - :material-state-machine:{ .lg .middle } **Другие менеджеры** *** Библиотеки менеджеров состояния + **[:octicons-arrow-right-24: XState](libs/xstate.5/index.md)** v5 + + [:octicons-arrow-right-24: Zustand](./libs/zustand/getting-started/introduction.md) v4 + [:octicons-arrow-right-24: React Query](libs/react-query.md) - [:octicons-arrow-right-24: XState](libs/xstate/index.md) v4 + [:octicons-arrow-right-24: XState](libs/xstate/index.md) v4 @@ -126,14 +120,7 @@ hide: [![Typescript](ts.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} Typescript](https://scriptdev.ru/)     [![Angular](angular.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} Angular](https://angdev.ru/)     **[![React](react.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} React](https://reactdev.ru/)**     - [![Solid](solid.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} Solid](https://soliddev.ru/)     [![React Native](rn.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} React Native](https://reactnativedev.ru/)     [![PWA](pwa.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} PWA](https://pwadev.ru/)     [![Node.js](nodejs.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} Node.js](https://nodejsdev.ru/)     - [![Python 3](python.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} Python 3](https://py3dev.ru/)     [![XSLT](xslt.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} XSLT](https://xsltdev.ru/)     - [![БД](db.svg){: class="nolightbox" style="height:16px;width:16px;vertical-align:middle;"} Базы данных](https://dbasedev.ru/)     - - diff --git a/docs/learn/.pages b/docs/learn/.pages index 347f2ea..271cddf 100644 --- a/docs/learn/.pages +++ b/docs/learn/.pages @@ -11,6 +11,7 @@ nav: - editor-setup.md - typescript.md - react-developer-tools.md + - react-compiler.md - "Разработка UI": - describing-the-ui.md - your-first-component.md diff --git a/docs/learn/conditional-rendering.md b/docs/learn/conditional-rendering.md index 27b561c..db1aef6 100644 --- a/docs/learn/conditional-rendering.md +++ b/docs/learn/conditional-rendering.md @@ -568,7 +568,7 @@ if (isPacked) { Обратите внимание, что вы должны написать `importance > 0 && ...`, а не `importance && ...`, чтобы если `importance` равно `0`, `0` не отображалось как результат! - В этом решении используются два отдельных условия для вставки пробела между именем и меткой важности. В качестве альтернативы можно использовать фрагмент с пробелом: `importance > 0 && <> ...` или добавьте пробел непосредственно внутри ``: `importance > 0 && ...`. + В этом решении используются два отдельных условия для вставки пробела между именем и меткой важности. В качестве альтернативы можно использовать фрагмент с пробелом: `importance > 0 && <​> ...<​/​>` или добавьте пробел непосредственно внутри ``: `importance > 0 && ...`. ### 3. Рефакторинг серии `? :` на `if` и переменные {#refactor-a-series-of---to-if-and-variables} diff --git a/docs/learn/manipulating-the-dom-with-refs.md b/docs/learn/manipulating-the-dom-with-refs.md index a1b9c7b..e4aa401 100644 --- a/docs/learn/manipulating-the-dom-with-refs.md +++ b/docs/learn/manipulating-the-dom-with-refs.md @@ -165,6 +165,8 @@ myRef.current.scrollIntoView(); +### Управление списком ссылок через callback ref {#how-to-manage-a-list-of-refs-using-a-ref-callback} + !!!note "Как управлять списком ссылок с помощью обратного вызова" В приведенных выше примерах существует предопределенное количество ссылок. Однако иногда вам может понадобиться ссылка на каждый элемент списка, и вы не знаете, сколько их будет. Что-то вроде этого **не будет работать**: @@ -625,7 +627,7 @@ React устанавливает `ref.current` во время фиксации. - Refs - это общая концепция, но чаще всего вы будете использовать их для хранения элементов DOM. - Вы даете команду React поместить узел DOM в `myRef.current`, передавая `
`. - - Обычно вы используете рефлексы для неразрушающих действий, таких как фокусировка, прокрутка или измерение элементов DOM. + - Обычно вы используете refs для неразрушающих действий, таких как фокусировка, прокрутка или измерение элементов DOM. - Компонент по умолчанию не раскрывает свои DOM-узлы. Вы можете раскрыть узел DOM, используя `forwardRef` и передавая второй аргумент `ref` вниз к определенному узлу. - Избегайте изменения узлов DOM, управляемых React. - Если вы изменяете узлы DOM, управляемые React, изменяйте те части, которые React не имеет причин обновлять. diff --git a/docs/learn/react-compiler.md b/docs/learn/react-compiler.md new file mode 100644 index 0000000..e9fcf48 --- /dev/null +++ b/docs/learn/react-compiler.md @@ -0,0 +1,305 @@ +--- +status: experimental +description: На этой странице вы узнаете о новом экспериментальном компиляторе React Compiler и о том, как его успешно использовать +--- + +# React Compiler + + +На этой странице вы узнаете о новом экспериментальном компиляторе React Compiler и о том, как его успешно использовать. + + +!!!warning "Внимание" + + Эта документация все еще находится в процессе разработки. Больше документации доступно в репозитории [React Compiler Working Group repo](https://github.com/reactwg/react-compiler/discussions), и будет перенесено в эту документацию, когда она станет более стабильной. + +!!!tip "Вы узнаете" + + - Начало работы с компилятором + - Установка компилятора и плагина eslint + - Устранение неполадок + +!!!note "Готовность" + + **React Compiler** - это новый экспериментальный компилятор, который мы выложили в открытый доступ, чтобы получить первые отзывы от сообщества. Он все еще имеет шероховатости и пока не полностью готов к использованию. + + Для работы React Compiler требуется React 19 Beta. + +React Compiler - это новый экспериментальный компилятор, который мы выложили в открытый доступ, чтобы получить ранние отзывы от сообщества. Это инструмент, который автоматически оптимизирует ваше React-приложение только во время сборки. Он работает с обычным JavaScript и понимает [Rules of React](../reference/rules/index.md), поэтому вам не нужно переписывать код, чтобы использовать его. + +В состав компилятора также входит плагин [eslint](#installing-eslint-plugin-react-compiler), который отображает анализ от компилятора прямо в вашем редакторе. Плагин работает независимо от компилятора и может быть использован, даже если вы не используете компилятор в своем приложении. Мы рекомендуем всем разработчикам React использовать этот плагин eslint для улучшения качества вашей кодовой базы. + +## Что делает компилятор? {#what-does-the-compiler-do} + +Компилятор понимает ваш код на глубоком уровне благодаря пониманию семантики обычного JavaScript и [Rules of React](../reference/rules/index.md). Это позволяет ему добавлять автоматические оптимизации в ваш код. + +Сегодня вы можете быть знакомы с ручной мемоизацией через [`useMemo`](../reference/react/useMemo.md), [`useCallback`](../reference/react/useCallback.md) и [`React.memo`](../reference/react/memo.md). Компилятор может автоматически сделать это за вас, если ваш код следует [Rules of React](../reference/rules/index.md). Если он обнаружит нарушения правил, то автоматически пропустит только эти компоненты или хуки и продолжит безопасную компиляцию остального кода. + +Если ваша кодовая база уже очень хорошо мемоизирована, вы можете не ожидать значительного повышения производительности компилятора. Тем не менее, на практике правильно указать зависимости, вызывающие проблемы с производительностью, вручную довольно сложно. + +## Стоит ли мне попробовать компилятор? {#should-i-try-out-the-compiler} + +Обратите внимание, что компилятор все еще является экспериментальным и имеет много неровностей. Хотя он уже используется в производстве в таких компаниях, как Meta, внедрение компилятора в производство для вашего приложения будет зависеть от состояния вашей кодовой базы и от того, насколько хорошо вы следуете [Правилам React](../reference/rules/index.md). + +**Вы не должны торопиться использовать компилятор сейчас. Можно подождать до выхода стабильного релиза, прежде чем использовать его.** Однако мы будем рады, если вы попробуете его в небольших экспериментах в своих приложениях, чтобы вы могли [предоставить нам обратную связь](#reporting-issues), чтобы помочь сделать компилятор лучше. + +## Начало работы {#getting-started} + +В дополнение к этой документации мы рекомендуем посетить [React Compiler Working Group](https://github.com/reactwg/react-compiler) для получения дополнительной информации и обсуждения компилятора. + +### Развертывание компилятора в вашей кодовой базе {#использование компилятора эффективно} + +#### Существующие проекты {#existing-projects} + +Компилятор предназначен для компиляции функциональных компонентов и хуков, которые следуют [Правилам React](../reference/rules/index.md). Он также может обрабатывать код, который нарушает эти правила, отбрасывая (пропуская) такие компоненты или хуки. Однако из-за гибкой природы JavaScript компилятор не может отловить все возможные нарушения и может компилировать с ложными отрицательными результатами: то есть компилятор может случайно скомпилировать компонент/хук, который нарушает правила React, что может привести к неопределенному поведению. + +По этой причине для успешного внедрения компилятора в существующие проекты мы рекомендуем сначала запустить его на небольшом каталоге в коде вашего продукта. Это можно сделать, настроив компилятор на запуск только в определенном наборе директорий: + +```js hl_lines="3" +const ReactCompilerConfig = { + sources: (filename) => { + return filename.indexOf('src/path/to/dir') !== -1; + }, +}; +``` + +В редких случаях вы также можете настроить компилятор на работу в режиме «opt-in» с помощью опции `compilationMode: "annotation"`. В этом случае компилятор будет компилировать только компоненты и хуки, аннотированные директивой `"use memo"`. Обратите внимание, что режим `annotation` является временным, чтобы помочь ранним пользователям, и что мы не планируем использовать директиву `"use memo"` в долгосрочной перспективе. + +```js hl_lines="2 7" +const ReactCompilerConfig = { + compilationMode: 'annotation', +}; + +// src/app.jsx +export default function App() { + 'use memo'; + // ... +} +``` + +Когда вы будете более уверены в развертывании компилятора, вы сможете расширить охват и на другие каталоги и постепенно развернуть его на все приложение. + +#### Новые проекты {#new-projects} + +Если вы начинаете новый проект, вы можете включить компилятор для всей вашей кодовой базы, что является поведением по умолчанию. + +## Установка {#installation} + +### Проверка совместимости {#checking-compatibility} + +Перед установкой компилятора вы можете сначала проверить, совместима ли ваша кодовая база: + +```sh +npx react-compiler-healthcheck +``` + +Этот скрипт будет: + +- Проверить, сколько компонентов может быть успешно оптимизировано: чем больше, тем лучше +- Проверит использование ``: если он включен и соблюдается, то вероятность соблюдения [Rules of React](../reference/rules/index.md) выше. +- Проверка использования несовместимых библиотек: известные библиотеки, которые несовместимы с компилятором + +В качестве примера: + +``` +Successfully compiled 8 out of 9 components. +StrictMode usage not found. +Found no usage of incompatible libraries. +``` + +### Установка eslint-plugin-react-compiler {#installing-eslint-plugin-react-compiler} + +React Compiler также поддерживает плагин eslint. Плагин eslint может использоваться **независимо** от компилятора, то есть вы можете использовать плагин eslint, даже если вы не используете компилятор. + +``` +npm install eslint-plugin-react-compiler +``` + +Затем добавьте его в конфигурацию eslint: + +```js +module.exports = { + plugins: ['eslint-plugin-react-compiler'], + rules: { + 'react-compiler/react-compiler': 2, + }, +}; +``` + +### Использование с Babel {#usage-with-babel} + +```sh +npm install babel-plugin-react-compiler +``` + +Компилятор включает в себя плагин Babel, который вы можете использовать в своем конвейере сборки для запуска компилятора. + +После установки добавьте его в конфигурацию Babel. Обратите внимание, что очень важно, чтобы компилятор запускался **первым** в конвейере: + +```js hl_lines="7" +// babel.config.js +const ReactCompilerConfig = { + /* ... */ +}; + +module.exports = function () { + return { + plugins: [ + [ + 'babel-plugin-react-compiler', + ReactCompilerConfig, + ], // must run first! + // ... + ], + }; +}; +``` + +`babel-plugin-react-compiler` должен запускаться первым перед другими плагинами Babel, поскольку компилятору требуется исходная информация для анализа языка. + +### Использование с Vite {#usage-with-vite} + +Если вы используете Vite, вы можете добавить плагин в vite-plugin-react: + +```js +// vite.config.js +const ReactCompilerConfig = { + /* ... */ +}; + +export default defineConfig(() => { + return { + plugins: [ + react({ + babel: { + plugins: [ + [ + 'babel-plugin-react-compiler', + ReactCompilerConfig, + ], + ], + }, + }), + ], + // ... + }; +}); +``` + +### Использование с Next.js {#usage-with-nextjs} + +Next.js позволяет использовать более медленный конвейер сборки через Babel, который может быть включен [настройкой Babel](#usage-with-babel) путем добавления файла `babel.config.js`. + +### Использование с Remix {#usage-with-remix} + +Установите `vite-plugin-babel` и добавьте к нему плагин компилятора Babel: + +```sh +npm install vite-plugin-babel +``` + +--- + +```js +// vite.config.js +import babel from 'vite-plugin-babel'; + +const ReactCompilerConfig = { + /* ... */ +}; + +export default defineConfig({ + plugins: [ + remix({ + /* ... */ + }), + babel({ + filter: /\.[jt]sx?$/, + babelConfig: { + presets: ['@babel/preset-typescript'], // if you use TypeScript + plugins: [ + [ + 'babel-plugin-react-compiler', + ReactCompilerConfig, + ], + ], + }, + }), + ], +}); +``` + +### Использование с Webpack {#usage-with-webpack} + +Вы можете создать свой собственный загрузчик для React Compiler, например, так: + +```js +const ReactCompilerConfig = { + /* ... */ +}; +const BabelPluginReactCompiler = require('babel-plugin-react-compiler'); + +function reactCompilerLoader(sourceCode, sourceMap) { + // ... + const result = transformSync(sourceCode, { + // ... + plugins: [ + [BabelPluginReactCompiler, ReactCompilerConfig], + ], + // ... + }); + + if (result === null) { + this.callback( + Error( + `Failed to transform "${options.filename}"` + ) + ); + return; + } + + this.callback( + null, + result.code, + result.map === null ? undefined : result.map + ); +} + +module.exports = reactCompilerLoader; +``` + +### Использование с Expo {#usage-with-expo} + +Expo использует Babel через Metro, поэтому обратитесь к разделу [Usage with Babel](#usage-with-babel) за инструкциями по установке. + +### Использование с React Native (Metro) {#usage-with-react-native-metro} + +React Native использует Babel через Metro, поэтому обратитесь к разделу [Usage with Babel](#usage-with-babel) за инструкциями по установке. + +## Устранение неполадок {#troubleshooting} + +### Сообщение о проблемах {#reporting-issues} + +Чтобы сообщить о проблемах, пожалуйста, сначала создайте минимальный реплейсер на [React Compiler Playground](https://playground.react.dev/) и включите его в ваше сообщение об ошибке. + +Вы можете открыть проблемы в репо [facebook/react](https://github.com/facebook/react/issues). + +Вы также можете оставить отзыв в рабочей группе React Compiler Working Group, подав заявку на вступление. Подробности о вступлении смотрите в [README](https://github.com/reactwg/react-compiler). + +### Общие проблемы {#common-issues} + +#### Ошибка `(0 , _c) is not a function` + +Это происходит во время оценки модуля JavaScript, если вы не используете React 19 Beta и выше. Чтобы исправить это, сначала [обновите свое приложение до React 19 Beta](https://react.dev/blog/2024/04/25/react-19-upgrade-guide). + +### Отладка {#debugging} + +#### Проверка того, были ли оптимизированы компоненты {#checking-if-components-have-been-optimized} + +##### React DevTools {#react-devtools} + +React Devtools (v5.0+) имеет встроенную поддержку React Compiler и будет отображать значок «Memo ✨» рядом с компонентами, которые были оптимизированы компилятором. + +##### Другие вопросы {#other-issues} + +См. . diff --git a/docs/learn/referencing-values-with-refs.md b/docs/learn/referencing-values-with-refs.md index 461c513..4e2af9f 100644 --- a/docs/learn/referencing-values-with-refs.md +++ b/docs/learn/referencing-values-with-refs.md @@ -265,7 +265,7 @@ const [now, setNow] = useState(null); - **Рассматривайте ссылки как аварийный люк.** Ссылки полезны, когда вы работаете с внешними системами или API браузера. Если большая часть логики вашего приложения и потока данных зависит от ссылок, возможно, вам стоит пересмотреть свой подход. - **Не читайте и не записывайте `ref.current` во время рендеринга.** Если какая-то информация необходима во время рендеринга, используйте [state](state-a-components-memory.md) вместо этого. Поскольку React не знает, когда изменяется `ref.current`, даже чтение его во время рендеринга делает поведение вашего компонента труднопредсказуемым. (Единственным исключением из этого является код типа `if (!ref.current) ref.current = new Thing()`, который устанавливает ссылку только один раз во время первого рендеринга). -Ограничения React state не распространяются на рефлексы. Например, состояние действует как [снимок для каждого рендера](state-as-a-snapshot.md) и [не обновляется синхронно](queueing-a-series-of-state-updates.md) Но когда вы изменяете текущее значение ссылки, оно немедленно меняется: +Ограничения React state не распространяются на refs. Например, состояние действует как [снимок для каждого рендера](state-as-a-snapshot.md) и [не обновляется синхронно](queueing-a-series-of-state-updates.md) Но когда вы изменяете текущее значение ссылки, оно немедленно меняется: ```js ref.current = 5; @@ -274,7 +274,7 @@ console.log(ref.current); // 5 Это происходит потому, что **ссылка сама по себе является обычным объектом JavaScript,** и поэтому ведет себя как обычный объект. -Вам также не нужно беспокоиться о [избегании мутации](updating-objects-in-state.md), когда вы работаете с ref. До тех пор, пока объект, который вы мутируете, не используется для рендеринга, React не волнует, что вы делаете с рефлексом или его содержимым. +Вам также не нужно беспокоиться о [избегании мутации](updating-objects-in-state.md), когда вы работаете с ref. До тех пор, пока объект, который вы мутируете, не используется для рендеринга, React не волнует, что вы делаете с ref или его содержимым. ## Ссылки и DOM {#refs-and-the-dom} diff --git a/docs/learn/responding-to-events.md b/docs/learn/responding-to-events.md index f20f14d..616a921 100644 --- a/docs/learn/responding-to-events.md +++ b/docs/learn/responding-to-events.md @@ -208,7 +208,7 @@ description: React позволяет добавлять обработчики ### Именование параметров обработчика событий {#naming-event-handler-props} -Встроенные компоненты, такие как `; - -// pages/index.js -import { Button } from 'components/button'; - -export default function Index() { - return ( - <> -

Привет, народ!

- ; - -// pages/index.js -import { Button } from '@/button'; - -export default function Index() { - return ( - <> -

Привет, народ!

- - ); -} -``` - -Переключение на страницу `pages/post/[pid].js`: - -```js -import { useRouter } from 'next/router'; - -export default function Page() { - const router = useRouter(); - - return ( - - ); -} -``` - -Перенаправление пользователя на страницу `pages/login.js` (может использоваться для реализации защищенных страниц): - -```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 ? ( -

Привет, {user.name}!

- ) : ( -

Загрузка...

- ); -} -``` - -Пример использования объекта вместо строки: - -```js -import { useRouter } from 'next/router'; - -export default function ReadMore({ post }) { - const router = useRouter(); - - return ( - - ); -} -``` - -#### router.replace - -Метод `router.replace` обновляет путь текущей страницы без добавления URL в стек `history`. - -```js -router.replace(url, as, options); -``` - -Использование - -```js -import { useRouter } from 'next/router'; - -export default function Page() { - const router = useRouter(); - - return ( - - ); -} -``` - -#### router.prefetch - -Метод `router.prefetch` позволяет выполнять предварительную загрузку страниц для ускорения навигации. Обратите внимание: `next/link` выполняет предварительную загрузку страниц автоматически. - -```js -router.prefetch(url, as); -``` - -Использование. Предположим, что после авторизации мы перенаправляем пользователя на страницу профиля. Для ускорения навигации мы можем предварительно загрузить страницу профиля: - -```js -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 ( -
- {/* Поля формы */} - -
- ); -} -``` - -#### router.beforePopState - -Метод `router.beforePopState` позволяет реагировать на событие `popstate` перед обработкой пути роутером. - -```js -router.beforePopState(cb); -``` - -`cb` — это функция, которая запускается при возникновении события `popstate`. Данная функция получает объект события со следующими свойствами: - -- `url: string` — путь для нового состояния (как правило, название страницы) -- `as: string` — URL, который будет отображен в браузере -- `options: object` — дополнительные настройки из `router.push` - -Использование. `beforePopState` может использоваться для модификации запроса или принудительного обновления SSR, как в следующем примере: - -```js -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

Добро пожаловать!

; -} -``` - -#### router.back - -Данный метод позволяет вернуться к предыдущей странице. При его использовании выполняется `window.history.back()`: - -```js -import { useRouter } from 'next/router'; - -export default function Page() { - const router = useRouter(); - - return ( - - ); -} -``` - -#### router.reload - -Данный метод перезагружает текущий URL. При его использовании выполняется `window.location.reload()`: - -```js -import { useRouter } from 'next/router'; - -export default function Page() { - const router = useRouter(); - - return ( - - ); -} -``` - -#### 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`. - -Использование - -```js -// 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 ; -} -``` - -#### withRouter - -Вместо `useRouter` может использоваться `withRouter`: - -```js -import { withRouter } from 'next/router'; - -function Page({ router }) { - return

{router.pathname}

; -} - -export default withRouter(Page); -``` - -#### TypeScript - -```js -import React from 'react'; -import { withRouter, NextRouter } from 'next/router'; - -interface WithRouterProps { - router: NextRouter; -} - -interface MyComponentProps extends WithRouterProps {} - -class MyComponent extends React.Component { - render() { - return

{this.props.router.pathname}

; - } -} - -export default withRouter(MyComponent); -``` - -### next/link - -Компонент `Link`, экспортируемый из `next/link`, позволяет переключаться между страницами на стороне клиента. - -Предположим, что в директории pages содержатся следующие файлы: - -``` -pages/index.js -pages/about.js -pages/blog/[slug].js -``` - -Пример навигации по этим страницам: - -```js -import Link from 'next/link'; - -export default function Home() { - return ( - - ); -} -``` - -`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`: - -```js -import Link from 'next/link'; - -export default function Posts({ posts }) { - return ( - - ); -} -``` - -#### Кастомный компонент — обертка для тега a - -Если дочерним компонентом `Link` является кастомный компонент, оборачивающий `a`, `Link` должен иметь проп `passHref`. Это необходимо при использовании таких библиотек как styled-components. Без этого тег `a` не получит атрибут `href`, что негативно скажется на доступности и SEO. - -```js -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 ( - - {name} - - ); -} -``` - -#### Функциональный компонент - -Если дочерним компонентом `Link` является функция, то кроме `passHref`, необходимо обернуть ее в `React.forwardRef`: - -```js -import Link from 'next/link'; -import { forwardRef } from 'react'; - -// `onClick`, `href` и `ref` должны быть переданы DOM-элементу -const MyButton = forwardRef(({ onClick, href }, ref) => { - return ( - - Кликни - - ); -}); - -export default function Home() { - return ( - - - - ); -} -``` - -#### Объект URL - -`Link` также принимает объект URL. Данный объект автоматически преобразуется в строку: - -```js -import Link from 'next/link'; - -export default function Home() { - return ( - - ); -} -``` - -В приведенном примере у нас имеются ссылки на: - -- предопределенный роут: `/about?name=test` -- динамический роут: `/blog/my-post` - -#### Замена URL - -```jsx - - О нас - -``` - -#### Отключение прокрутки - -```jsx - - Загрузить еще - -``` - -### next/image - -#### Обязательные пропы - -Компонент `Image` принимает следующие обязательные пропы: - -- `src` — статически импортированный файл или строка, которая может быть абсолютной ссылкой или относительным путем в зависимости от пропа `loader` или настроек загрузки. При использовании ссылок на внешние ресурсы, эти ресурсы должны быть указаны в разделе `domains` файла `next.config.js` -- `width` — ширина изображения в пикселях: целое число без единицы измерения -- `height` — высота изображения в пикселях: целое число без единицы измерения - -#### Опциональные пропы - -- `layout` — `intrinsic | fixed | responsive | fill`. Значением по умолчанию является `intrinsic` -- `loader` — кастомная функция для разрешения URL. Установка этого пропа перезаписывает настройки из раздела `images` в `next.config.js`. `loader` принимает параметры `src`, `width` и `quality` - -```js -import Image from 'next/image'; - -const myLoader = ({ src, width, quality }) => - `https://example.com/${src}?w=${width}&q=${ - quality || 75 - }`; - -const MyImage = (props) => ( - -); -``` - -- `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`. - -**Домены**. Настройка доменов для провайдеров изображений позволяет защитить приложение от атак, связанных с внедрением в изображение вредоносного кода. - -```js -module.exports = { - images: { - domains: ['assets.acme.com'], - }, -}; -``` - -**Размеры устройств**. Настройка `deviceSizes` позволяет определить список контрольных точек для ширины устройств потенциальных пользователей. Эти контрольные точки предназначены для предоставления подходящего изображения при использовании `layout="responsive"` или `layout="fill"`. - -```js -// настройки по умолчанию -module.exports = { - images: { - deviceSizes: [ - 640, - 750, - 828, - 1080, - 1200, - 1920, - 2048, - 3840, - ], - }, -}; -``` - -**Размеры изображений**. Настройка `imageSizes` позволяет определить размеры изображений в виде списка. Этот список объединяется с массивом размеров устройств для формирования полного перечня размеров для генерации набора источников (`srcset`) изображения. - -```js -module.exports = { - images: { - imageSizes: [16, 32, 48, 64, 96, 128, 256, 384], - }, -}; -``` - -### next/head - -Компонент `Head` позволяет добавлять элементы в [`head`](https://hcdev.ru/html/head/) страницы: - -```js -import Head from 'next/head'; - -export default function IndexPage() { - return ( -
- - Заголовок страницы - - -

Привет, народ!

-
- ); -} -``` - -Проп `key` позволяет дедуплицировать теги: - -```js -import Head from 'next/head'; - -export default function IndexPage() { - return ( -
- - Заголовок страницы - - - - - -

Привет, народ!

-
- ); -} -``` - -В данном случае будет отрендерен только ``. - -Контент `head` удаляется при размонтировании компонента. - -`title`, `meta` и другие элементы должны быть прямыми потомками `Head`. Они могут быть обернуты в один `` или рендериться из массива. - -### next/server - -Посредники создаются с помощью функции `middleware`, находящейся в файле `_middleware`. Интерфейс посредников основан на нативных объектах `FetchEvent`, `Response` и `Request`. - -Эти нативные объекты расширены для предоставления большего контроля над формированием ответов на основе входящих запросов. - -Сигнатура функции: - -```js -import type { - NextRequest, - NextFetchEvent, -} from 'next/server'; - -export type Middleware = ( - request: NextRequest, - event: NextFetchEvent -) => Promise | Response | undefined; -``` - -Функция не обязательно должна называться `middleware`. Это всего лишь соглашение. Функция также не обязательно должна быть асинхронной. - -#### NextRequest - -Объект `NextRequest` расширяет нативный интерфейс `Request` следующими методами и свойствами: - -- `cookies` — куки запроса -- `nextUrl` — расширенный, разобранный объект URL, предоставляющий доступ к таким свойствам, как `pathname`, `basePath`, `trailingSlash` и `i18n` -- `geo` — геоинформация о запросе -- `country` — код страны -- `region` — код региона -- `city` — город -- `latitude` — долгота -- `longitude` — широта -- `ip` — IP-адрес запроса -- `ua` — агент пользователя - -`NextRequest` может использоваться вместо `Request`. - -```js -import type { NextRequest } from 'next/server'; -``` - -#### NextFetchEvent - -`NextFetchEvent` расширяет объект `FetchEvent` методом `waitUntil`. - -Данный метод позволяет продолжить выполнение функции после отправки ответа. - -Например, `waitUntil` может использоваться для интеграции с такими системами мониторинга ошибок, как `Sentry`. - -```js -import type { NextFetchEvent } from 'next/server'; -``` - -#### NextResponse - -`NextResponse` расширяет `Response` следующими методами и свойствами: - -- `cookies` — куки ответа -- `redirect()` — возвращает `NextResponse` с набором перенаправлений (`redirects`) -- `rewrite()` — возвращает `NextResponse` с набором перезаписей (`rewrites`) -- `next()` — возвращает `NextResponse`, который является частью цепочки посредников - -```js -import { NextResponse } from 'next/server'; -``` diff --git a/docs/libs/nextjs/app-router/.pages b/docs/libs/nextjs/app-router/.pages new file mode 100644 index 0000000..6b7cfd4 --- /dev/null +++ b/docs/libs/nextjs/app-router/.pages @@ -0,0 +1,17 @@ +title: "Изучаем app router в Next.js" +nav: + - index.md + - getting-started.md + - css-styling.md + - optimizing-fonts-images.md + - creating-layouts-and-pages.md + - navigating-between-pages.md + - setting-up-your-database.md + - fetching-data.md + - static-and-dynamic-rendering.md + - streaming.md + - adding-search-and-pagination.md + - mutating-data.md + - error-handling.md + - adding-authentication.md + - adding-metadata.md \ No newline at end of file diff --git a/docs/libs/nextjs/app-router/404-not-found-page.png b/docs/libs/nextjs/app-router/404-not-found-page.png new file mode 100644 index 0000000..b2fc3e0 Binary files /dev/null and b/docs/libs/nextjs/app-router/404-not-found-page.png differ diff --git a/docs/libs/nextjs/app-router/acme-unstyled.png b/docs/libs/nextjs/app-router/acme-unstyled.png new file mode 100644 index 0000000..2f1627c Binary files /dev/null and b/docs/libs/nextjs/app-router/acme-unstyled.png differ diff --git a/docs/libs/nextjs/app-router/adding-authentication.md b/docs/libs/nextjs/app-router/adding-authentication.md new file mode 100644 index 0000000..45cfc88 --- /dev/null +++ b/docs/libs/nextjs/app-router/adding-authentication.md @@ -0,0 +1,515 @@ +--- +description: В предыдущей главе вы завершили создание маршрутов счетов-фактур, добавив проверку формы и улучшив доступность. В этой главе вы добавите аутентификацию в дашборд. +--- + +# Аутентификация + +В предыдущей главе вы завершили создание маршрутов счетов-фактур, добавив проверку формы и улучшив доступность. В этой главе вы добавите аутентификацию в дашборд. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Что такое аутентификация. + - Как добавить аутентификацию в приложение с помощью NextAuth.js. + - Как использовать Middleware для перенаправления пользователей и защиты маршрутов. + - Как использовать `UseActionState` в React для обработки отложенных состояний и ошибок формы. + +## Что такое аутентификация? + +Аутентификация - ключевая часть многих современных веб-приложений. С ее помощью система проверяет, является ли пользователь тем, за кого себя выдает. + +Безопасный веб-сайт часто использует несколько способов проверки личности пользователя. Например, после ввода имени пользователя и пароля сайт может отправить проверочный код на ваше устройство или использовать внешнее приложение, например Google Authenticator. Такая двухфакторная аутентификация (2FA) помогает повысить уровень безопасности. Даже если кто-то узнает ваш пароль, он не сможет получить доступ к вашей учетной записи без вашего уникального маркера. + +### Аутентификация и авторизация + +В веб-разработке аутентификация и авторизация выполняют разные функции: + +- **Аутентификация** - это проверка того, что пользователь является тем, за кого себя выдает. Вы подтверждаете свою личность с помощью чего-то, что у вас есть, например, имени пользователя и пароля. +- **Авторизация** - это следующий шаг. После того как личность пользователя подтверждена, авторизация определяет, какие части приложения ему разрешено использовать. + +Итак, аутентификация проверяет, кто вы, а авторизация определяет, что вы можете делать или к чему можете получить доступ в приложении. + +## Создание маршрута входа в систему + +Начните с создания нового маршрута в вашем приложении под названием `/login` и вставьте следующий код: + +```ts title="/app/login/page.tsx" +import AcmeLogo from '@/app/ui/acme-logo'; +import LoginForm from '@/app/ui/login-form'; +import { Suspense } from 'react'; + +export default function LoginPage() { + return ( +
+
+
+
+ +
+
+ + + +
+
+ ); +} +``` + +Вы заметите, что страница импортирует ``, который вы обновите позже в этой главе. Этот компонент обернут в React ``, потому что он будет получать доступ к информации из входящего запроса (параметры поиска URL). + +## NextAuth.js + +Мы будем использовать [NextAuth.js](https://nextjs.authjs.dev/) для добавления аутентификации в ваше приложение. NextAuth.js абстрагирует большую часть сложностей, связанных с управлением сессиями, входом и выходом из системы, а также другими аспектами аутентификации. Хотя вы можете реализовать эти функции вручную, этот процесс может занять много времени и привести к ошибкам. NextAuth.js упрощает этот процесс, предоставляя унифицированное решение для аутентификации в приложениях Next.js. + +## Установка NextAuth.js + +Установите NextAuth.js, выполнив следующую команду в терминале: + +```sh title="Terminal" +pnpm i next-auth@beta +``` + +Здесь вы устанавливаете `beta` версию NextAuth.js, которая совместима с Next.js 14+. + +Далее сгенерируйте секретный ключ для вашего приложения. Этот ключ используется для шифрования файлов cookie, обеспечивая безопасность пользовательских сессий. Для этого выполните следующую команду в терминале: + +```sh title="Terminal" +# macOS +openssl rand -base64 32 +# Windows can use https://generate-secret.vercel.app/32 +``` + +Затем в файле `.env` добавьте сгенерированный ключ в переменную `AUTH_SECRET`: + +```sh title=".env" hl_lines="1" +AUTH_SECRET=your-secret-key +``` + +Чтобы auth работал в продакшне, вам нужно будет обновить переменные окружения и в проекте Vercel. Посмотрите это [руководство](https://vercel.com/docs/environment-variables) о том, как добавить переменные окружения в Vercel. + +### Добавление опции `pages` + +Создайте файл `auth.config.ts` в корне нашего проекта, который экспортирует объект `authConfig`. Этот объект будет содержать параметры конфигурации для NextAuth.js. Пока что он будет содержать только опцию `pages`: + +```ts title="/auth.config.ts" +import type { NextAuthConfig } from 'next-auth'; + +export const authConfig = { + pages: { + signIn: '/login', + }, +} satisfies NextAuthConfig; +``` + +Вы можете использовать опцию `pages`, чтобы указать маршрут для пользовательских страниц входа, выхода и ошибок. Это не обязательно, но если добавить `signIn: '/login'` в опцию `pages`, пользователь будет перенаправлен на нашу пользовательскую страницу входа, а не на страницу NextAuth.js по умолчанию. + +## Защита маршрутов с помощью Next.js Middleware + +Далее добавьте логику для защиты маршрутов. Это не позволит пользователям получить доступ к страницам дашборда, если они не вошли в систему. + +```ts title="/auth.config.ts" hl_lines="7-19" +import type { NextAuthConfig } from 'next-auth'; + +export const authConfig = { + pages: { + signIn: '/login', + }, + callbacks: { + authorized({ auth, request: { nextUrl } }) { + const isLoggedIn = !!auth?.user; + const isOnDashboard = nextUrl.pathname.startsWith('/dashboard'); + if (isOnDashboard) { + if (isLoggedIn) return true; + return false; // Redirect unauthenticated users to login page + } else if (isLoggedIn) { + return Response.redirect(new URL('/dashboard', nextUrl)); + } + return true; + }, + }, + providers: [], // Add providers with an empty array for now +} satisfies NextAuthConfig; +``` + +Коллбэк `authorized` используется для проверки того, авторизован ли запрос для доступа к странице с помощью [Next.js Middleware](https://nextjs.org/docs/app/building-your-application/routing/middleware). Он вызывается перед завершением запроса и получает объект со свойствами `auth` и `request`. Свойство `auth` содержит сессию пользователя, а свойство `request` - входящий запрос. + +Параметр `providers` представляет собой массив, в котором перечисляются различные варианты входа в систему. На данный момент это пустой массив, чтобы удовлетворить конфигурацию NextAuth. Подробнее об этом вы узнаете в разделе [Добавление провайдера учетных данных](https://nextjs.org/learn/dashboard-app/adding-authentication#adding-the-credentials-provider). + +Далее вам нужно будет импортировать объект `authConfig` в файл Middleware. В корне вашего проекта создайте файл `middleware.ts` и вставьте в него следующий код: + +```ts title="/middleware.ts" +import NextAuth from 'next-auth'; +import { authConfig } from './auth.config'; + +export default NextAuth(authConfig).auth; + +export const config = { + // https://nextjs.org/docs/app/building-your-application/routing/middleware#matcher + matcher: [ + '/((?!api|_next/static|_next/image|.*\\.png$).*)', + ], +}; +``` + +Здесь вы инициализируете NextAuth.js объектом `authConfig` и экспортируете свойство `auth`. Вы также используете опцию `matcher` из Middleware, чтобы указать, что он должен запускаться по определенным путям. + +Преимущество использования Middleware для этой задачи заключается в том, что защищенные маршруты не начнут отрисовываться, пока Middleware не проверит аутентификацию, что повышает как безопасность, так и производительность вашего приложения. + +### Хеширование паролей + +Хорошей практикой является **хеширование** паролей перед их хранением в базе данных. Хеширование преобразует пароль в строку символов фиксированной длины, которая выглядит случайной, обеспечивая уровень безопасности, даже если данные пользователя открыты. + +При загрузке базы данных вы использовали пакет `bcrypt` для хэширования пароля пользователя перед его сохранением в базе данных. Позже в этой главе вы снова будете использовать его для проверки соответствия пароля, введенного пользователем, паролю в базе данных. Однако для пакета `bcrypt` вам придется создать отдельный файл. Это связано с тем, что `bcrypt` опирается на API Node.js, недоступные в Next.js Middleware. + +Создайте новый файл `auth.ts`, который будет содержать объект `authConfig`: + +```ts title="/auth.ts" +import NextAuth from 'next-auth'; +import { authConfig } from './auth.config'; + +export const { auth, signIn, signOut } = NextAuth({ + ...authConfig, +}); +``` + +### Добавление провайдера учетных данных + +Далее вам нужно будет добавить опцию `providers` для NextAuth.js. `providers` - это массив, в котором вы перечисляете различные варианты входа в систему, такие как Google или GitHub. В этом курсе мы сосредоточимся на использовании только [Credentials provider](https://authjs.dev/getting-started/providers/credentials-tutorial). + +Провайдер Credentials позволяет пользователям входить в систему с помощью имени пользователя и пароля. + +```ts title="/auth.ts" hl_lines="3 7" +import NextAuth from 'next-auth'; +import { authConfig } from './auth.config'; +import Credentials from 'next-auth/providers/credentials'; + +export const { auth, signIn, signOut } = NextAuth({ + ...authConfig, + providers: [Credentials({})], +}); +``` + +!!!info "Полезно знать" + + Существуют и другие альтернативные провайдеры, такие как [OAuth](https://authjs.dev/getting-started/providers/oauth-tutorial) или [email](https://authjs.dev/getting-started/providers/email-tutorial). Полный список возможностей см. в [документации NextAuth.js](https://authjs.dev/getting-started/providers). + +### Добавление функциональности авторизации + +Вы можете использовать функцию `authorize` для обработки логики аутентификации. Аналогично Server Actions, вы можете использовать `zod` для проверки электронной почты и пароля перед тем, как проверить, существует ли пользователь в базе данных: + +```ts title="/auth.ts" hl_lines="4 9-18" +import NextAuth from 'next-auth'; +import { authConfig } from './auth.config'; +import Credentials from 'next-auth/providers/credentials'; +import { z } from 'zod'; + +export const { auth, signIn, signOut } = NextAuth({ + ...authConfig, + providers: [ + Credentials({ + async authorize(credentials) { + const parsedCredentials = z + .object({ + email: z.string().email(), + password: z.string().min(6), + }) + .safeParse(credentials); + }, + }), + ], +}); +``` + +После проверки учетных данных создайте новую функцию `getUser`, которая будет запрашивать пользователя из базы данных. + +```ts title="/auth.ts" hl_lines="9-11 13-25 38-45" +import NextAuth from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import { authConfig } from './auth.config'; +import { z } from 'zod'; +import type { User } from '@/app/lib/definitions'; +import bcrypt from 'bcrypt'; +import postgres from 'postgres'; + +const sql = postgres(process.env.POSTGRES_URL!, { + ssl: 'require', +}); + +async function getUser( + email: string +): Promise { + try { + const user = await sql< + User[] + >`SELECT * FROM users WHERE email=${email}`; + return user[0]; + } catch (error) { + console.error('Failed to fetch user:', error); + throw new Error('Failed to fetch user.'); + } +} + +export const { auth, signIn, signOut } = NextAuth({ + ...authConfig, + providers: [ + Credentials({ + async authorize(credentials) { + const parsedCredentials = z + .object({ + email: z.string().email(), + password: z.string().min(6), + }) + .safeParse(credentials); + + if (parsedCredentials.success) { + const { + email, + password, + } = parsedCredentials.data; + const user = await getUser(email); + if (!user) return null; + } + + return null; + }, + }), + ], +}); +``` + +Затем вызовите `bcrypt.compare`, чтобы проверить, совпадают ли пароли: + +```ts title="/auth.ts" title="9-11 28 34" +import NextAuth from 'next-auth'; +import Credentials from 'next-auth/providers/credentials'; +import { authConfig } from './auth.config'; +import { z } from 'zod'; +import type { User } from '@/app/lib/definitions'; +import bcrypt from 'bcrypt'; +import postgres from 'postgres'; + +const sql = postgres(process.env.POSTGRES_URL!, { + ssl: 'require', +}); + +// ... + +export const { auth, signIn, signOut } = NextAuth({ + ...authConfig, + providers: [ + Credentials({ + async authorize(credentials) { + // ... + + if (parsedCredentials.success) { + const { + email, + password, + } = parsedCredentials.data; + const user = await getUser(email); + if (!user) return null; + const passwordsMatch = await bcrypt.compare( + password, + user.password + ); + + if (passwordsMatch) return user; + } + + console.log('Invalid credentials'); + return null; + }, + }), + ], +}); +``` + +Наконец, если пароли совпадают, вы хотите вернуть пользователя, в противном случае верните `null`, чтобы пользователь не смог войти в систему. + +### Обновление формы входа + +Теперь вам нужно связать логику авторизации с формой входа. В файле `actions.ts` создайте новое действие под названием `authenticate`. Это действие должно импортировать функцию `signIn` из файла `auth.ts`: + +```ts title="/app/lib/actions.ts" +'use server'; + +import { signIn } from '@/auth'; +import { AuthError } from 'next-auth'; + +// ... + +export async function authenticate( + prevState: string | undefined, + formData: FormData +) { + try { + await signIn('credentials', formData); + } catch (error) { + if (error instanceof AuthError) { + switch (error.type) { + case 'CredentialsSignin': + return 'Invalid credentials.'; + default: + return 'Something went wrong.'; + } + } + throw error; + } +} +``` + +Если возникла ошибка `'CredentialsSignin'`, вы хотите вывести соответствующее сообщение об ошибке. Вы можете узнать об ошибках NextAuth.js в [документации](https://errors.authjs.dev/). + +Наконец, в компоненте `login-form.tsx` вы можете использовать функцию React `useActionState` для вызова действия сервера, обработки ошибок формы и отображения ее состояния ожидания: + +```ts title="app/ui/login-form.tsx" hl_lines="1 11-13 16-23 26 74-85 91-98" +'use client'; + +import { lusitana } from '@/app/ui/fonts'; +import { + AtSymbolIcon, + KeyIcon, + ExclamationCircleIcon, +} from '@heroicons/react/24/outline'; +import { ArrowRightIcon } from '@heroicons/react/20/solid'; +import { Button } from '@/app/ui/button'; +import { useActionState } from 'react'; +import { authenticate } from '@/app/lib/actions'; +import { useSearchParams } from 'next/navigation'; + +export default function LoginForm() { + const searchParams = useSearchParams(); + const callbackUrl = + searchParams.get('callbackUrl') || '/dashboard'; + const [ + errorMessage, + formAction, + isPending, + ] = useActionState(authenticate, undefined); + + return ( +
+
+

+ Please log in to continue. +

+
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ + +
+ {errorMessage && ( + <> + +

+ {errorMessage} +

+ + )} +
+
+
+ ); +} +``` + +## Добавление функции выхода из системы + +Чтобы добавить функцию выхода из системы в ``, вызовите функцию `signOut` из `auth.ts` в элементе `
`: + +```ts title="/ui/dashboard/sidenav.tsx" hl_lines="5 15-18" +import Link from 'next/link'; +import NavLinks from '@/app/ui/dashboard/nav-links'; +import AcmeLogo from '@/app/ui/acme-logo'; +import { PowerIcon } from '@heroicons/react/24/outline'; +import { signOut } from '@/auth'; + +export default function SideNav() { + return ( +
+ // ... +
+ +
+ { + 'use server'; + await signOut({ redirectTo: '/' }); + }} + > + + +
+
+ ); +} +``` + +## Попробуйте + +Теперь попробуйте. Вы должны иметь возможность входить и выходить из приложения, используя следующие учетные данные: + +- Email: `user@nextmail.com` +- Password: `123456` + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/adding-metadata.md b/docs/libs/nextjs/app-router/adding-metadata.md new file mode 100644 index 0000000..f26432a --- /dev/null +++ b/docs/libs/nextjs/app-router/adding-metadata.md @@ -0,0 +1,179 @@ +--- +description: Метаданные имеют решающее значение для SEO и удобного обмена информацией. В этой главе мы обсудим, как можно добавить метаданные в приложение Next.js. +--- + +# Метаданные + +Метаданные имеют решающее значение для SEO и удобного обмена информацией. В этой главе мы обсудим, как можно добавить метаданные в приложение Next.js. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Что такое метаданные. + - Типы метаданных. + - Как добавить изображение Open Graph с помощью метаданных. + - Как добавить фавикон с помощью метаданных. + +## Что такое метаданные? + +В веб-разработке метаданные представляют собой дополнительные сведения о веб-странице. Метаданные не видны пользователям, посещающим страницу. Вместо этого они работают за кулисами, встраиваясь в HTML страницы, обычно в элемент ``. Эта скрытая информация очень важна для поисковых систем и других систем, которым необходимо лучше понять содержание вашей веб-страницы. + +## Почему метаданные важны? + +Метаданные играют важную роль в улучшении SEO-страницы, делая ее более доступной и понятной для поисковых систем и платформ социальных сетей. Правильные метаданные помогают поисковым системам эффективно индексировать веб-страницы, повышая их рейтинг в результатах поиска. Кроме того, такие метаданные, как Open Graph, улучшают внешний вид общих ссылок в социальных сетях, делая контент более привлекательным и информативным для пользователей. + +## Типы метаданных + +Существуют различные типы метаданных, каждый из которых служит уникальной цели. Некоторые распространенные типы включают: + +**Метаданные заголовка**: Отвечают за заголовок веб-страницы, который отображается на вкладке браузера. Это очень важно для SEO, так как помогает поисковым системам понять, о чем идет речь на веб-странице. + +```html +Page Title +``` + +**Метаданные Описание**: Эти метаданные представляют собой краткий обзор содержимого веб-страницы и часто отображаются в результатах поисковых систем. + +```html + +``` + +**Метаданные ключевых слов**: Эти метаданные включают ключевые слова, связанные с содержанием веб-страницы, что помогает поисковым системам индексировать страницу. + +```html + +``` + +**Метаданные Open Graph**: Эти метаданные улучшают представление веб-страницы, когда ею делятся в социальных сетях, предоставляя такую информацию, как заголовок, описание и изображение для предварительного просмотра. + +```html + + + +``` + +**Метаданные фавикона**: Эти метаданные связывают фавикон (маленький значок) с веб-страницей, отображаемой в адресной строке браузера или на вкладке. + +```html + +``` + +## Добавление метаданных + +Next.js имеет API метаданных, который можно использовать для определения метаданных вашего приложения. Существует два способа добавления метаданных в приложение: + +- **На основе конфигурации**: Экспорт [статического объекта `metadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadata-object) или динамической функции [`generateMetadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#generatemetadata-function) в файл `layout.js` или `page.js`. +- **Файл-ориентированные**: В Next.js есть ряд специальных файлов, которые специально используются для метаданных: + - `favicon.ico`, `apple-icon.jpg` и `icon.jpg`: Используются для фавиконов и иконок + - `opengraph-image.jpg` и `twitter-image.jpg`: Используются для изображений социальных сетей + - `robots.txt`: Предоставляет инструкции для поисковых систем + - `itemap.xml`: Предоставляет информацию о структуре сайта. + +Вы можете использовать эти файлы для статических метаданных, а можете генерировать их программно в рамках вашего проекта. + +В обоих случаях Next.js будет автоматически генерировать соответствующие элементы `` для ваших страниц. + +### Фавикон и изображение Open Graph + +В папке `/public` вы заметите два изображения: `favicon.ico` и `opengraph-image.jpg`. + +Переместите эти изображения в корень папки `/app`. + +После этого Next.js автоматически определит и будет использовать эти файлы в качестве фавикона и изображения OG. Вы можете убедиться в этом, проверив элемент `` вашего приложения в dev tools. + +!!!tip "Полезно знать" + + Вы также можете создавать динамические изображения OG с помощью конструктора [`ImageResponse`](https://nextjs.org/docs/app/api-reference/functions/image-response). + +### Заголовок и описание страницы + +Вы также можете включить объект [`metadata`](https://nextjs.org/docs/app/api-reference/functions/generate-metadata#metadata-fields) из любого файла `layout.js` или `page.js`, чтобы добавить дополнительную информацию о странице, например, заголовок и описание. Любые метаданные в файле `layout.js` будут унаследованы всеми страницами, которые его используют. + +В корневом макете создайте новый объект `metadata` со следующими полями: + +```ts title="/app/layout.tsx" hl_lines="1 3-10" +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Acme Dashboard', + description: + 'The official Next.js Course Dashboard, built with App Router.', + metadataBase: new URL( + 'https://next-learn-dashboard.vercel.sh' + ), +}; + +export default function RootLayout() { + // ... +} +``` + +Next.js автоматически добавит заголовок и метаданные в ваше приложение. + +Но что, если вы хотите добавить собственный заголовок для конкретной страницы? Вы можете сделать это, добавив объект `metadata` к самой странице. Метаданные во вложенных страницах будут переопределять метаданные в родительской. + +Например, на странице `/dashboard/invoices` можно обновить заголовок страницы: + +```ts title="/app/dashboard/invoices/page.tsx" hl_lines="1 3-5" +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: 'Invoices | Acme Dashboard', +}; +``` + +Это работает, но мы повторяем название приложения на каждой странице. Если что-то изменится, например название компании, вам придется обновлять его на каждой странице. + +Вместо этого вы можете использовать поле `title.template` в объекте `metadata`, чтобы определить шаблон для заголовков ваших страниц. Этот шаблон может включать в себя заголовок страницы и любую другую информацию, которую вы хотите включить. + +В корневом макете обновите объект `metadata`, чтобы включить в него шаблон: + +```ts title="/app/layout.tsx" hl_lines="1 3-13" +import { Metadata } from 'next'; + +export const metadata: Metadata = { + title: { + template: '%s | Acme Dashboard', + default: 'Acme Dashboard', + }, + description: + 'The official Next.js Learn Dashboard built with App Router.', + metadataBase: new URL( + 'https://next-learn-dashboard.vercel.sh' + ), +}; +``` + +`%s` в шаблоне будет заменен на конкретный заголовок страницы. + +Теперь на странице `/dashboard/invoices` вы можете добавить заголовок страницы: + +```ts title="/app/dashboard/invoices/page.tsx" +export const metadata: Metadata = { + title: 'Invoices', +}; +``` + +Перейдите на страницу `/dashboard/invoices` и проверьте элемент ``. Вы должны увидеть, что теперь заголовок страницы стал `Invoices | Acme Dashboard`. + +## Практика: Добавление метаданных + +Теперь, когда вы узнали о метаданных, попрактикуйтесь в добавлении заголовков на другие страницы: + +1. `/login` страницу. +2. `/dashboard/` страницу. +3. `/dashboard/customers` страницу. +4. `/dashboard/invoices/create` страницу. +5. `/dashboard/invoices/[id]/edit` страницу. + +Next.js Metadata API является мощным и гибким, предоставляя вам полный контроль над метаданными вашего приложения. Здесь мы показали, как добавить некоторые базовые метаданные, но вы можете добавить множество полей, включая `keywords`, `robots`, `canonical` и другие. Не стесняйтесь изучать [документацию](https://nextjs.org/docs/app/api-reference/functions/generate-metadata) и добавлять любые дополнительные метаданные, которые вы хотите добавить в свое приложение. + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/adding-search-and-pagination.md b/docs/libs/nextjs/app-router/adding-search-and-pagination.md new file mode 100644 index 0000000..d67c868 --- /dev/null +++ b/docs/libs/nextjs/app-router/adding-search-and-pagination.md @@ -0,0 +1,619 @@ +--- +description: В предыдущей главе вы улучшили производительность начальной загрузки дашборда с помощью потоковой передачи данных. Теперь давайте перейдем к странице invoices и узнаем, как добавить поиск и пагинацию +--- + +# Поиска и пагинации + +В предыдущей главе вы улучшили производительность начальной загрузки дашборда с помощью потоковой передачи данных. Теперь давайте перейдем к странице `/invoices` и узнаем, как добавить поиск и пагинацию. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Научитесь использовать API Next.js: `useSearchParams`, `usePathname` и `useRouter`. + - Внедрите поиск и пагинацию с помощью параметров поиска URL. + +## Начальный код + +Внутри файла `/dashboard/invoices/page.tsx` вставьте следующий код: + +```ts title="/app/dashboard/invoices/page.tsx" +import Pagination from '@/app/ui/invoices/pagination'; +import Search from '@/app/ui/search'; +import Table from '@/app/ui/invoices/table'; +import { CreateInvoice } from '@/app/ui/invoices/buttons'; +import { lusitana } from '@/app/ui/fonts'; +import { InvoicesTableSkeleton } from '@/app/ui/skeletons'; +import { Suspense } from 'react'; + +export default async function Page() { + return ( +
+
+

+ Invoices +

+
+
+ + +
+ {/* }> + + */} +
+ {/* */} +
+ + ); +} +``` + +Потратьте некоторое время на ознакомление со страницей и компонентами, с которыми вам предстоит работать: + +- `` позволяет пользователям искать конкретные счета-фактуры. +- `` позволяет пользователям перемещаться между страницами счетов-фактур. +- `
` отображает счета-фактуры. + +Функциональность поиска будет охватывать клиент и сервер. Когда пользователь ищет счет-фактуру на клиенте, параметры URL обновляются, данные извлекаются на сервере, и таблица перерисовывается на сервере с новыми данными. + +## Зачем использовать параметры поиска по URL? + +Как было сказано выше, вы будете использовать параметры поиска URL для управления состоянием поиска. Этот паттерн может быть новым, если вы привыкли делать это с состоянием на стороне клиента. + +Есть несколько преимуществ реализации поиска с помощью параметров URL: + +- **URL, доступные для закладок и совместного использования**: Поскольку параметры поиска находятся в URL, пользователи могут сохранять текущее состояние приложения, включая поисковые запросы и фильтры, в закладках для последующего использования или обмена. +- **Серверный рендеринг**: Параметры URL можно напрямую использовать на сервере для рендеринга начального состояния, что упрощает работу с серверным рендерингом. +- **Аналитика и отслеживание**: Наличие поисковых запросов и фильтров непосредственно в URL упрощает отслеживание поведения пользователей, не требуя дополнительной логики на стороне клиента. + +## Добавление функциональности поиска + +Вот клиентские хуки Next.js, которые вы будете использовать для реализации функциональности поиска: + +- **`useSearchParams`**- Позволяет получить доступ к параметрам текущего URL. Например, параметры поиска для этого URL `/dashboard/invoices?page=1&query=pending` будут выглядеть следующим образом: `{page: '1', query: 'pending'}`. +- **`usePathname`** - Позволяет прочитать имя пути текущего URL. Например, для маршрута `/dashboard/invoices`, `usePathname` вернет `'/dashboard/invoices'`. +- **`useRouter`** - Обеспечивает навигацию между маршрутами внутри клиентских компонентов программным способом. Существует [несколько методов](https://nextjs.org/docs/app/api-reference/functions/use-router#userouter), которые вы можете использовать. + +Вот краткий обзор шагов реализации: + +1. Перехватить ввод пользователя. +2. Обновление URL с параметрами поиска. +3. Поддерживайте синхронизацию URL с полем ввода. +4. Обновите таблицу, чтобы отразить поисковый запрос. + +### 1. Перехват пользовательского ввода + +Перейдите в компонент `` (`/app/ui/search.tsx`), и вы заметите: + +- `"use client"` - Это клиентский компонент, что означает, что вы можете использовать обработчики событий и хуки. +- `` - поле ввода. + +Создайте новую функцию `handleSearch` и добавьте обработчик `onChange` к элементу ``. `onChange` будет вызывать `handleSearch` при каждом изменении значения ввода. + +```ts title="/app/ui/search.tsx" hl_lines="10-12 22-24" +'use client'; + +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; + +export default function Search({ + placeholder, +}: { + placeholder: string; +}) { + function handleSearch(term: string) { + console.log(term); + } + + return ( +
+ + { + handleSearch(e.target.value); + }} + /> + +
+ ); +} +``` + +Убедитесь, что все работает правильно, открыв консоль в инструментах разработчика браузера, а затем наберите в поле поиска. Вы должны увидеть, как поисковый запрос записывается в консоль браузера. + +Отлично! Вы перехватили поисковый ввод пользователя. Теперь вам нужно обновить URL, добавив в него поисковый запрос. + +### 2. Обновление URL с параметрами поиска + +Импортируйте хук `useSearchParams` из `next/navigation` и присвойте его переменной: + +```ts title="/app/ui/search.tsx" hl_lines="4 7" +'use client'; + +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { useSearchParams } from 'next/navigation'; + +export default function Search() { + const searchParams = useSearchParams(); + + function handleSearch(term: string) { + console.log(term); + } + // ... +} +``` + +Внутри `handleSearch` создайте новый экземпляр [`URLSearchParams`](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams), используя новую переменную `searchParams`. + +```ts title="/app/ui/search.tsx" hl_lines="10" +'use client'; + +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { useSearchParams } from 'next/navigation'; + +export default function Search() { + const searchParams = useSearchParams(); + + function handleSearch(term: string) { + const params = new URLSearchParams(searchParams); + } + // ... +} +``` + +`URLSearchParams` - это веб-интерфейс, предоставляющий методы для манипулирования параметрами запроса URL. Вместо того чтобы создавать сложный строковый литерал, вы можете использовать его для получения строки params типа `?page=1&query=a`. + +Затем `set` строку params, основанную на вводе данных пользователем. Если введенное значение пустое, его нужно `delete`: + +```ts title="/app/ui/search.tsx" hl_lines="11-15" +'use client'; + +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { useSearchParams } from 'next/navigation'; + +export default function Search() { + const searchParams = useSearchParams(); + + function handleSearch(term: string) { + const params = new URLSearchParams(searchParams); + if (term) { + params.set('query', term); + } else { + params.delete('query'); + } + } + // ... +} +``` + +Теперь у вас есть строка запроса. Вы можете использовать хуки Next.js `useRouter` и `usePathname` для обновления URL. + +Импортируйте `useRouter` и `usePathname` из `'next/navigation'` и используйте метод `replace` из `useRouter()` внутри `handleSearch`: + +```ts title="/app/ui/search.tsx" hl_lines="4-8 12-13 22" +'use client'; + +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { + useSearchParams, + usePathname, + useRouter, +} from 'next/navigation'; + +export default function Search() { + const searchParams = useSearchParams(); + const pathname = usePathname(); + const { replace } = useRouter(); + + function handleSearch(term: string) { + const params = new URLSearchParams(searchParams); + if (term) { + params.set('query', term); + } else { + params.delete('query'); + } + replace(`${pathname}?${params.toString()}`); + } +} +``` + +Вот краткое описание происходящего: + +- `${pathname}` - это текущий путь, в вашем случае `"/dashboard/invoices"`. +- Когда пользователь набирает текст в строке поиска, `params.toString()` преобразует его в формат, удобный для URL. +- `replace(${pathname}?${params.toString()})` обновляет URL с данными поиска пользователя. Например, `/dashboard/invoices?query=lee`, если пользователь ищет «Lee». +- URL обновляется без перезагрузки страницы, благодаря навигации на стороне клиента Next.js, о которой вы узнали в главе [навигация между страницами](navigating-between-pages.md). + +### 3. Обеспечение синхронизации URL и поля ввода + +Чтобы убедиться, что поле ввода синхронизировано с URL и будет заполнено при совместном использовании, вы можете передать `defaultValue` в `input`, читая из `searchParams`: + +```ts title="/app/ui/search.tsx" hl_lines="7" + { + handleSearch(e.target.value); + }} + defaultValue={searchParams.get('query')?.toString()} +/> +``` + +!!!info "`defaultValue` vs. `value` / Контролируемые vs. Неконтролируемых" + + Если вы используете состояние для управления значением ввода, вы используете атрибут `value`, чтобы сделать его управляемым компонентом. Это означает, что React будет управлять состоянием ввода. + + Однако, поскольку вы не используете состояние, вы можете использовать `defaultValue`. Это означает, что нативный ввод будет управлять своим собственным состоянием. Это нормально, поскольку вы сохраняете поисковый запрос в URL вместо состояния. + +### 4. Обновление таблицы + +Наконец, вам нужно обновить компонент таблицы, чтобы отразить поисковый запрос. + +Перейдите обратно на страницу счетов-фактур. + +Компоненты страницы [принимают параметр `searchParams`](https://nextjs.org/docs/app/api-reference/file-conventions/page), поэтому вы можете передать текущие URL-параметры компоненту `
`. + +```ts title="/app/dashboard/invoices/page.tsx" hl_lines="9-17 32-40" +import Pagination from '@/app/ui/invoices/pagination'; +import Search from '@/app/ui/search'; +import Table from '@/app/ui/invoices/table'; +import { CreateInvoice } from '@/app/ui/invoices/buttons'; +import { lusitana } from '@/app/ui/fonts'; +import { Suspense } from 'react'; +import { InvoicesTableSkeleton } from '@/app/ui/skeletons'; + +export default async function Page(props: { + searchParams?: Promise<{ + query?: string; + page?: string; + }>; +}) { + const searchParams = await props.searchParams; + const query = searchParams?.query || ''; + const currentPage = Number(searchParams?.page) || 1; + + return ( +
+
+

+ Invoices +

+
+
+ + +
+ } + > +
+ +
+ {/* */} +
+ + ); +} +``` + +Если вы перейдете к компоненту `
`, то увидите, что два параметра, `query` и `currentPage`, передаются функции `fetchFilteredInvoices()`, которая возвращает счета-фактуры, соответствующие запросу. + +```ts title="/app/ui/invoices/table.tsx" +// ... +export default async function InvoicesTable({ + query, + currentPage, +}: { + query: string; + currentPage: number; +}) { + const invoices = await fetchFilteredInvoices( + query, + currentPage + ); + // ... +} +``` + +Внеся эти изменения, приступайте к тестированию. При поиске термина вы обновите URL-адрес, который отправит новый запрос на сервер, данные будут получены на сервере, и будут возвращены только те счета, которые соответствуют вашему запросу. + +!!!note "Когда следует использовать хук `useSearchParams()`, а не свойство `searchParams`?" + + Вы могли заметить, что для извлечения параметров поиска используются два разных способа. Использование того или иного способа зависит от того, где вы работаете - на клиенте или на сервере. + + - `` - это клиентский компонент, поэтому вы использовали хук `useSearchParams()` для доступа к параметрам с клиента. + - `
` - это серверный компонент, который получает свои собственные данные, поэтому вы можете передавать свойство `searchParams` со страницы в компонент. + + Как правило, если вы хотите читать параметры с клиента, используйте хук `useSearchParams()`, так как это избавит вас от необходимости возвращаться на сервер. + +### Лучшие практики: debouncing + +Поздравляем! Вы реализовали поиск в Next.js! Но есть кое-что, что можно сделать для его оптимизации. + +Внутри функции `handleSearch` добавьте следующий `console.log`: + +```ts title="/app/ui/search.tsx" hl_lines="2" +function handleSearch(term: string) { + console.log(`Searching... ${term}`); + + const params = new URLSearchParams(searchParams); + if (term) { + params.set('query', term); + } else { + params.delete('query'); + } + replace(`${pathname}?${params.toString()}`); +} +``` + +Затем введите «Delba» в строку поиска и проверьте консоль в dev tools. Что происходит? + +```sh title="Dev Tools Console" +Searching... D +Searching... De +Searching... Del +Searching... Delb +Searching... Delba +``` + +Вы обновляете URL при каждом нажатии клавиши, а значит, запрашиваете базу данных при каждом нажатии! Это не проблема, поскольку наше приложение небольшое, но представьте, если бы в вашем приложении были тысячи пользователей, каждый из которых отправлял бы новый запрос в вашу базу данных при каждом нажатии клавиши. + +**Дебаунсинг** - это практика программирования, которая ограничивает скорость выполнения функции. В нашем случае вы хотите запрашивать базу данных только тогда, когда пользователь перестал набирать текст. + +!!!note "Как работает дебаунсинг:" + + - **Триггерное событие**: При наступлении события, которое должно быть отменено (например, нажатие клавиши в строке поиска), запускается таймер. + - **Ожидание**: Если до истечения срока действия таймера произойдет новое событие, таймер будет сброшен. + - **Исполнение**: Если таймер достигает конца обратного отсчета, выполняется функция дебаунсинга. + +Вы можете реализовать дебаггинг несколькими способами, в том числе вручную создать собственную функцию дебаггинга. Для простоты мы будем использовать библиотеку под названием [use-debounce](https://www.npmjs.com/package/use-debounce). + +Установите `use-debounce`: + +```sh +pnpm i use-debounce +``` + +В компоненте `` импортируйте функцию `useDebouncedCallback`: + +```ts title="/app/ui/search.tsx" hl_lines="2 5 15" +// ... +import { useDebouncedCallback } from 'use-debounce'; + +// Inside the Search Component... +const handleSearch = useDebouncedCallback((term) => { + console.log(`Searching... ${term}`); + + const params = new URLSearchParams(searchParams); + if (term) { + params.set('query', term); + } else { + params.delete('query'); + } + replace(`${pathname}?${params.toString()}`); +}, 300); +``` + +Эта функция будет оборачивать содержимое `handleSearch` и запускать код только через определенное время после того, как пользователь перестанет набирать текст (300 мс). + +Теперь снова введите текст в строку поиска и откройте консоль в dev tools. Вы должны увидеть следующее: + +```sh title="Dev Tools Console" +Searching... Delba +``` + +Дебаунсинг позволяет сократить количество запросов к базе данных и тем самым сэкономить ресурсы. + +## Добавление пагинации + +После внедрения функции поиска вы заметите, что в таблице отображается только 6 счетов за раз. Это связано с тем, что функция `fetchFilteredInvoices()` в `data.ts` возвращает максимум 6 счетов на страницу. + +Добавление пагинации позволяет пользователям перемещаться по различным страницам, чтобы просмотреть все счета. Давайте посмотрим, как можно реализовать пагинацию с помощью параметров URL, как это было сделано с поиском. + +Перейдите к компоненту ``, и вы заметите, что это клиентский компонент. Вы не хотите получать данные на клиенте, так как это приведет к раскрытию секретов вашей базы данных (помните, что вы не используете слой API). Вместо этого вы можете получить данные на сервере и передать их компоненту в качестве параметра. + +В файле `/dashboard/invoices/page.tsx` импортируйте новую функцию `fetchInvoicesPages` и передайте в качестве аргумента `query` из `searchParams`: + +```ts title="/app/dashboard/invoices/page.tsx" hl_lines="2 13" +// ... +import { fetchInvoicesPages } from '@/app/lib/data'; + +export default async function Page(props: { + searchParams?: Promise<{ + query?: string; + page?: string; + }>; +}) { + const searchParams = await props.searchParams; + const query = searchParams?.query || ''; + const currentPage = Number(searchParams?.page) || 1; + const totalPages = await fetchInvoicesPages(query); + + return ( + // ... + ); +} +``` + +Функция `fetchInvoicesPages` возвращает общее количество страниц по поисковому запросу. Например, если есть 12 счетов-фактур, соответствующих поисковому запросу, и на каждой странице отображается 6 счетов-фактур, то общее количество страниц будет равно 2. + +Далее передайте параметр `totalPages` компоненту ``: + +```ts title="/app/dashboard/invoices/page.tsx" hl_lines="37" +// ... + +export default async function Page(props: { + searchParams?: Promise<{ + query?: string; + page?: string; + }>; +}) { + const searchParams = await props.searchParams; + const query = searchParams?.query || ''; + const currentPage = Number(searchParams?.page) || 1; + const totalPages = await fetchInvoicesPages(query); + + return ( +
+
+

+ Invoices +

+
+
+ + +
+ } + > +
+ +
+ +
+ + ); +} +``` + +Перейдите к компоненту `` и импортируйте хуки `usePathname` и `useSearchParams`. Мы будем использовать их для получения текущей страницы и установки новой страницы. Не забудьте также откомментировать код в этом компоненте. Ваше приложение временно сломается, так как вы еще не реализовали логику ``. Давайте сделаем это сейчас! + +```ts title="/app/ui/invoices/pagination.tsx" hl_lines="10-13 20-23" +'use client'; + +import { + ArrowLeftIcon, + ArrowRightIcon, +} from '@heroicons/react/24/outline'; +import clsx from 'clsx'; +import Link from 'next/link'; +import { generatePagination } from '@/app/lib/utils'; +import { + usePathname, + useSearchParams, +} from 'next/navigation'; + +export default function Pagination({ + totalPages, +}: { + totalPages: number; +}) { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const currentPage = + Number(searchParams.get('page')) || 1; + + // ... +} +``` + +Далее создайте новую функцию внутри компонента `` под названием `createPageURL`. Аналогично поиску, вы будете использовать `URLSearchParams` для задания номера новой страницы и `pathName` для создания строки URL. + +```ts title="/app/ui/invoices/pagination.tsx" hl_lines="25-29" +'use client'; + +import { + ArrowLeftIcon, + ArrowRightIcon, +} from '@heroicons/react/24/outline'; +import clsx from 'clsx'; +import Link from 'next/link'; +import { generatePagination } from '@/app/lib/utils'; +import { + usePathname, + useSearchParams, +} from 'next/navigation'; + +export default function Pagination({ + totalPages, +}: { + totalPages: number; +}) { + const pathname = usePathname(); + const searchParams = useSearchParams(); + const currentPage = + Number(searchParams.get('page')) || 1; + + const createPageURL = (pageNumber: number | string) => { + const params = new URLSearchParams(searchParams); + params.set('page', pageNumber.toString()); + return `${pathname}?${params.toString()}`; + }; + + // ... +} +``` + +Вот краткое описание происходящего: + +- `createPageURL` создает экземпляр текущих параметров поиска. +- Затем он обновляет параметр `page` до указанного номера страницы. +- Наконец, он создает полный URL, используя имя пути и обновленные параметры поиска. + +Остальная часть компонента `` занимается стилизацией и различными состояниями (первое, последнее, активное, отключенное и т. д.). Мы не будем вдаваться в подробности в рамках данного курса, но не стесняйтесь просмотреть код, чтобы увидеть, где вызывается `createPageURL`. + +Наконец, когда пользователь набирает новый поисковый запрос, вы хотите сбросить номер страницы на `1`. Вы можете сделать это, обновив функцию `handleSearch` в компоненте ``: + +```ts title="/app/ui/search.tsx" hl_lines="22" +'use client'; + +import { MagnifyingGlassIcon } from '@heroicons/react/24/outline'; +import { + usePathname, + useRouter, + useSearchParams, +} from 'next/navigation'; +import { useDebouncedCallback } from 'use-debounce'; + +export default function Search({ + placeholder, +}: { + placeholder: string; +}) { + const searchParams = useSearchParams(); + const { replace } = useRouter(); + const pathname = usePathname(); + + const handleSearch = useDebouncedCallback((term) => { + const params = new URLSearchParams(searchParams); + params.set('page', '1'); + if (term) { + params.set('query', term); + } else { + params.delete('query'); + } + replace(`${pathname}?${params.toString()}`); + }, 300); +} +``` + +## Резюме + +Поздравляем! Вы только что реализовали поиск и пагинацию с помощью параметров поиска URL и API Next.js. + +Подводя итог, можно сказать, что в этой главе + +- Вы реализовали поиск и пагинацию с помощью параметров поиска URL вместо состояния клиента. +- Вы получали данные на сервере. +- Вы используете крючок маршрутизатора `useRouter` для более плавных переходов на стороне клиента. + +Эти паттерны отличаются от тех, к которым вы привыкли при работе с React на стороне клиента, но, надеемся, теперь вы лучше понимаете преимущества использования параметров поиска по URL и передачи этого состояния на сервер. + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/complete-dashboard.png b/docs/libs/nextjs/app-router/complete-dashboard.png new file mode 100644 index 0000000..d264cba Binary files /dev/null and b/docs/libs/nextjs/app-router/complete-dashboard.png differ diff --git a/docs/libs/nextjs/app-router/configure-project.png b/docs/libs/nextjs/app-router/configure-project.png new file mode 100644 index 0000000..01f883f Binary files /dev/null and b/docs/libs/nextjs/app-router/configure-project.png differ diff --git a/docs/libs/nextjs/app-router/create-database.png b/docs/libs/nextjs/app-router/create-database.png new file mode 100644 index 0000000..7fabce7 Binary files /dev/null and b/docs/libs/nextjs/app-router/create-database.png differ diff --git a/docs/libs/nextjs/app-router/create-invoice-page.png b/docs/libs/nextjs/app-router/create-invoice-page.png new file mode 100644 index 0000000..5c9dcac Binary files /dev/null and b/docs/libs/nextjs/app-router/create-invoice-page.png differ diff --git a/docs/libs/nextjs/app-router/create-invoice-route.png b/docs/libs/nextjs/app-router/create-invoice-route.png new file mode 100644 index 0000000..dcd4a27 Binary files /dev/null and b/docs/libs/nextjs/app-router/create-invoice-route.png differ diff --git a/docs/libs/nextjs/app-router/creating-layouts-and-pages.md b/docs/libs/nextjs/app-router/creating-layouts-and-pages.md new file mode 100644 index 0000000..b92f16e --- /dev/null +++ b/docs/libs/nextjs/app-router/creating-layouts-and-pages.md @@ -0,0 +1,151 @@ +--- +description: Пока что в вашем приложении есть только главная страница. Давайте узнаем, как можно создать больше маршрутов с помощью макетов и страниц. +--- + +# Создание макетов и страниц + +Пока что в вашем приложении есть только главная страница. Давайте узнаем, как можно создать больше маршрутов с помощью макетов и страниц. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Создание маршрутов дашборда с использованием маршрутизации файловой системы. + - Поймите роль папок и файлов при создании новых сегментов маршрута. + - Создайте вложенный макет, который можно использовать совместно для нескольких страниц дашборда. + - Поймите, что такое размещение, частичный рендеринг и корневой макет. + +## Вложенная маршрутизация + +Next.js использует маршрутизацию с помощью файловой системы, где папки используются для создания вложенных маршрутов. Каждая папка представляет собой сегмент маршрута, который сопоставляется с сегментом URL. + +![Диаграмма, показывающая, как папки сопоставляются с сегментами URL](folders-to-url-segments.png) + +Вы можете создавать отдельные пользовательские интерфейсы для каждого маршрута с помощью файлов `layout.tsx` и `page.tsx`. + +`page.tsx` - это специальный файл Next.js, который экспортирует компонент React, и он необходим для того, чтобы маршрут был доступен. В вашем приложении уже есть файл страницы: `/app/page.tsx` - это главная страница, связанная с маршрутом `/`. + +Чтобы создать вложенный маршрут, вы можете вложить папки друг в друга и добавить в них файлы `page.tsx`. Например: + +![Диаграмма, показывающая, как добавление папки с названием dashboard создает новый маршрут '/dashboard'](dashboard-route.png) + +`/app/dashboard/page.tsx` ассоциируется с путем `/dashboard`. Давайте создадим страницу, чтобы посмотреть, как она работает! + +## Создание страницы дашборда + +Создайте новую папку `dashboard` внутри `/app`. Затем создайте новый файл `page.tsx` в папке `dashboard` со следующим содержимым: + +```ts title="/app/dashboard/page.tsx" +export default function Page() { + return

Dashboard Page

; +} +``` + +Теперь убедитесь, что сервер разработки запущен, и посетите . Вы должны увидеть текст «Страница дашборда». + +Вот как можно создавать различные страницы в Next.js: создайте новый сегмент маршрута, используя папку, и добавьте в него файл `page`. + +Благодаря специальному названию для файлов `page` Next.js позволяет [размещать](https://nextjs.org/docs/app/building-your-application/routing#colocation) компоненты пользовательского интерфейса, тестовые файлы и другой связанный код вместе с маршрутами. Только содержимое внутри файла `page` будет общедоступным. Например, папки `/ui` и `/lib` _размещаются_ внутри папки `/app` вместе с маршрутами. + +## Практика: Создание страниц дашборда + +Давайте попрактикуемся в создании дополнительных маршрутов. В вашем дашборде создайте еще две страницы: + +1. Страница клиентов: Страница должна быть доступна по адресу . Пока что она должна возвращать элемент `

Customers Page

`. +2. Страница счетов: Страница счетов должна быть доступна по адресу . Пока что она также должна возвращать элемент `

Invoices Page

`. + +Потратьте некоторое время на выполнение этого упражнения, а когда будете готовы, разверните тумблер ниже для получения решения: + +???info "Откройте решение" + + У вас должна быть следующая структура папок: + + ![Диаграмма, показывающая, как добавление папки с именем login создает новый маршрут '/login'](routing-solution.png) + + Страница клиентов: + + ```ts title="/app/dashboard/customers/page.tsx" + export default function Page() { + return

Customers Page

; + } + ``` + + Страница «Счета-фактуры»: + + ```ts title="/app/dashboard/invoices/page.tsx" + export default function Page() { + return

Invoices Page

; + } + ``` + +## Создание макета дашборда + +Дашборды имеют некую навигацию, которая используется на нескольких страницах. В Next.js вы можете использовать специальный файл `layout.tsx` для создания пользовательского интерфейса, разделяемого между несколькими страницами. Давайте создадим макет для страниц дашборда! + +В папке `/dashboard` добавьте новый файл `layout.tsx` и вставьте в него следующий код: + +```ts title="/app/dashboard/layout.tsx" +import SideNav from '@/app/ui/dashboard/sidenav'; + +export default function Layout({ + children, +}: { + children: React.ReactNode; +}) { + return ( +
+
+ +
+
+ {children} +
+
+ ); +} +``` + +В этом коде происходит несколько вещей, поэтому давайте разберем их по порядку: + +Во-первых, вы импортируете компонент `` в ваш макет. Все компоненты, которые вы импортируете в этот файл, будут частью макета. + +Компонент `` получает свойство `children`. Этим дочерним компонентом может быть либо страница, либо другой макет. В вашем случае страницы внутри `/dashboard` будут автоматически вложены в `` следующим образом: + +![Структура папки с макетом дашборда, в котором страницы дашборда вложены как дочерние](shared-layout.png) + +Проверьте, что все работает правильно, сохранив изменения и проверив локальный хост. Вы должны увидеть следующее: + +![Страница дашборда с сайднавом и областью основного контента](shared-layout-page.png) + +Одним из преимуществ использования макетов в Next.js является то, что при навигации обновляются только компоненты страницы, а макет не перерисовывается. Это называется [частичным рендерингом](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#4-partial-rendering), который сохраняет состояние React на стороне клиента в макете при переходе между страницами. + +![Структура папки, показывающая макет дашборда, в котором вложены страницы дашборда, но при навигации меняются только страницы пользовательского интерфейса](partial-rendering-dashboard.png) + +## Корневой макет + +В главе 3 вы импортировали шрифт `Inter` в другой макет: `/app/layout.tsx`. Напоминаем: + +```ts title="/app/layout.tsx" +import '@/app/ui/global.css'; +import { inter } from '@/app/ui/fonts'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} +``` + +Это называется [корневой макет](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts) и требуется в каждом приложении Next.js. Любой пользовательский интерфейс, который вы добавите в корневой макет, будет общим для **всех** страниц вашего приложения. Вы можете использовать корневой макет для изменения тегов `` и ``, а также для добавления метаданных (подробнее о метаданных вы узнаете в [следующей главе](adding-metadata.md)). + +Поскольку новый макет, который вы только что создали (`/app/dashboard/layout.tsx`), уникален для страниц дашборда, вам не нужно добавлять какой-либо пользовательский интерфейс в корневой макет выше. + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/css-styling.md b/docs/libs/nextjs/app-router/css-styling.md new file mode 100644 index 0000000..34efb7c --- /dev/null +++ b/docs/libs/nextjs/app-router/css-styling.md @@ -0,0 +1,184 @@ +--- +description: В настоящее время у вашей домашней страницы нет никаких стилей. Давайте рассмотрим различные способы стилизации вашего приложения Next.js. +--- + +# Стилизация CSS + +В настоящее время у вашей домашней страницы нет никаких стилей. Давайте рассмотрим различные способы стилизации вашего приложения Next.js. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Как добавить глобальный CSS-файл в ваше приложение. + - Два разных способа стилизации: Tailwind и CSS-модули. + - Как условно добавлять имена классов с помощью пакета утилит `clsx`. + +## Глобальные стили + +Если вы заглянете в папку `/app/ui`, то увидите файл под названием `global.css`. Вы можете использовать этот файл для добавления CSS-правил ко **всем** маршрутам в вашем приложении - например, правила сброса CSS, общие для сайта стили для HTML-элементов, таких как ссылки, и многое другое. + +Вы можете импортировать `global.css` в любой компонент вашего приложения, но обычно рекомендуется добавлять его в компонент верхнего уровня. В Next.js это [корневой макет](https://nextjs.org/docs/app/api-reference/file-conventions/layout#root-layouts) (подробнее об этом позже). + +Добавьте глобальные стили в приложение, перейдя в `/app/layout.tsx` и импортировав файл `global.css`: + +```ts title="/app/layout.tsx" hl_lines="1" +import '@/app/ui/global.css'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + {children} + + ); +} +``` + +Если сервер разработки все еще работает, сохраните изменения и просмотрите их в браузере. Теперь ваша домашняя страница должна выглядеть следующим образом: + +![Стилизованная страница с логотипом 'Acme', описанием и ссылкой для входа](home-page-with-tailwind.png) + +Но подождите секунду, вы же не добавили никаких CSS-правил, откуда взялись стили? + +Если вы заглянете в `global.css`, то заметите несколько директив `@tailwind`: + +```css title="/app/ui/global.css" +@tailwind base; +@tailwind components; +@tailwind utilities; +``` + +## Tailwind + +[Tailwind](https://tailwindcss.com/) - это CSS-фреймворк, который ускоряет процесс разработки, позволяя вам быстро писать [полезные классы](https://tailwindcss.com/docs/utility-first) прямо в коде React. + +В Tailwind вы придаете стиль элементам, добавляя имена классов. Например, добавив `"text-blue-500"`, вы сделаете текст `

` синим: + +```html +

I'm blue!

+``` + +Хотя стили CSS используются глобально, каждый класс применяется к каждому элементу отдельно. Это означает, что если вы добавляете или удаляете элемент, вам не нужно беспокоиться о поддержании отдельных таблиц стилей, коллизии стилей или о том, что размер вашего пакета CSS будет расти по мере расширения приложения. + +Когда вы используете `create-next-app` для начала нового проекта, Next.js спросит, хотите ли вы использовать Tailwind. Если вы выберете `да`, Next.js автоматически установит необходимые пакеты и настроит Tailwind в вашем приложении. + +Если вы посмотрите на `/app/page.tsx`, то увидите, что в примере мы используем классы Tailwind. + +```ts title="/app/page.tsx" +import AcmeLogo from '@/app/ui/acme-logo'; +import { ArrowRightIcon } from '@heroicons/react/24/outline'; +import Link from 'next/link'; + +export default function Page() { + return ( +
+ {/* These are Tailwind classes: */} +
+ {/* ... */} +
+
+ ); +} +``` + +Не волнуйтесь, если вы впервые используете Tailwind. Чтобы сэкономить время, мы уже стилизовали все компоненты, которые вы будете использовать. + +Давайте поиграем с Tailwind! Скопируйте приведенный ниже код и вставьте его над элементом `

` в файле `/app/page.tsx`: + +```html title="/app/page.tsx" +

+``` + +Если вы предпочитаете писать традиционные правила CSS или хранить стили отдельно от JSX - модули CSS являются отличной альтернативой. + +## CSS модули + +[CSS модули](https://nextjs.org/docs/app/getting-started/css#css-modules) позволяют привязать CSS к компоненту, автоматически создавая уникальные имена классов, так что вам не придется беспокоиться о коллизии стилей. + +Мы продолжим использовать Tailwind в этом курсе, но давайте посмотрим, как можно добиться тех же результатов, что и в приведенном выше тесте, используя модули CSS. + +Внутри `/app/ui` создайте новый файл `home.module.css` и добавьте в него следующие CSS-правила: + +```css title="/app/ui/home.module.css" +.shape { + height: 0; + width: 0; + border-bottom: 30px solid black; + border-left: 20px solid transparent; + border-right: 20px solid transparent; +} +``` + +Затем в файле `/app/page.tsx` импортируйте стили и замените имена классов Tailwind из `
`, которые вы добавили, на `styles.shape`: + +```ts title="/app/page.tsx" hl_lines="4 9" +import AcmeLogo from '@/app/ui/acme-logo'; +import { ArrowRightIcon } from '@heroicons/react/24/outline'; +import Link from 'next/link'; +import styles from '@/app/ui/home.module.css'; + +export default function Page() { + return ( +
+
+ {/* ... */} +
+ ); +} +``` + +Сохраните изменения и просмотрите их в браузере. Вы должны увидеть ту же форму, что и раньше. + +Tailwind и модули CSS - это два наиболее распространенных способа стилизации приложений Next.js. Использовать тот или иной способ - это вопрос предпочтений, вы даже можете использовать оба в одном приложении! + +## Использование библиотеки clsx для переключения имен классов + +Бывают случаи, когда необходимо условно стилизовать элемент, основываясь на состоянии или каком-то другом условии. + +[`clsx`](https://www.npmjs.com/package/clsx) - это библиотека, позволяющая легко переключать имена классов. Для получения более подробной информации мы рекомендуем заглянуть в [documentation](https://github.com/lukeed/clsx), но вот основные способы использования: + +Предположим, вы хотите создать компонент `InvoiceStatus`, который принимает `status`. Статус может быть `'pending'` или `'paid'`. +Если статус `'paid'`, то цвет должен быть зеленым. Если `'pending'`, то цвет должен быть серым. + +Вы можете использовать `clsx` для условного применения классов, например, так: + +```ts title="/app/ui/invoices/status.tsx" hl_lines="13-16" +import clsx from 'clsx'; + +export default function InvoiceStatus({ + status, +}: { + status: string; +}) { + return ( + + {/* ... */} + + ); +} +``` + +## Другие решения для стилизации + +В дополнение к рассмотренным подходам, вы также можете стилизовать свое приложение Next.js с помощью: + +- Sass, который позволяет импортировать файлы `.css` и `.scss`. +- Библиотеки CSS-in-JS, такие как [styled-jsx](https://github.com/vercel/styled-jsx), [styled-components](https://github.com/vercel/next.js/tree/canary/examples/with-styled-components) и [emotion](https://github.com/vercel/next.js/tree/canary/examples/with-emotion). + +Для получения дополнительной информации посмотрите [CSS documentation](https://nextjs.org/docs/app/building-your-application/styling). + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/dashboard-route.png b/docs/libs/nextjs/app-router/dashboard-route.png new file mode 100644 index 0000000..03bed24 Binary files /dev/null and b/docs/libs/nextjs/app-router/dashboard-route.png differ diff --git a/docs/libs/nextjs/app-router/database-dashboard.png b/docs/libs/nextjs/app-router/database-dashboard.png new file mode 100644 index 0000000..1cef70c Binary files /dev/null and b/docs/libs/nextjs/app-router/database-dashboard.png differ diff --git a/docs/libs/nextjs/app-router/database-region.png b/docs/libs/nextjs/app-router/database-region.png new file mode 100644 index 0000000..9e23727 Binary files /dev/null and b/docs/libs/nextjs/app-router/database-region.png differ diff --git a/docs/libs/nextjs/app-router/deployed-project.png b/docs/libs/nextjs/app-router/deployed-project.png new file mode 100644 index 0000000..dc00603 Binary files /dev/null and b/docs/libs/nextjs/app-router/deployed-project.png differ diff --git a/docs/libs/nextjs/app-router/edit-invoice-page.png b/docs/libs/nextjs/app-router/edit-invoice-page.png new file mode 100644 index 0000000..0acb686 Binary files /dev/null and b/docs/libs/nextjs/app-router/edit-invoice-page.png differ diff --git a/docs/libs/nextjs/app-router/edit-invoice-route.png b/docs/libs/nextjs/app-router/edit-invoice-route.png new file mode 100644 index 0000000..46619b4 Binary files /dev/null and b/docs/libs/nextjs/app-router/edit-invoice-route.png differ diff --git a/docs/libs/nextjs/app-router/error-handling.md b/docs/libs/nextjs/app-router/error-handling.md new file mode 100644 index 0000000..13091ea --- /dev/null +++ b/docs/libs/nextjs/app-router/error-handling.md @@ -0,0 +1,261 @@ +--- +description: В предыдущей главе вы узнали, как изменять данные с помощью Server Actions. Давайте посмотрим, как можно изящно обрабатывать ошибки, используя операторы try/catch JavaScript и API Next.js для не пойманных исключений. +--- + +# Обработка ошибок + +В предыдущей главе вы узнали, как изменять данные с помощью Server Actions. Давайте посмотрим, как можно обрабатывать ошибки _грациозно_, используя операторы JavaScript `try/catch` и API Next.js для не пойманных исключений. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Как с помощью специального файла `error.tsx` отлавливать ошибки в сегментах маршрута и показывать пользователю резервный UI. + - Как использовать функцию `notFound` и файл `not-found` для обработки 404 ошибки (для несуществующих ресурсов). + +## Добавление `try/catch` к действиям сервера + +Во-первых, давайте добавим операторы JavaScript `try/catch` в ваши действия сервера, чтобы вы могли изящно обрабатывать ошибки. + +Если вы знаете, как это сделать, потратьте несколько минут на обновление своих действий сервера, или вы можете скопировать приведенный ниже код: + +???note "Раскрыть решение" + + ```ts title="/app/lib/actions.ts" + export async function createInvoice(formData: FormData) { + const { + customerId, + amount, + status, + } = CreateInvoice.parse({ + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }); + + const amountInCents = amount * 100; + const date = new Date().toISOString().split('T')[0]; + + try { + await sql` + INSERT INTO invoices (customer_id, amount, status, date) + VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) + `; + } catch (error) { + // We'll log the error to the console for now + console.error(error); + } + + revalidatePath('/dashboard/invoices'); + redirect('/dashboard/invoices'); + } + ``` + +???note "Раскрыть решение" + + ```ts title="/app/lib/actions.ts" + export async function updateInvoice( + id: string, + formData: FormData + ) { + const { + customerId, + amount, + status, + } = UpdateInvoice.parse({ + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }); + + const amountInCents = amount * 100; + + try { + await sql` + UPDATE invoices + SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status} + WHERE id = ${id} + `; + } catch (error) { + // We'll log the error to the console for now + console.error(error); + } + + revalidatePath('/dashboard/invoices'); + redirect('/dashboard/invoices'); + } + ``` + +Обратите внимание, что `redirect` вызывается вне блока `try/catch`. Это потому, что `redirect` работает, выбрасывая ошибку, которая будет поймана блоком `catch`. Чтобы избежать этого, вы можете вызвать `redirect` **после** `try/catch`. `redirect` будет доступен только в случае успешного завершения `try`. + +Мы изящно справляемся с этими ошибками, отлавливая проблему с базой данных и возвращая полезное сообщение от нашего Server Action. + +Что произойдет, если в вашем действии возникнет не пойманное исключение? Мы можем смоделировать это, вручную выбросив ошибку. Например, в действии `deleteInvoice` бросьте ошибку в верхней части функции: + +```ts title="/app/lib/actions.ts" hl_lines="2" +export async function deleteInvoice(id: string) { + throw new Error('Failed to Delete Invoice'); + + // Unreachable code block + await sql`DELETE FROM invoices WHERE id = ${id}`; + revalidatePath('/dashboard/invoices'); +} +``` + +Когда вы пытаетесь удалить счет-фактуру, вы должны увидеть ошибку на localhost. При запуске в производство вы хотите более изящно показывать пользователю сообщение, когда происходит что-то непредвиденное. + +Здесь на помощь приходит файл Next.js [`error.tsx`](https://nextjs.org/docs/app/api-reference/file-conventions/error). Убедитесь, что вы удалили эту добавленную вручную ошибку после тестирования и перед переходом к следующему разделу. + +## Обработка всех ошибок с помощью `error.tsx` + +Файл `error.tsx` можно использовать для определения границ пользовательского интерфейса для сегмента маршрута. Он служит в качестве **catch-all** для непредвиденных ошибок и позволяет отображать пользователям резервный пользовательский интерфейс. + +В папке `/dashboard/invoices` создайте новый файл с именем `error.tsx` и вставьте в него следующий код: + +```ts title="/dashboard/invoices/error.tsx" +'use client'; + +import { useEffect } from 'react'; + +export default function Error({ + error, + reset, +}: { + error: Error & { digest?: string }; + reset: () => void; +}) { + useEffect(() => { + // Optionally log the error to an error reporting service + console.error(error); + }, [error]); + + return ( +
+

+ Something went wrong! +

+ +
+ ); +} +``` + +В приведенном выше коде вы заметите несколько вещей: + +- **"use client"** - `error.tsx` должен быть клиентским компонентом. +- Он принимает два реквизита: + - `error`: Этот объект является экземпляром родного объекта JavaScript [`Error`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error). + - `reset`: Это функция для сброса границы ошибки. При выполнении функции будет предпринята попытка повторного отображения сегмента маршрута. + +При повторной попытке удалить счет-фактуру вы должны увидеть следующий пользовательский интерфейс: + +![Файл error.tsx, показывающий принимаемые реквизиты](error-page.png) + +## Обработка 404 ошибки с помощью функции `notFound` + +Еще один способ изящной обработки ошибок - использование функции `notFound`. В то время как `error.tsx` полезна для перехвата не пойманных исключений, `notFound` можно использовать, когда вы пытаетесь получить несуществующий ресурс. + +Например, посетите . + +Это поддельный UUID, который не существует в вашей базе данных. + +Вы сразу увидите, как сработает `error.tsx`, потому что это дочерний маршрут `/invoices`, где определен `error.tsx`. + +Однако если вы хотите быть более конкретными, вы можете показать ошибку 404, чтобы сообщить пользователю, что ресурс, к которому он пытается получить доступ, не найден. + +Вы можете подтвердить, что ресурс не был найден, зайдя в функцию `fetchInvoiceById` в `data.ts` и записав в консольный лог возвращаемый `invoice`: + +```ts title="/app/lib/data.ts" hl_lines="6" +export async function fetchInvoiceById(id: string) { + try { + // ... + + console.log(invoice); // Invoice is an empty array [] + return invoice[0]; + } catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch invoice.'); + } +} +``` + +Теперь, когда вы знаете, что счет-фактура не существует в вашей базе данных, давайте воспользуемся функцией `notFound` для его обработки. Перейдите в `/dashboard/invoices/[id]/edit/page.tsx` и импортируйте `{ notFound }` из `'next/navigation'`. + +Затем вы можете использовать условие для вызова `notFound`, если счет-фактура не существует: + +```ts title="/dashboard/invoices/[id]/edit/page.tsx" hl_lines="5 17-19" +import { + fetchInvoiceById, + fetchCustomers, +} from '@/app/lib/data'; +import { notFound } from 'next/navigation'; + +export default async function Page(props: { + params: Promise<{ id: string }>; +}) { + const params = await props.params; + const id = params.id; + const [invoice, customers] = await Promise.all([ + fetchInvoiceById(id), + fetchCustomers(), + ]); + + if (!invoice) { + notFound(); + } + + // ... +} +``` + +Затем, чтобы показать пользователю UI ошибки, создайте файл `not-found.tsx` внутри папки `/edit`. + +![Файл not-found.tsx внутри папки edit](not-found-file.png) + +Внутри файла `not-found.tsx` вставьте следующий код: + +```ts title="/dashboard/invoices/[id]/edit/not-found.tsx" +import Link from 'next/link'; +import { FaceFrownIcon } from '@heroicons/react/24/outline'; + +export default function NotFound() { + return ( +
+ +

+ 404 Not Found +

+

Could not find the requested invoice.

+ + Go Back + +
+ ); +} +``` + +Обновите маршрут, и теперь вы должны увидеть следующий пользовательский интерфейс: + +![404 Not Found Page](404-not-found-page.png) + +Имейте в виду, что `notFound` будет иметь приоритет над `error.tsx`, так что вы можете обратиться к нему, когда захотите обработать более специфические ошибки! + +## Рекомендуемая литература + +Чтобы узнать больше об обработке ошибок в Next.js, ознакомьтесь со следующей документацией: + +- [Обработка ошибок](https://nextjs.org/docs/app/building-your-application/routing/error-handling) +- [`error.js` API Reference](https://nextjs.org/docs/app/api-reference/file-conventions/error) +- [`notFound()` API Reference](https://nextjs.org/docs/app/api-reference/functions/not-found) +- [`not-found.js` API Reference](https://nextjs.org/docs/app/api-reference/file-conventions/not-found) + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/error-page.png b/docs/libs/nextjs/app-router/error-page.png new file mode 100644 index 0000000..61ffede Binary files /dev/null and b/docs/libs/nextjs/app-router/error-page.png differ diff --git a/docs/libs/nextjs/app-router/fetching-data.md b/docs/libs/nextjs/app-router/fetching-data.md new file mode 100644 index 0000000..139ba63 --- /dev/null +++ b/docs/libs/nextjs/app-router/fetching-data.md @@ -0,0 +1,351 @@ +--- +description: Теперь, когда вы создали и запустили свою базу данных, давайте обсудим различные способы получения данных для вашего приложения и создадим страницу обзора дашборда. +--- + +# Получение данных + +Теперь, когда вы создали и засеяли свою базу данных, давайте обсудим различные способы получения данных для вашего приложения и построим обзорную страницу дашборда. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Узнайте о некоторых подходах к получению данных: API, ORM, SQL и т. д. + - Как серверные компоненты могут помочь вам получить более безопасный доступ к внутренним ресурсам. + - Что такое сетевые водопады. + - Как реализовать параллельную выборку данных с помощью JavaScript-паттерна. + +## Выбор способа получения данных + +### Слой API + +API - это промежуточный слой между кодом вашего приложения и базой данных. Есть несколько случаев, когда вы можете использовать API: + +- Если вы используете сторонние сервисы, которые предоставляют API. +- Если вы получаете данные от клиента, вам нужен слой API, работающий на сервере, чтобы не раскрывать клиенту секреты вашей базы данных. + +В Next.js вы можете создавать конечные точки API с помощью [Route Handlers](https://nextjs.org/docs/app/building-your-application/routing/route-handlers). + +### Запросы к базе данных + +При создании приложения полного стека вам также потребуется написать логику для взаимодействия с базой данных. Для [реляционных баз данных](https://aws.amazon.com/relational-database/), таких как Postgres, вы можете сделать это с помощью SQL или [ORM](https://vercel.com/docs/storage/vercel-postgres/using-an-orm). + +Есть несколько случаев, когда вам придется писать запросы к базе данных: + +- При создании конечных точек API вам нужно написать логику для взаимодействия с базой данных. +- Если вы используете React Server Components (получение данных на сервере), вы можете пропустить слой API и запрашивать базу данных напрямую, не рискуя раскрыть секреты базы данных клиенту. + +Давайте узнаем больше о серверных компонентах React. + +### Использование серверных компонентов для получения данных + +По умолчанию приложения Next.js используют **React Server Components**. Получение данных с помощью серверных компонентов - относительно новый подход, и у его использования есть несколько преимуществ: + +- Серверные компоненты поддерживают JavaScript Promises, предоставляя решение для асинхронных задач, таких как получение данных. Вы можете использовать синтаксис `async/await`, не нуждаясь в `useEffect`, `useState` или других библиотеках для получения данных. +- Серверные компоненты работают на сервере, поэтому вы можете хранить дорогостоящие операции по выборке данных и логику на сервере, отправляя клиенту только результат. +- Поскольку серверные компоненты работают на сервере, вы можете запрашивать базу данных напрямую, без дополнительного уровня API. Это избавляет вас от необходимости писать и поддерживать дополнительный код. + +## Использование SQL + +Для вашего приложения дашборда вы будете писать запросы к базе данных с помощью библиотеки [postgres.js](https://github.com/porsager/postgres) и SQL. Есть несколько причин, по которым мы будем использовать SQL: + +- SQL является промышленным стандартом для запросов к реляционным базам данных (например, ORM генерируют SQL под капотом). +- Базовое понимание SQL может помочь вам понять основы реляционных баз данных, что позволит вам применить свои знания в других инструментах. +- SQL универсален и позволяет получать конкретные данные и манипулировать ими. +- Библиотека `postgres.js` обеспечивает защиту от [SQL-инъекций](https://github.com/porsager/postgres?tab=readme-ov-file#query-parameters). + +Не волнуйтесь, если вы раньше не использовали SQL - мы подготовили для вас запросы. + +Перейдите к файлу `/app/lib/data.ts`. Здесь вы увидите, что мы используем `postgres`. Функция `sql` [function](https://github.com/porsager/postgres) позволяет запросить базу данных: + +```ts title="/app/lib/data.ts" +import postgres from 'postgres'; + +const sql = postgres(process.env.POSTGRES_URL!, { + ssl: 'require', +}); + +// ... +``` + +Вы можете вызвать `sql` в любом месте на сервере, как и серверный компонент. Но чтобы вам было проще ориентироваться в компонентах, мы сохранили все запросы к данным в файле `data.ts`, и вы можете импортировать их в компоненты. + +!!!note "" + + Если в главе 6 вы использовали собственный провайдер баз данных, вам нужно будет обновить запросы к базе данных, чтобы они работали с вашим провайдером. Запросы можно найти в файле `/app/lib/data.ts`. + +## Получение данных для страницы обзора дашборда + +Теперь, когда вы поняли различные способы получения данных, давайте получим данные для страницы обзора дашборда. Перейдите в `/app/dashboard/page.tsx`, вставьте следующий код и потратьте некоторое время на его изучение: + +```ts title="/app/dashboard/page.tsx" +import { Card } from '@/app/ui/dashboard/cards'; +import RevenueChart from '@/app/ui/dashboard/revenue-chart'; +import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; +import { lusitana } from '@/app/ui/fonts'; + +export default async function Page() { + return ( +
+

+ Dashboard +

+
+ {/* */} + {/* */} + {/* */} + {/* */} +
+
+ {/* */} + {/* */} +
+
+ ); +} +``` + +Код выше намеренно закомментирован. Теперь мы начнем приводить примеры каждой части. + +- `page` - это серверный компонент **async**. Он позволяет использовать `await` для получения данных. +- Также есть 3 компонента, которые получают данные: ``, `` и ``. В настоящее время они закомментированы и пока не реализованы. + +## Получение данных для ``. + +Чтобы получить данные для компонента ``, импортируйте функцию `fetchRevenue` из `data.ts` и вызовите ее внутри вашего компонента: + +```ts title="/app/dashboard/page.tsx" hl_lines="5 7-8" +import { Card } from '@/app/ui/dashboard/cards'; +import RevenueChart from '@/app/ui/dashboard/revenue-chart'; +import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; +import { lusitana } from '@/app/ui/fonts'; +import { fetchRevenue } from '@/app/lib/data'; + +export default async function Page() { + const revenue = await fetchRevenue(); + // ... +} +``` + +Далее сделаем следующее: + +- Откомментируйте компонент ``. +- Перейдите к файлу компонента (`/app/ui/dashboard/revenue-chart.tsx`) и откомментируйте код внутри него. +- Проверьте `localhost:3000` и вы должны увидеть график, использующий данные `revenue`. + +![График выручки, показывающий общую выручку за последние 12 месяцев](recent-revenue.png) + +Давайте продолжим импортировать больше данных и отображать их на дашборде. + +## Получение данных для `` + +Для компонента `` нам нужно получить 5 последних счетов-фактур, отсортированных по дате. + +Вы можете получить все счета и отсортировать их с помощью JavaScript. Это не проблема, поскольку наши данные невелики, но по мере роста вашего приложения может значительно увеличиться объем данных, передаваемых при каждом запросе, и JavaScript, необходимый для их сортировки. + +Вместо того чтобы сортировать последние счета в памяти, вы можете использовать SQL-запрос, чтобы получить только 5 последних счетов. Например, вот SQL-запрос из вашего файла `data.ts`: + +```ts title="/app/lib/data.ts" +// Fetch the last 5 invoices, sorted by date +const data = await sql` + SELECT invoices.amount, customers.name, customers.image_url, customers.email + FROM invoices + JOIN customers ON invoices.customer_id = customers.id + ORDER BY invoices.date DESC + LIMIT 5`; +``` + +На своей странице импортируйте функцию `fetchLatestInvoices`: + +```ts title="/app/dashboard/page.tsx" hl_lines="5-8 12" +import { Card } from '@/app/ui/dashboard/cards'; +import RevenueChart from '@/app/ui/dashboard/revenue-chart'; +import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; +import { lusitana } from '@/app/ui/fonts'; +import { + fetchRevenue, + fetchLatestInvoices, +} from '@/app/lib/data'; + +export default async function Page() { + const revenue = await fetchRevenue(); + const latestInvoices = await fetchLatestInvoices(); + // ... +} +``` + +Затем откомментируйте компонент ``. Вам также нужно будет откомментировать соответствующий код в самом компоненте ``, расположенном по адресу `/app/ui/dashboard/latest-invoices`. + +Если вы зайдете на свой `localhost`, то увидите, что из базы данных возвращаются только последние `5`. Надеюсь, вы начинаете понимать преимущества прямого запроса к базе данных! + +![Компонент «Последние счета» рядом с графиком выручки](latest-invoices.png) + +## Практика: Получение данных для компонентов `` + +Теперь настала ваша очередь получить данные для компонентов ``. На карточках будут отображаться следующие данные: + +- Общая сумма собранных счетов. +- Общая сумма счетов, ожидающих оплаты. +- Общее количество счетов-фактур. +- Общее количество клиентов. + +Опять же, у вас может возникнуть соблазн получить все счета и клиентов и использовать JavaScript для работы с данными. Например, вы можете использовать `Array.length` для получения общего количества счетов и клиентов: + +```ts +const totalInvoices = allInvoices.length; +const totalCustomers = allCustomers.length; +``` + +Но с помощью SQL вы можете получить только те данные, которые вам нужны. Это немного дольше, чем использование `Array.length`, но это означает, что во время запроса нужно передать меньше данных. Это альтернатива SQL: + +```ts title="/app/lib/data.ts" +const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`; +const customerCountPromise = sql`SELECT COUNT(*) FROM customers`; +``` + +Функция, которую вам нужно импортировать, называется `fetchCardData`. Вам нужно будет деструктурировать значения, возвращаемые функцией. + +!!!tip "Подсказка" + + - Проверьте компоненты карты, чтобы узнать, какие данные им нужны. + - Проверьте файл `data.ts`, чтобы увидеть, что возвращает функция. + +Когда все будет готово, разверните тумблер ниже, чтобы увидеть окончательный код: + +???note "Раскрыть решение" + + ```ts title="/app/dashboard/page.tsx" hl_lines="8 14-19" + import { Card } from '@/app/ui/dashboard/cards'; + import RevenueChart from '@/app/ui/dashboard/revenue-chart'; + import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; + import { lusitana } from '@/app/ui/fonts'; + import { + fetchRevenue, + fetchLatestInvoices, + fetchCardData, + } from '@/app/lib/data'; + + export default async function Page() { + const revenue = await fetchRevenue(); + const latestInvoices = await fetchLatestInvoices(); + const { + numberOfInvoices, + numberOfCustomers, + totalPaidInvoices, + totalPendingInvoices, + } = await fetchCardData(); + + return ( +
+

+ Dashboard +

+
+ + + + +
+
+ + +
+
+ ); + } + ``` + +Отлично! Теперь вы получили все данные для страницы обзора дашборда. Ваша страница должна выглядеть следующим образом: + +![Страница дашборда со всеми полученными данными](complete-dashboard.png) + +Однако... есть две вещи, о которых вы должны знать: + +- Запросы данных непреднамеренно блокируют друг друга, создавая **водопад запросов**. +- По умолчанию Next.js **предусматривает** маршруты для повышения производительности, это называется Static Rendering. Таким образом, если ваши данные изменятся, они не будут отражены в дашборде. + +Давайте обсудим номер 1 в этой главе, а затем подробно рассмотрим номер 2 в следующей главе. + +## Что такое водопад запросов? + +Под «водопадом» понимается последовательность сетевых запросов, зависящих от завершения предыдущих запросов. В случае с получением данных каждый запрос может начаться только после того, как предыдущий запрос вернет данные. + +![Диаграмма, показывающая время при последовательной выборке данных и параллельной выборке данных](sequential-parallel-data-fetching.png) + +Например, нам нужно дождаться выполнения `fetchRevenue()`, прежде чем `fetchLatestInvoices()` сможет начать работу, и так далее. + +```ts title="/app/dashboard/page.tsx" +const revenue = await fetchRevenue(); +const latestInvoices = await fetchLatestInvoices(); // wait for fetchRevenue() to finish +const { + numberOfInvoices, + numberOfCustomers, + totalPaidInvoices, + totalPendingInvoices, +} = await fetchCardData(); // wait for fetchLatestInvoices() to finish +``` + +Такая схема не обязательно плоха. Бывают случаи, когда водопады нужны, потому что вы хотите, чтобы условие было выполнено до того, как вы сделаете следующий запрос. Например, сначала нужно получить идентификатор пользователя и информацию о его профиле. Получив идентификатор, можно перейти к получению списка его друзей. В этом случае каждый запрос зависит от данных, полученных от предыдущего запроса. + +Однако такое поведение может быть непреднамеренным и влиять на производительность. + +## Параллельная выборка данных + +Распространенный способ избежать водопада - инициировать все запросы данных одновременно - параллельно. + +В JavaScript вы можете использовать функции [`Promise.all()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/all) или [`Promise.allSettled()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Promise/allSettled) для одновременной инициации всех обещаний. Например, в файле `data.ts` мы используем функцию `Promise.all()` в функции `fetchCardData()`: + +```ts title="/app/lib/data.ts" hl_lines="10-14" +export async function fetchCardData() { + try { + const invoiceCountPromise = sql`SELECT COUNT(*) FROM invoices`; + const customerCountPromise = sql`SELECT COUNT(*) FROM customers`; + const invoiceStatusPromise = sql`SELECT + SUM(CASE WHEN status = 'paid' THEN amount ELSE 0 END) AS "paid", + SUM(CASE WHEN status = 'pending' THEN amount ELSE 0 END) AS "pending" + FROM invoices`; + + const data = await Promise.all([ + invoiceCountPromise, + customerCountPromise, + invoiceStatusPromise, + ]); + // ... + } catch (e) { + // ... + } +} +``` + +Используя этот паттерн, вы можете: + +- Начать выполнять все запросы на получение данных одновременно, что быстрее, чем ждать завершения каждого запроса в водопаде. +- Использовать собственный JavaScript-шаблон, который можно применить к любой библиотеке или фреймворку. + +Однако есть один **недостаток** в том, чтобы полагаться только на этот JavaScript-шаблон: что произойдет, если один запрос данных будет выполняться медленнее, чем все остальные? Давайте узнаем об этом в следующей главе. + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/folders-to-url-segments.png b/docs/libs/nextjs/app-router/folders-to-url-segments.png new file mode 100644 index 0000000..1a097dc Binary files /dev/null and b/docs/libs/nextjs/app-router/folders-to-url-segments.png differ diff --git a/docs/libs/nextjs/app-router/font-layout-shift.png b/docs/libs/nextjs/app-router/font-layout-shift.png new file mode 100644 index 0000000..55025eb Binary files /dev/null and b/docs/libs/nextjs/app-router/font-layout-shift.png differ diff --git a/docs/libs/nextjs/app-router/getting-started.md b/docs/libs/nextjs/app-router/getting-started.md new file mode 100644 index 0000000..ff7932f --- /dev/null +++ b/docs/libs/nextjs/app-router/getting-started.md @@ -0,0 +1,128 @@ +--- +description: Начало работы +--- + +# Начало работы + +## Создание нового проекта + +Мы рекомендуем использовать [`pnpm`](https://pnpm.io/) в качестве менеджера пакетов, так как он быстрее и эффективнее, чем `npm` или `yarn`. Если у вас не установлен `pnpm`, вы можете установить его глобально, выполнив команду: + +```sh title="Терминал" +npm install -g pnpm +``` + +Чтобы создать приложение Next.js, откройте терминал, перейдите в папку, в которой вы хотите хранить свой проект, и выполните следующую команду: + +```sh title="Терминал" +npx create-next-app@latest nextjs-dashboard --example «https://github.com/vercel/next-learn/tree/main/dashboard/starter-example» --use-pnpm +``` + +Эта команда использует [`create-next-app`](https://nextjs.org/docs/app/api-reference/create-next-app), инструмент интерфейса командной строки (CLI), который создает для вас приложение Next.js. В приведенной выше команде вы также используете флаг `--example` с [начальным примером](https://github.com/vercel/next-learn/tree/main/dashboard/starter-example) для этого курса. + +## Изучение проекта + +В отличие от учебников, в которых вам приходится писать код с нуля, большая часть кода для этого курса уже написана за вас. Это лучше отражает реальную разработку, где вы, скорее всего, будете работать с существующими кодовыми базами. + +Наша цель - помочь вам сосредоточиться на изучении основных возможностей Next.js, не прибегая к написанию всего кода приложения. + +После установки откройте проект в редакторе кода и перейдите к `nextjs-dashboard`. + +```sh title="Терминал" +cd nextjs-dashboard +``` + +Давайте потратим немного времени на изучение проекта. + +### Структура папок + +Вы заметите, что проект имеет следующую структуру папок: + +![Структура папок проекта дашборда, показывающая основные папки и файлы: app, public и config.](learn-folder-structure.png) + +- `/app`: Содержит все маршруты, компоненты и логику вашего приложения, именно здесь вы будете работать в основном. +- `/app/lib`: Содержит функции, используемые в вашем приложении, такие как утилиты многократного использования и функции получения данных. +- `/app/ui`: Содержит все компоненты пользовательского интерфейса вашего приложения, такие как карточки, таблицы и формы. Чтобы сэкономить время, мы предварительно стилизовали эти компоненты для вас. +- `/public`: Содержит все статические активы вашего приложения, такие как изображения. +- _Файлы конфигурации_: В корне вашего приложения вы также заметите файлы конфигурации, такие как `next.config.ts`. Большинство этих файлов создаются и предварительно настраиваются при запуске нового проекта с помощью `create-next-app`. В этом курсе вам не придется их изменять. + +Не стесняйтесь исследовать эти папки и не волнуйтесь, если вы пока не понимаете всего, что делает код. + +### Данные-заполнители + +Когда вы создаете пользовательские интерфейсы, полезно иметь некоторые данные-заполнители. Если база данных или API еще не доступны, вы можете: + +- Использовать данные-заполнители в формате JSON или в виде объектов JavaScript. +- Использовать сторонний сервис, например [mockAPI](https://mockapi.io/). + +Для этого проекта мы предоставили некоторые данные-плейсхолдеры в `app/lib/placeholder-data.ts`. Каждый объект JavaScript в этом файле представляет собой таблицу в вашей базе данных. Например, для таблицы счетов-фактур: + +```ts title="/app/lib/placeholder-data.ts" +const invoices = [ + { + customer_id: customers[0].id, + amount: 15795, + status: 'pending', + date: '2022-12-06', + }, + { + customer_id: customers[1].id, + amount: 20348, + status: 'pending', + date: '2022-11-14', + }, + // ... +]; +``` + +В главе, посвященной [настройке базы данных](./setting-up-your-database.md), вы будете использовать эти данные для посева базы данных (наполнения ее некоторыми исходными данными). + +### TypeScript + +Вы также можете заметить, что большинство файлов имеют суффикс `.ts` или `.tsx`. Это потому, что проект написан на TypeScript. Мы хотели создать курс, отражающий современный веб-ландшафт. + +Ничего страшного, если вы не знаете TypeScript - мы будем предоставлять фрагменты кода на TypeScript, когда это потребуется. + +А пока посмотрите на файл `/app/lib/definitions.ts`. Здесь мы вручную определяем типы, которые будут возвращаться из базы данных. Например, таблица `invoices` имеет следующие типы: + +```ts title="/app/lib/definitions.ts" +export type Invoice = { + id: string; + customer_id: string; + amount: number; + date: string; + // In TypeScript, this is called a string union type. + // It means that the "status" property can only be one + // of the two strings: 'pending' or 'paid'. + status: 'pending' | 'paid'; +}; +``` + +Используя TypeScript, вы можете исключить случайную передачу неправильного формата данных в компоненты или базу данных, например, передачу `string` вместо `number` в `amount`. + +Если вы являетесь разработчиком TypeScript: + +- Мы вручную объявляем типы данных, но для большей безопасности типов мы рекомендуем [Prisma](https://www.prisma.io/) или [Drizzle](https://orm.drizzle.team/), которые автоматически генерируют типы на основе схемы вашей базы данных. +- Next.js определяет, использует ли ваш проект TypeScript, и автоматически устанавливает необходимые пакеты и конфигурацию. Next.js также поставляется с [плагином TypeScript](https://nextjs.org/docs/app/building-your-application/configuring/typescript#typescript-plugin) для вашего редактора кода, чтобы помочь с автозаполнением и безопасностью типов. + +## Запуск сервера разработки + +Запустите `pnpm i`, чтобы установить пакеты проекта. + +```sh title="Терминал" +pnpm i +``` + +За ним следует `pnpm dev`, чтобы запустить сервер разработки. + +```sh title="Терминал" +pnpm dev +``` + +`pnpm dev` запускает ваш сервер разработки Next.js на порту `3000`. Давайте проверим, работает ли он. + +Откройте в вашем браузере. Ваша домашняя страница должна выглядеть так, как показано на этой странице, которая намеренно не оформлена: + +![Нетипичная страница с заголовком 'Acme', описанием и ссылкой для входа.](./acme-unstyled.png) + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/home-page-with-hero.png b/docs/libs/nextjs/app-router/home-page-with-hero.png new file mode 100644 index 0000000..956b2d9 Binary files /dev/null and b/docs/libs/nextjs/app-router/home-page-with-hero.png differ diff --git a/docs/libs/nextjs/app-router/home-page-with-tailwind.png b/docs/libs/nextjs/app-router/home-page-with-tailwind.png new file mode 100644 index 0000000..6c5a21a Binary files /dev/null and b/docs/libs/nextjs/app-router/home-page-with-tailwind.png differ diff --git a/docs/libs/nextjs/app-router/import-git-repo.png b/docs/libs/nextjs/app-router/import-git-repo.png new file mode 100644 index 0000000..4839900 Binary files /dev/null and b/docs/libs/nextjs/app-router/import-git-repo.png differ diff --git a/docs/libs/nextjs/app-router/index.md b/docs/libs/nextjs/app-router/index.md new file mode 100644 index 0000000..67672a2 --- /dev/null +++ b/docs/libs/nextjs/app-router/index.md @@ -0,0 +1,52 @@ +--- +description: Добро пожаловать на курс «Основы Next.js»! В этом бесплатном интерактивном курсе вы познакомитесь с основными возможностями Next.js, создав веб-приложение полного стека. +--- + +# Основы Next.js + +Добро пожаловать на курс «Основы Next.js»! В этом бесплатном интерактивном курсе вы познакомитесь с основными возможностями Next.js, создав веб-приложение полного стека. + +## Что мы будем создавать + +![Объяснительная по курсу](../course-explainer.png) + +В этом курсе мы создадим финансовый дашборд, который будет иметь: + +- Публичная главная страница. +- Страница входа в систему. +- Страницы дашборда, защищенные аутентификацией. +- Возможность для пользователей добавлять, редактировать и удалять счета-фактуры. + +Дашборд также будет иметь сопутствующую базу данных, которую вы настроите в одной из [следующих глав](setting-up-your-database.md). + +К концу курса вы получите основные навыки, необходимые для создания полнофункциональных приложений Next.js. + +## Обзор + +Вот обзор функций, о которых вы узнаете в этом курсе: + +- **Стилизация**: Различные способы стилизации вашего приложения в Next.js. +- **Оптимизации**: Как оптимизировать изображения, ссылки и шрифты. +- **Маршрутизация**: Как создавать вложенные макеты и страницы с помощью маршрутизации файловой системы. +- **Получение данных**: как настроить базу данных Postgres на Vercel, а также лучшие практики для получения и потоковой передачи данных. +- **Поиск и пагинация**: Как реализовать поиск и пагинацию с использованием параметров поиска URL. +- **Мутирование данных**: Как изменять данные с помощью React Server Actions и перепроверять кэш Next.js. +- **Обработка ошибок**: Как обрабатывать общие ошибки и ошибки 404 not found. +- **Валидация форм и доступность**: как выполнять валидацию форм на стороне сервера и советы по улучшению доступности. +- **Аутентификация**: Как добавить аутентификацию в приложение с помощью NextAuth.js и Middleware. +- **Метаданные**: Как добавить метаданные и подготовить приложение к публикации в социальных сетях. + +## Необходимые знания + +Этот курс предполагает, что вы имеете базовое представление о React и JavaScript. Если вы новичок в React, мы рекомендуем сначала пройти наш курс «[Основы React](https://nextjs.org/learn/react-foundations)», чтобы узнать об основах React, таких как компоненты, реквизиты, состояние и хуки, а также о новых функциях, таких как серверные компоненты и Suspense. + +## Системные требования + +Прежде чем начать этот курс, убедитесь, что ваша система соответствует следующим требованиям: + +- Установлен [Node.js](https://nodejs.org/en) 18.18.0 или более поздняя версия. +- Операционные системы: macOS, Windows (включая WSL) или Linux. + +Кроме того, вам понадобятся учетная [запись GitHub](https://github.com/join/) и [учетная запись Vercel](https://vercel.com/signup). + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/latest-invoices.png b/docs/libs/nextjs/app-router/latest-invoices.png new file mode 100644 index 0000000..1027ad7 Binary files /dev/null and b/docs/libs/nextjs/app-router/latest-invoices.png differ diff --git a/docs/libs/nextjs/app-router/learn-folder-structure.png b/docs/libs/nextjs/app-router/learn-folder-structure.png new file mode 100644 index 0000000..6953479 Binary files /dev/null and b/docs/libs/nextjs/app-router/learn-folder-structure.png differ diff --git a/docs/libs/nextjs/app-router/loading-page-with-skeleton.png b/docs/libs/nextjs/app-router/loading-page-with-skeleton.png new file mode 100644 index 0000000..ec9408a Binary files /dev/null and b/docs/libs/nextjs/app-router/loading-page-with-skeleton.png differ diff --git a/docs/libs/nextjs/app-router/loading-page.png b/docs/libs/nextjs/app-router/loading-page.png new file mode 100644 index 0000000..3a25303 Binary files /dev/null and b/docs/libs/nextjs/app-router/loading-page.png differ diff --git a/docs/libs/nextjs/app-router/loading-revenue-chart.png b/docs/libs/nextjs/app-router/loading-revenue-chart.png new file mode 100644 index 0000000..af3b0de Binary files /dev/null and b/docs/libs/nextjs/app-router/loading-revenue-chart.png differ diff --git a/docs/libs/nextjs/app-router/mutating-data.md b/docs/libs/nextjs/app-router/mutating-data.md new file mode 100644 index 0000000..dccdfa9 --- /dev/null +++ b/docs/libs/nextjs/app-router/mutating-data.md @@ -0,0 +1,707 @@ +--- +description: В предыдущей главе вы реализовали поиск и пагинацию с помощью URL Search Params и Next.js API. Давайте продолжим работу над страницей «Счета», добавив возможность создавать, обновлять и удалять счета! +--- + +# Изменение данных + +В предыдущей главе вы реализовали поиск и пагинацию с помощью URL Search Params и Next.js API. Давайте продолжим работу над страницей «Счета», добавив возможность создавать, обновлять и удалять счета! + +!!!info "Вот темы, которые мы рассмотрим" + + - Что такое React Server Actions и как их использовать для изменения данных. + - Как работать с формами и серверными компонентами. + - Лучшие практики работы с родным объектом `FormData`, включая валидацию типов. + - Как пересмотреть клиентский кэш с помощью API `revalidatePath`. + - Как создавать динамические сегменты маршрута с определенными идентификаторами. + +## Что такое серверные операции? + +React Server Actions позволяют запускать асинхронный код непосредственно на сервере. Они устраняют необходимость создания конечных точек API для изменения данных. Вместо этого вы пишете асинхронные функции, которые выполняются на сервере и могут быть вызваны из ваших клиентских или серверных компонентов. + +Безопасность является главным приоритетом для веб-приложений, поскольку они могут быть уязвимы для различных угроз. Именно здесь на помощь приходят Server Actions. Они включают такие функции, как зашифрованные закрытия, строгие проверки ввода, хэширование сообщений об ошибках, ограничения хоста и многое другое - все вместе они значительно повышают безопасность вашего приложения. + +## Использование форм с Server Actions + +В React вы можете использовать атрибут `action` в элементе `
` для вызова действий. Действие автоматически получит нативный объект [FormData](https://developer.mozilla.org/en-US/docs/Web/API/FormData), содержащий перехваченные данные. + +Например: + +```ts +// Server Component +export default function Page() { + // Action + async function create(formData: FormData) { + 'use server'; + + // Logic to mutate data... + } + + // Invoke the action using the "action" attribute + return ...; +} +``` + +Преимуществом вызова Server Actions внутри Server Component является прогрессивное улучшение - формы работают, даже если JavaScript еще не загружен на клиенте. Например, при отсутствии медленных интернет-соединений. + +## Next.js с Server Actions + +Server Actions также глубоко интегрированы с Next.js [кэшированием](https://nextjs.org/docs/app/building-your-application/caching). Когда форма отправляется через Server Actions, вы можете не только использовать действие для изменения данных, но и пересмотреть связанный с ним кэш с помощью таких API, как `revalidatePath` и `revalidateTag`. + +Давайте посмотрим, как все это работает вместе! + +## Создание счета-фактуры + +Вот шаги, которые необходимо предпринять, чтобы создать новый счет-фактуру: + +1. Создайте форму для ввода данных пользователем. +2. Создайте Server Actions и вызовите его из формы. +3. Внутри действия сервера извлеките данные из объекта `formData`. +4. Проверьте и подготовьте данные для вставки в базу данных. +5. Вставьте данные и обработайте все ошибки. +6. Переопределите кэш и перенаправьте пользователя обратно на страницу счетов. + +### 1. Создайте новый маршрут и форму + +Для начала внутри папки `/invoices` добавьте новый сегмент маршрута `/create` с файлом `page.tsx`: + +![Папка Invoices с вложенной папкой create и файлом page.tsx внутри нее](create-invoice-route.png) + +Вы будете использовать этот маршрут для создания новых счетов-фактур. Внутри вашего файла `page.tsx` вставьте следующий код, а затем потратьте некоторое время на его изучение: + +```ts title="/dashboard/invoices/create/page.tsx" +import Form from '@/app/ui/invoices/create-form'; +import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; +import { fetchCustomers } from '@/app/lib/data'; + +export default async function Page() { + const customers = await fetchCustomers(); + + return ( +
+ +
+
+ ); +} +``` + +Ваша страница - это серверный компонент, который получает данные `customers` и передает их компоненту ``. Чтобы сэкономить время, мы уже создали компонент `` для вас. + +Перейдите к компоненту ``, и вы увидите, что форма: + +- Имеет один элемент `` для **суммы** с `type=«number»`. +- Имеет два элемента `` для статуса с `type=«radio»`. +- Имеет одну кнопку с `type=«submit»`. + +На вы должны увидеть следующий пользовательский интерфейс: + +![Создание страницы счета-фактуры с хлебными крошками и формой](create-invoice-page.png) + +### 2. Создание Server Action + +Отлично, теперь давайте создадим серверное действие, которое будет вызываться при отправке формы. + +Перейдите в директорию `lib/` и создайте новый файл с именем `actions.ts`. В верхней части этого файла добавьте директиву React [use server](https://react.dev/reference/react/use-server): + +```ts title="/app/lib/actions.ts" +'use server'; +``` + +Добавив `"use server"`, вы помечаете все экспортируемые функции в файле как Server Actions. Затем эти серверные функции можно импортировать и использовать в компонентах Client и Server. Все неиспользуемые функции, включенные в этот файл, будут автоматически удалены из конечного пакета приложения. + +Вы также можете писать Server Actions непосредственно в компонентах сервера, добавляя `"use server"` внутри действия. Но для этого курса мы сохраним их все в отдельном файле. Мы рекомендуем иметь отдельный файл для ваших действий. + +В файле `actions.ts` создайте новую асинхронную функцию, которая принимает `formData`: + +```ts title="/app/lib/actions.ts" hl_lines="3" +'use server'; + +export async function createInvoice(formData: FormData) {} +``` + +Затем в компоненте `` импортируйте действие `createInvoice` из файла `actions.ts`. Добавьте атрибут `action` к элементу `` и вызовите действие `createInvoice`. + +```ts title="/app/ui/invoices/create-form.tsx" hl_lines="10 17" +import { CustomerField } from '@/app/lib/definitions'; +import Link from 'next/link'; +import { + CheckIcon, + ClockIcon, + CurrencyDollarIcon, + UserCircleIcon, +} from '@heroicons/react/24/outline'; +import { Button } from '@/app/ui/button'; +import { createInvoice } from '@/app/lib/actions'; + +export default function Form({ + customers, +}: { + customers: CustomerField[]; +}) { + return ...; +} +``` + +!!!note "Полезно знать" + + В HTML вы передаете URL в атрибуте `action`. Этот URL будет местом назначения, куда должны быть отправлены данные вашей формы (обычно это конечная точка API). + + Однако в React атрибут `action` считается специальным реквизитом - то есть React строит поверх него, чтобы позволить вызывать действия. + + За кулисами Server Actions создают конечную точку API `POST`. Вот почему при использовании Server Actions вам не нужно создавать конечные точки API вручную. + +### 3. Извлечение данных из формы FormData + +Вернувшись в файл `actions.ts`, вам нужно будет извлечь значения из `formData`, есть [пара методов](https://developer.mozilla.org/en-US/docs/Web/API/FormData), которые вы можете использовать. Для этого примера воспользуемся методом [`.get(name)`](https://developer.mozilla.org/en-US/docs/Web/API/FormData/get). + +```ts title="/app/lib/actions.ts" hl_lines="3-11" +'use server'; + +export async function createInvoice(formData: FormData) { + const rawFormData = { + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }; + // Test it out: + console.log(rawFormData); +} +``` + +!!!tip "Подсказка" + + Если вы работаете с формами, содержащими много полей, возможно, вам стоит подумать об использовании метода [`entries()`](https://developer.mozilla.org/docs/Web/API/FormData/entries) с методом JavaScript [`Object.fromEntries()`](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Object/fromEntries). + +Чтобы убедиться, что все подключено правильно, попробуйте использовать форму. После отправки вы должны увидеть данные, которые вы только что ввели в форму, зарегистрированные в вашем **терминале** (не в браузере). + +Теперь, когда ваши данные имеют форму объекта, с ними будет гораздо проще работать. + +### 4. Проверка и подготовка данных + +Перед отправкой данных из формы в базу данных необходимо убедиться, что они имеют правильный формат и типы. Если вы помните из предыдущего курса, таблица счетов-фактур ожидает данные в следующем формате: + +```ts title="/app/lib/definitions.ts" +export type Invoice = { + id: string; // Will be created on the database + customer_id: string; + amount: number; // Stored in cents + status: 'pending' | 'paid'; + date: string; +}; +``` + +Пока у вас есть только `customer_id`, `amount` и `status` из формы. + +**Валидация и принуждение типов** + +Важно проверить, чтобы данные из формы соответствовали ожидаемым типам в вашей базе данных. Например, если вы добавите `console.log` внутри вашего действия: + +```ts +console.log(typeof rawFormData.amount); +``` + +Вы заметите, что `amount` имеет тип `string`, а не `number`. Это потому, что элементы `input` с `type=«number»` на самом деле возвращают строку, а не число! + +Чтобы справиться с проверкой типов, у вас есть несколько вариантов. Хотя вы можете проверять типы вручную, использование библиотеки проверки типов поможет вам сэкономить время и силы. В нашем примере мы будем использовать [Zod](https://zod.dev/), библиотеку проверки типов на основе TypeScript, которая может упростить вам эту задачу. + +В файле `actions.ts` импортируйте Zod и определите схему, соответствующую форме объекта формы. Эта схема будет проверять `formData` перед сохранением в базу данных. + +```ts title="/app/lib/actions.ts" hl_lines="3 5-11 13-16" +'use server'; + +import { z } from 'zod'; + +const FormSchema = z.object({ + id: z.string(), + customerId: z.string(), + amount: z.coerce.number(), + status: z.enum(['pending', 'paid']), + date: z.string(), +}); + +const CreateInvoice = FormSchema.omit({ + id: true, + date: true, +}); + +export async function createInvoice(formData: FormData) { + // ... +} +``` + +Поле `amount` специально настроено на коэрцитивную смену строки на число с одновременной проверкой его типа. + +Затем вы можете передать ваши `rawFormData` в `CreateInvoice` для проверки типов: + +```ts title="/app/lib/actions.ts" hl_lines="3-7" +// ... +export async function createInvoice(formData: FormData) { + const { + customerId, + amount, + status, + } = CreateInvoice.parse({ + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }); +} +``` + +**Хранение значений в центах** + +Обычно в базе данных принято хранить денежные значения в центах, чтобы исключить ошибки JavaScript с плавающей точкой и обеспечить большую точность. + +Давайте переведем сумму в центы: + +```ts title="/app/lib/actions.ts" hl_lines="12" +// ... +export async function createInvoice(formData: FormData) { + const { + customerId, + amount, + status, + } = CreateInvoice.parse({ + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }); + const amountInCents = amount * 100; +} +``` + +**Создание новых дат** + +Наконец, давайте создадим новую дату с форматом «ГГГГ-ММ-ДД» для даты создания счета-фактуры: + +```ts title="/app/lib/actions.ts" hl_lines="13" +// ... +export async function createInvoice(formData: FormData) { + const { + customerId, + amount, + status, + } = CreateInvoice.parse({ + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }); + const amountInCents = amount * 100; + const date = new Date().toISOString().split('T')[0]; +} +``` + +### 5. Вставка данных в базу данных + +Теперь, когда у вас есть все необходимые значения для базы данных, вы можете создать SQL-запрос для вставки нового счета-фактуры в базу данных и передать переменные: + +```ts title="/app/lib/actions.ts" hl_lines="2 23-26" +import { z } from 'zod'; +import postgres from 'postgres'; + +const sql = postgres(process.env.POSTGRES_URL!, { + ssl: 'require', +}); + +// ... + +export async function createInvoice(formData: FormData) { + const { + customerId, + amount, + status, + } = CreateInvoice.parse({ + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }); + const amountInCents = amount * 100; + const date = new Date().toISOString().split('T')[0]; + + await sql` + INSERT INTO invoices (customer_id, amount, status, date) + VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) + `; +} +``` + +Сейчас мы не обрабатываем никаких ошибок. Мы поговорим об этом в следующей главе. А пока перейдем к следующему шагу. + +### 6. Ревалидация и перенаправление + +Next.js имеет кэш маршрутизатора на стороне клиента, который хранит сегменты маршрута в браузере пользователя в течение некоторого времени. Вместе с [prefetching](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#1-prefetching) этот кэш обеспечивает пользователям быстрый переход между маршрутами, сокращая при этом количество запросов к серверу. + +Поскольку вы обновляете данные, отображаемые в маршруте «Счета-фактуры», вам нужно очистить этот кэш и вызвать новый запрос к серверу. Это можно сделать с помощью функции [`revalidatePath`](https://nextjs.org/docs/app/api-reference/functions/revalidatePath) из Next.js: + +```ts title="/app/lib/actions.ts" hl_lines="4 31" +'use server'; + +import { z } from 'zod'; +import { revalidatePath } from 'next/cache'; +import postgres from 'postgres'; + +const sql = postgres(process.env.POSTGRES_URL!, { + ssl: 'require', +}); + +// ... + +export async function createInvoice(formData: FormData) { + const { + customerId, + amount, + status, + } = CreateInvoice.parse({ + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }); + const amountInCents = amount * 100; + const date = new Date().toISOString().split('T')[0]; + + await sql` + INSERT INTO invoices (customer_id, amount, status, date) + VALUES (${customerId}, ${amountInCents}, ${status}, ${date}) + `; + + revalidatePath('/dashboard/invoices'); +} +``` + +После обновления базы данных путь `/dashboard/invoices` будет перепроверен, и с сервера будут получены свежие данные. + +В этот момент вы также хотите перенаправить пользователя обратно на страницу `/dashboard/invoices`. Это можно сделать с помощью функции [`redirect`](https://nextjs.org/docs/app/api-reference/functions/redirect) из Next.js: + +```ts title="/app/lib/actions.ts" hl_lines="5 18" +'use server'; + +import { z } from 'zod'; +import { revalidatePath } from 'next/cache'; +import { redirect } from 'next/navigation'; +import postgres from 'postgres'; + +const sql = postgres(process.env.POSTGRES_URL!, { + ssl: 'require', +}); + +// ... + +export async function createInvoice(formData: FormData) { + // ... + + revalidatePath('/dashboard/invoices'); + redirect('/dashboard/invoices'); +} +``` + +Поздравляем! Вы только что реализовали свое первое Server Actions. Протестируйте его, добавив новый счет-фактуру, если все работает правильно: + +1. При отправке вы должны быть перенаправлены на маршрут `/dashboard/invoices`. +2. Вы должны увидеть новый счет-фактуру в верхней части таблицы. + +## Обновление счета-фактуры + +Форма обновления счета-фактуры аналогична форме создания счета-фактуры, за исключением того, что вам нужно будет передать `id` счета-фактуры, чтобы обновить запись в вашей базе данных. Давайте посмотрим, как можно получить и передать `id` счета-фактуры. + +Вот шаги, которые необходимо выполнить для обновления счета-фактуры: + +1. Создайте новый сегмент динамического маршрута с `id` счета-фактуры. +2. Считайте `id` счета-фактуры из параметров страницы. +3. Получите конкретный счет-фактуру из базы данных. +4. Предварительно заполните форму данными о счете. +5. Обновите данные о счете-фактуре в своей базе данных. + +### 1. Создайте сегмент динамического маршрута с идентификатором счета-фактуры + +Next.js позволяет создавать [Сегменты динамических маршрутов](https://nextjs.org/docs/app/building-your-application/routing/dynamic-routes), когда вы не знаете точного названия сегмента и хотите создавать маршруты на основе данных. Это могут быть заголовки записей в блоге, страницы товаров и т. д. Вы можете создать динамические сегменты маршрутов, обернув имя папки в квадратные скобки. Например, `[id]`, `[post]` или `[slug]`. + +В папке `/invoices` создайте новый динамический маршрут `[id]`, затем новый маршрут `edit` с файлом `page.tsx`. Ваша файловая структура должна выглядеть следующим образом: + +![Папка Invoices с вложенной папкой id и папкой edit внутри нее](edit-invoice-route.png) + +В компоненте `
` обратите внимание на кнопку ``, которая получает `id` счета-фактуры из записей таблицы. + +```ts title="/app/ui/invoices/table.tsx" hl_lines="11" +export default async function InvoicesTable({ + query, + currentPage, +}: { + query: string; + currentPage: number; +}) { + return ( + // ... + + // ... + ); +} +``` + +Перейдите к компоненту `` и обновите `href` в `Link`, чтобы он принимал свойство `id`. Вы можете использовать литералы шаблона для ссылки на динамический сегмент маршрута: + +```ts title="/app/ui/invoices/buttons.tsx" hl_lines="13" +import { + PencilIcon, + PlusIcon, + TrashIcon, +} from '@heroicons/react/24/outline'; +import Link from 'next/link'; + +// ... + +export function UpdateInvoice({ id }: { id: string }) { + return ( + + + + ); +} +``` + +### 2. Считайте `id` счета-фактуры из страницы `params` + +Вернитесь к компоненту `` и вставьте следующий код: + +```ts title="/app/dashboard/invoices/[id]/edit/page.tsx" +import Form from '@/app/ui/invoices/edit-form'; +import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; +import { fetchCustomers } from '@/app/lib/data'; + +export default async function Page() { + return ( +
+ +
+
+ ); +} +``` + +Обратите внимание, что она похожа на страницу `/create` счета-фактуры, только импортирует другую форму (из файла `edit-form.tsx`). Эта форма должна быть **предварительно заполнена** со значением `defaultValue` для имени клиента, суммы счета и статуса. Чтобы предварительно заполнить поля формы, необходимо получить конкретный счет-фактуру с помощью `id`. + +В дополнение к `searchParams`, компоненты страницы также принимают параметр `params`, который можно использовать для доступа к `id`. Обновите свой компонент ``, чтобы получить этот реквизит: + +```ts title="/app/dashboard/invoices/[id]/edit/page.tsx" hl_lines="5-8" +import Form from '@/app/ui/invoices/edit-form'; +import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; +import { fetchCustomers } from '@/app/lib/data'; + +export default async function Page(props: { + params: Promise<{ id: string }>; +}) { + const params = await props.params; + const id = params.id; + // ... +} +``` + +### 3. Получите конкретный счет-фактуру + +Затем: + +- Импортируйте новую функцию `fetchInvoiceById` и передайте `id` в качестве аргумента. +- Импортируйте функцию `fetchCustomers`, чтобы получить имена клиентов для выпадающего списка. + +Вы можете использовать `Promise.all` для параллельного получения данных о счетах и клиентах: + +```ts title="/dashboard/invoices/[id]/edit/page.tsx" hl_lines="4-5 13-16" +import Form from '@/app/ui/invoices/edit-form'; +import Breadcrumbs from '@/app/ui/invoices/breadcrumbs'; +import { + fetchInvoiceById, + fetchCustomers, +} from '@/app/lib/data'; + +export default async function Page(props: { + params: Promise<{ id: string }>; +}) { + const params = await props.params; + const id = params.id; + const [invoice, customers] = await Promise.all([ + fetchInvoiceById(id), + fetchCustomers(), + ]); + // ... +} +``` + +В терминале вы увидите временную ошибку TypeScript для свойства `invoice`, потому что `invoice` может быть потенциально `undefined`. Пока что не беспокойтесь об этом, вы решите эту проблему в следующей главе, когда добавите обработку ошибок. + +Отлично! Теперь проверьте, что все подключено правильно. Зайдите на сайт и нажмите на значок карандаша, чтобы отредактировать счет-фактуру. После навигации вы должны увидеть форму, в которую предварительно заносятся данные о счете: + +![Страница редактирования счетов-фактур с хлебными крошками и формой](edit-invoice-page.png) + +URL также должен быть обновлен с `id` следующим образом: `http://localhost:3000/dashboard/invoice/uuid/edit` + +!!!tip "UUID против автоинкрементных ключей" + + Мы используем UUID вместо инкрементных ключей (например, 1, 2, 3 и т. д.). Это делает URL длиннее, однако UUID исключают риск столкновения идентификаторов, являются глобально уникальными и снижают риск атак перечисления, что делает их идеальными для больших баз данных. + + Однако если вы предпочитаете более чистые URL-адреса, лучше использовать автоинкрементные ключи. + +### 4. Передача id серверному действию + +Наконец, вы хотите передать `id` серверному действию, чтобы оно могло обновить нужную запись в вашей базе данных. Вы **не можете** передать `id` в качестве аргумента, например: + +```ts title="/app/ui/invoices/edit-form.tsx" +// Passing an id as argument won't work + +``` + +Вместо этого вы можете передать `id` серверному действию с помощью JS `bind`. Это обеспечит кодировку любых значений, передаваемых серверному действию. + +```ts title="/app/ui/invoices/edit-form.tsx" hl_lines="2 11-14 16-20" +// ... +import { updateInvoice } from '@/app/lib/actions'; + +export default function EditInvoiceForm({ + invoice, + customers, +}: { + invoice: InvoiceForm; + customers: CustomerField[]; +}) { + const updateInvoiceWithId = updateInvoice.bind( + null, + invoice.id + ); + + return ( + + {/* ... */} + + ); +} +``` + +!!!note "Замечание" + + Использование скрытого поля ввода в форме также работает (например, ``). Однако значения будут отображаться в виде полного текста в HTML-источнике, что не идеально для конфиденциальных данных. + +Затем в файле `actions.ts` создайте новое действие `updateInvoice`: + +```ts title="/app/lib/actions.ts" +// Use Zod to update the expected types +const UpdateInvoice = FormSchema.omit({ + id: true, + date: true, +}); + +// ... + +export async function updateInvoice( + id: string, + formData: FormData +) { + const { + customerId, + amount, + status, + } = UpdateInvoice.parse({ + customerId: formData.get('customerId'), + amount: formData.get('amount'), + status: formData.get('status'), + }); + + const amountInCents = amount * 100; + + await sql` + UPDATE invoices + SET customer_id = ${customerId}, amount = ${amountInCents}, status = ${status} + WHERE id = ${id} + `; + + revalidatePath('/dashboard/invoices'); + redirect('/dashboard/invoices'); +} +``` + +Аналогично действию `createInvoice`, здесь вы: + +1. Извлекаем данные из `formData`. +2. Проверка типов с помощью Zod. +3. Преобразование суммы в центы. +4. Передача переменных в SQL-запрос. +5. Вызов `revalidatePath` для очистки клиентского кэша и выполнения нового запроса к серверу. +6. Вызов `redirect` для перенаправления пользователя на страницу счета-фактуры. + +Протестируйте его, отредактировав счет-фактуру. После отправки формы вы должны быть перенаправлены на страницу счета-фактуры, а счет-фактура должен быть обновлен. + +## Удаление счета-фактуры + +Чтобы удалить счет-фактуру с помощью Server Actions, оберните кнопку удаления в элемент `
` и передайте `id` серверному действию с помощью `bind`: + +```ts title="/app/ui/invoices/buttons.tsx" hl_lines="1 6-9 12" +import { deleteInvoice } from '@/app/lib/actions'; + +// ... + +export function DeleteInvoice({ id }: { id: string }) { + const deleteInvoiceWithId = deleteInvoice.bind( + null, + id + ); + + return ( + + + + ); +} +``` + +Внутри файла `actions.ts` создайте новое действие под названием `deleteInvoice`. + +```ts title="/app/lib/actions.ts" +export async function deleteInvoice(id: string) { + await sql`DELETE FROM invoices WHERE id = ${id}`; + revalidatePath('/dashboard/invoices'); +} +``` + +Поскольку это действие вызывается по пути `/dashboard/invoices`, вам не нужно вызывать `redirect`. Вызов `revalidatePath` вызовет новый запрос к серверу и перерисует таблицу. + +## Дальнейшее чтение + +В этой главе вы узнали, как использовать Server Actions для изменения данных. Вы также узнали, как использовать API `revalidatePath` для повторной проверки кэша Next.js и `redirect` для перенаправления пользователя на новую страницу. + +Для получения дополнительных знаний вы также можете прочитать о [безопасность Server Actions](https://nextjs.org/blog/security-nextjs-server-components-actions). + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/navigating-between-pages.md b/docs/libs/nextjs/app-router/navigating-between-pages.md new file mode 100644 index 0000000..d94ddba --- /dev/null +++ b/docs/libs/nextjs/app-router/navigating-between-pages.md @@ -0,0 +1,160 @@ +--- +description: В предыдущей главе вы создали макет и страницы дашборда. Теперь давайте добавим несколько ссылок, чтобы пользователи могли перемещаться между маршрутами дашборда. +--- + +# Навигация между страницами + +В предыдущей главе вы создали макет и страницы дашборда. Теперь давайте добавим несколько ссылок, чтобы пользователи могли перемещаться между маршрутами дашборда. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Как использовать компонент `next/link`. + - Как показать активную ссылку с помощью хука `usePathname()`. + - Как работает навигация в Next.js. + +## Зачем оптимизировать навигацию? + +Для ссылок между страницами традиционно используется HTML-элемент ``. В данный момент ссылки на боковую панель используют элементы ``, но обратите внимание, что происходит, когда вы переходите между страницами «Главная», «Счета» и «Клиенты» в вашем браузере. + +Вы видели? + +На каждой странице навигации происходит полное обновление страницы! + +## Компонент `` + +В Next.js вы можете использовать компонент `` для создания ссылок между страницами в вашем приложении. `` позволяет осуществлять [навигацию на стороне клиента](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#how-routing-and-navigation-works) с помощью JavaScript. + +Чтобы использовать компонент ``, откройте файл `/app/ui/dashboard/nav-links.tsx` и импортируйте компонент Link из [`next/link`](https://nextjs.org/docs/app/api-reference/components/link). Затем замените тег `` на ``: + +```ts title="/app/ui/dashboard/nav-links.tsx" hl_lines="6 16 25" +import { + UserGroupIcon, + HomeIcon, + DocumentDuplicateIcon, +} from '@heroicons/react/24/outline'; +import Link from 'next/link'; + +// ... + +export default function NavLinks() { + return ( + <> + {links.map((link) => { + const LinkIcon = link.icon; + return ( + + +

+ {link.name} +

+ + ); + })} + + ); +} +``` + +Как видите, компонент `Link` похож на использование тегов `
`, но вместо `` вы используете ``. + +Сохраните изменения и проверьте, работает ли это на вашем localhost. Теперь вы должны иметь возможность перемещаться между страницами без полного обновления. Хотя части вашего приложения отображаются на сервере, полное обновление страницы не происходит, что позволяет чувствовать себя как родное веб-приложение. Почему так? + +## Автоматическое разделение кода и предварительная выборка + +Для улучшения навигации Next.js автоматически разделяет код вашего приложения по сегментам маршрута. Это отличается от традиционного React [SPA](https://nextjs.org/docs/app/building-your-application/upgrading/single-page-applications), где браузер загружает весь код вашего приложения при начальной загрузке страницы. + +Разделение кода по маршрутам означает, что страницы становятся изолированными. Если на какой-то странице произойдет ошибка, остальная часть приложения будет работать. Кроме того, браузеру приходится разбирать меньше кода, что делает ваше приложение быстрее. + +Кроме того, в процессе работы, когда в области просмотра браузера появляются компоненты [``](https://nextjs.org/docs/api-reference/next/link), Next.js автоматически **загружают** код для связанного маршрута в фоновом режиме. К тому моменту, когда пользователь нажимает на ссылку, код целевой страницы уже будет загружен в фоновом режиме, и именно это делает переход на страницу практически мгновенным! + +Подробнее о том [как работает навигация](https://nextjs.org/docs/app/building-your-application/routing/linking-and-navigating#how-routing-and-navigation-works). + +## Паттерн: Показ активных ссылок + +Распространенным шаблоном пользовательского интерфейса является отображение активной ссылки, чтобы указать пользователю, на какой странице он находится в данный момент. Для этого необходимо получить текущий путь пользователя из URL. Next.js предоставляет хук [`usePathname()`](https://nextjs.org/docs/app/api-reference/functions/use-pathname), который можно использовать для проверки пути и реализации этого паттерна. + +Поскольку [`usePathname()`](https://nextjs.org/docs/app/api-reference/functions/use-pathname) - это хук React, вам нужно будет превратить `nav-links.tsx` в клиентский компонент. Добавьте директиву React `"use client"` в верхнюю часть файла, затем импортируйте `usePathname()` из `next/navigation`: + +```ts title="/app/ui/dashboard/nav-links.tsx" hl_lines="1 9" +'use client'; + +import { + UserGroupIcon, + HomeIcon, + DocumentDuplicateIcon, +} from '@heroicons/react/24/outline'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; + +// ... +``` + +Затем присвойте путь переменной `pathname` внутри компонента ``: + +```ts title="/app/ui/dashboard/nav-links.tsx" hl_lines="2" +export default function NavLinks() { + const pathname = usePathname(); + // ... +} +``` + +!!!note "" + + `nav-links.tsx` не является специальным файлом для Next.js - его можно назвать как угодно. Если вы переименуете его, убедитесь, что вы соответствующим образом обновили операторы импорта. + +Вы можете использовать библиотеку `clsx`, представленную в главе [CSS styling](./css-styling.md), для условного применения имен классов, когда ссылка активна. Когда `link.href` совпадает с `pathname`, ссылка должна отображаться с синим текстом и светло-голубым фоном. + +Вот финальный код для `nav-links.tsx`: + +```ts title="/app/ui/dashboard/nav-links.tsx" hl_lines="10 25-30" +'use client'; + +import { + UserGroupIcon, + HomeIcon, + DocumentDuplicateIcon, +} from '@heroicons/react/24/outline'; +import Link from 'next/link'; +import { usePathname } from 'next/navigation'; +import clsx from 'clsx'; + +// ... + +export default function NavLinks() { + const pathname = usePathname(); + + return ( + <> + {links.map((link) => { + const LinkIcon = link.icon; + return ( + + +

+ {link.name} +

+ + ); + })} + + ); +} +``` + +Сохраните и проверьте свой localhost. Теперь вы должны увидеть активную ссылку, выделенную синим цветом. + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/not-found-file.png b/docs/libs/nextjs/app-router/not-found-file.png new file mode 100644 index 0000000..8382660 Binary files /dev/null and b/docs/libs/nextjs/app-router/not-found-file.png differ diff --git a/docs/libs/nextjs/app-router/optimizing-fonts-images.md b/docs/libs/nextjs/app-router/optimizing-fonts-images.md new file mode 100644 index 0000000..d442bb4 --- /dev/null +++ b/docs/libs/nextjs/app-router/optimizing-fonts-images.md @@ -0,0 +1,259 @@ +--- +description: В предыдущей главе вы узнали, как стилизовать приложение Next.js. Давайте продолжим работу над главной страницей, добавив настраиваемый шрифт и основное изображение. +--- + +# Оптимизация шрифтов и изображений + +В предыдущей главе вы узнали, как придать стиль своему приложению Next.js. Давайте продолжим работу над главной страницей, добавив настраиваемый шрифт и основное изображение. + +!!!tip "Вот темы, которые мы рассмотрим:" + + - Как добавить пользовательские шрифты с помощью `next/font`. + - Как добавлять изображения с помощью `next/image`. + - Как оптимизируются шрифты и изображения в Next.js. + +## Зачем оптимизировать шрифты? + +Шрифты играют важную роль в дизайне сайта, но использование пользовательских шрифтов в вашем проекте может повлиять на производительность, если файлы шрифтов должны быть получены и загружены. + +[Cumulative Layout Shift](https://vercel.com/blog/how-core-web-vitals-affect-seo) - это метрика, используемая Google для оценки производительности и удобства работы с сайтом. При использовании шрифтов смещение макета происходит, когда браузер сначала отображает текст резервным или системным шрифтом, а затем меняет его на пользовательский шрифт после загрузки. Такая замена может привести к изменению размера текста, расстояния между шрифтами или макета, смещая элементы вокруг него. + +![Макет пользовательского интерфейса, показывающий начальную загрузку страницы, а затем смену макета при загрузке пользовательского шрифта](font-layout-shift.png) + +Next.js автоматически оптимизирует шрифты в приложении, когда вы используете модуль `next/font`. Он загружает файлы шрифтов во время сборки и размещает их вместе с другими статическими активами. Это означает, что когда пользователь посещает ваше приложение, не происходит дополнительных сетевых запросов к шрифтам, которые могли бы повлиять на производительность. + +## Добавление основного шрифта + +Давайте добавим пользовательский шрифт Google в ваше приложение, чтобы посмотреть, как это работает. + +В папке `/app/ui` создайте новый файл `fonts.ts`. Вы будете использовать этот файл для хранения шрифтов, которые будут использоваться во всем приложении. + +Импортируйте шрифт `Inter` из модуля `next/font/google` - это будет ваш основной шрифт. Затем укажите, какое [подмножество](https://fonts.google.com/knowledge/glossary/subsetting) вы хотите загрузить. В данном случае `'latin'`: + +```ts title="/app/layout.tsx" hl_lines="2 12" +import '@/app/ui/global.css'; +import { inter } from '@/app/ui/fonts'; + +export default function RootLayout({ + children, +}: { + children: React.ReactNode; +}) { + return ( + + + {children} + + + ); +} +``` + +Добавив `Inter` к элементу ``, шрифт будет применяться во всем приложении. Здесь вы также добавляете класс Tailwind [antialiased](https://tailwindcss.com/docs/font-smoothing), который сглаживает шрифт. Использовать этот класс необязательно, но он добавляет приятный штрих. + +Перейдите в браузер, откройте dev tools и выберите элемент `body`. Вы должны увидеть, что `Inter` и `Inter_Fallback` теперь применяются в стилях. + +## Практика: Добавление дополнительного шрифта + +Вы также можете добавить шрифты к определенным элементам вашего приложения. + +Теперь ваша очередь! В файле `fonts.ts` импортируйте вторичный шрифт `Lusitana` и передайте его элементу `

` в файле `/app/page.tsx`. В дополнение к указанию подмножества, как вы делали раньше, вы также должны указать различные **веса** шрифта. Например, `400` (нормальный) и `700` (жирный). + +Когда все будет готово, разверните фрагмент кода ниже, чтобы увидеть решение. + +!!!tip "Подсказки:" + + - Если вы не знаете, какие параметры веса передать шрифту, проверьте ошибки TypeScript в редакторе кода. + - Посетите сайт Google Fonts и найдите Lusitana, чтобы узнать, какие параметры доступны. + - Посмотрите документацию по добавлению нескольких шрифтов и полный список опций. + +???info "Раскрыть решение" + + ```ts title="/app/ui/fonts.ts" hl_lines="1 5-8" + import { Inter, Lusitana } from 'next/font/google'; + + export const inter = Inter({ subsets: ['latin'] }); + + export const lusitana = Lusitana({ + weight: ['400', '700'], + subsets: ['latin'], + }); + ``` + + --- + + ```ts title="/app/page.tsx" hl_lines="4 10" + import AcmeLogo from '@/app/ui/acme-logo'; + import { ArrowRightIcon } from '@heroicons/react/24/outline'; + import Link from 'next/link'; + import { lusitana } from '@/app/ui/fonts'; + + export default function Page() { + return ( + // ... +

+ Welcome to Acme. This is the + example for the{' '} + + Next.js Learn Course + + , brought to you by Vercel. +

+ // ... + ); + } + ``` + +Наконец, компонент `` также использует Lusitana. Он был закомментирован для предотвращения ошибок, теперь его можно не комментировать: + +```ts title="/app/page.tsx" hl_lines="7" +// ... + +export default function Page() { + return ( +
+
+ + {/* ... */} +
+
+ ); +} +``` + +Отлично, вы добавили два пользовательских шрифта в свое приложение! Далее давайте добавим основное изображение на главную страницу. + +## Зачем оптимизировать изображения? + +Next.js может обслуживать **статические активы**, такие как изображения, в папке верхнего уровня [/public](https://nextjs.org/docs/app/building-your-application/optimizing/static-assets). На файлы внутри `/public` можно ссылаться в вашем приложении. + +Используя обычный HTML, вы добавите изображение следующим образом: + +```html +Screenshots of the dashboard project showing desktop version +``` + +Однако это означает, что вам придется работать вручную: + +- Убедиться, что изображение реагирует на разные размеры экрана. +- Задать размеры изображений для разных устройств. +- Предотвратить смещение макета при загрузке изображений. +- Ленивая загрузка изображений, которые находятся вне области просмотра пользователя. + +Оптимизация изображений - это большая тема в веб-разработке, которую можно считать отдельной специализацией. Вместо того чтобы вручную выполнять эти оптимизации, вы можете использовать компонент `next/image` для автоматической оптимизации ваших изображений. + +## Компонент `` + +Компонент `` является расширением тега HTML `` и включает в себя автоматическую оптимизацию изображений, такую как: + +- Предотвращение автоматического смещения макета при загрузке изображений. +- Изменение размера изображений, чтобы избежать отправки больших изображений на устройства с меньшей областью просмотра. +- Ленивая загрузка изображений по умолчанию (изображения загружаются по мере их попадания в область просмотра). +- Использование изображений в современных форматах, таких как [WebP](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#webp) и [AVIF](https://developer.mozilla.org/en-US/docs/Web/Media/Formats/Image_types#avif_image), если браузер поддерживает их. + +## Добавление основного изображения рабочего стола + +Давайте воспользуемся компонентом ``. Если вы заглянете в папку `/public`, то увидите, что там есть два изображения: `hero-desktop.png` и `hero-mobile.png`. Эти два изображения совершенно разные, и они будут показаны в зависимости от того, является ли устройство пользователя настольным или мобильным. + +В файле `/app/page.tsx` импортируйте компонент из [next/image](https://nextjs.org/docs/api-reference/next/image). Затем добавьте изображение под комментарием: + +```ts title="/app/page.tsx" hl_lines="5 12-18" +import AcmeLogo from '@/app/ui/acme-logo'; +import { ArrowRightIcon } from '@heroicons/react/24/outline'; +import Link from 'next/link'; +import { lusitana } from '@/app/ui/fonts'; +import Image from 'next/image'; + +export default function Page() { + return ( + // ... +
+ {/* Add Hero Images Here */} + +
+ //... + ); +} +``` + +Здесь вы устанавливаете `width` в `1000` и `height` в `760` пикселей. Хорошей практикой является установка `width` и `height` ваших изображений, чтобы избежать смещения макета, они должны иметь соотношение сторон **идентичное** исходному изображению. Эти значения не являются размером изображения, которое будет отрисовано, а представляют собой размер фактического файла изображения, используемого для определения соотношения сторон. + +Вы также заметите класс `hidden`, чтобы убрать изображение из DOM на мобильных экранах, и `md:block`, чтобы показать его на настольных экранах. + +Вот как теперь должна выглядеть ваша главная страница: + +![Стилизованная главная страница с пользовательским шрифтом и основным изображением](home-page-with-hero.png) + +## Практика: Добавление основного изображения для мобильных устройств + +Теперь ваша очередь! Под основным изображением, которое вы только что добавили, добавьте еще один компонент `` для `hero-mobile.png`. + +- Это изображение должно иметь `width` в `560` и `height` в `620` пикселей. +- Оно должно быть показано на мобильных экранах и скрыто на десктопе - вы можете использовать инструменты dev, чтобы проверить, правильно ли поменялись местами десктопные и мобильные изображения. + +Когда все будет готово, разверните фрагмент кода ниже, чтобы увидеть решение. + +???info "Раскрыть решение" + + ```ts title="/app/page.tsx" hl_lines="19-25" + import AcmeLogo from '@/app/ui/acme-logo'; + import { ArrowRightIcon } from '@heroicons/react/24/outline'; + import Link from 'next/link'; + import { lusitana } from '@/app/ui/fonts'; + import Image from 'next/image'; + + export default function Page() { + return ( + // ... +
+ {/* Add Hero Images Here */} + + +
+ //... + ); + } + ``` + +Отлично! Теперь на вашей главной странице есть пользовательский шрифт и основные изображения. + +## Рекомендуемая литература + +Вы можете узнать еще много нового по этим темам, включая оптимизацию удаленных изображений и использование локальных файлов шрифтов. Если вы хотите глубже изучить шрифты и изображения, см: + +- [Image Optimization Docs](https://nextjs.org/docs/app/building-your-application/optimizing/images) +- [Font Optimization Docs](https://nextjs.org/docs/app/building-your-application/optimizing/fonts) +- [Improving Web Performance with Images (MDN)](https://developer.mozilla.org/en-US/docs/Learn/Performance/Multimedia) +- [Web Fonts (MDN)](https://developer.mozilla.org/en-US/docs/Learn/CSS/Styling_text/Web_fonts) +- [Как основные показатели веб-страниц влияют на SEO](https://vercel.com/blog/how-core-web-vitals-affect-seo) +- [Как Google обрабатывает JavaScript в процессе индексации](https://vercel.com/blog/how-google-handles-javascript-throughout-the-indexing-process) + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/partial-rendering-dashboard.png b/docs/libs/nextjs/app-router/partial-rendering-dashboard.png new file mode 100644 index 0000000..095bffb Binary files /dev/null and b/docs/libs/nextjs/app-router/partial-rendering-dashboard.png differ diff --git a/docs/libs/nextjs/app-router/recent-revenue.png b/docs/libs/nextjs/app-router/recent-revenue.png new file mode 100644 index 0000000..8ea9d40 Binary files /dev/null and b/docs/libs/nextjs/app-router/recent-revenue.png differ diff --git a/docs/libs/nextjs/app-router/route-group.png b/docs/libs/nextjs/app-router/route-group.png new file mode 100644 index 0000000..566f2fc Binary files /dev/null and b/docs/libs/nextjs/app-router/route-group.png differ diff --git a/docs/libs/nextjs/app-router/routing-solution.png b/docs/libs/nextjs/app-router/routing-solution.png new file mode 100644 index 0000000..f609ecc Binary files /dev/null and b/docs/libs/nextjs/app-router/routing-solution.png differ diff --git a/docs/libs/nextjs/app-router/sequential-parallel-data-fetching.png b/docs/libs/nextjs/app-router/sequential-parallel-data-fetching.png new file mode 100644 index 0000000..59e7b71 Binary files /dev/null and b/docs/libs/nextjs/app-router/sequential-parallel-data-fetching.png differ diff --git a/docs/libs/nextjs/app-router/server-rendering-with-streaming-chart.png b/docs/libs/nextjs/app-router/server-rendering-with-streaming-chart.png new file mode 100644 index 0000000..42364b1 Binary files /dev/null and b/docs/libs/nextjs/app-router/server-rendering-with-streaming-chart.png differ diff --git a/docs/libs/nextjs/app-router/server-rendering-with-streaming.png b/docs/libs/nextjs/app-router/server-rendering-with-streaming.png new file mode 100644 index 0000000..cbe6877 Binary files /dev/null and b/docs/libs/nextjs/app-router/server-rendering-with-streaming.png differ diff --git a/docs/libs/nextjs/app-router/setting-up-your-database.md b/docs/libs/nextjs/app-router/setting-up-your-database.md new file mode 100644 index 0000000..fb3354a --- /dev/null +++ b/docs/libs/nextjs/app-router/setting-up-your-database.md @@ -0,0 +1,96 @@ +--- +description: Прежде чем продолжить работу над дашбордом, вам понадобятся некоторые данные. В этой главе вы будете настраивать базу данных PostgreSQL из одной из интеграций Vercel с рынком +--- + +# Настройка базы данных + +Прежде чем продолжить работу над дашбордом, вам понадобятся некоторые данные. В этой главе вы будете настраивать базу данных PostgreSQL из одного из [Vercel's marketplace integrations](https://vercel.com/marketplace?category=databases). Если вы уже знакомы с PostgreSQL и предпочитаете использовать собственный поставщик баз данных, вы можете пропустить эту главу и настроить ее самостоятельно. В противном случае, давайте продолжим! + +!!!tip "Вот темы, которые мы рассмотрим" + + - Разместите свой проект на GitHub. + - Создайте учетную запись Vercel и свяжите с ней репозиторий GitHub для мгновенного предварительного просмотра и развертывания. + - Создайте и свяжите свой проект с базой данных Postgres. + - Наполните базу данных исходными данными. + +## Создайте репозиторий GitHub. + +Для начала давайте разместим ваш репозиторий на GitHub, если вы этого еще не сделали. Это облегчит настройку базы данных и развертывание. + +Если вам нужна помощь в настройке репозитория, посмотрите [это руководство на GitHub](https://help.github.com/en/github/getting-started-with-github/create-a-repo). + +!!!info "Полезно знать:" + + - Вы также можете использовать другие git-провайдеры, например GitLab или Bitbucket. + - Если вы новичок в GitHub, мы рекомендуем [GitHub Desktop App](https://desktop.github.com/) для упрощения рабочего процесса разработки. + +## Создайте учетную запись Vercel + +Посетите сайт [vercel.com/signup](https://vercel.com/signup), чтобы создать учетную запись. Выберите бесплатный тарифный план «Хобби». Выберите **Continue with GitHub**, чтобы соединить ваши аккаунты GitHub и Vercel. + +## Подключение и развертывание проекта + +Далее вы попадете на этот экран, где сможете выбрать и **импортировать** репозиторий GitHub, который вы только что создали: + +![Скриншот дашборда Vercel, показывающий экран импорта проекта со списком GitHub-репозиториев пользователя](import-git-repo.png) + +Назовите свой проект и нажмите **Deploy**. + +![Экран развертывания, показывающий поле имени проекта и кнопку развертывания](configure-project.png) + +Ура! 🎉 Теперь ваш проект развернут. + +![Экран обзора проекта, показывающий имя проекта, домен и статус развертывания](deployed-project.png) + +Подключив ваш репозиторий GitHub, при внесении изменений в вашу **главную** ветку Vercel будет автоматически развертывать ваше приложение без необходимости настройки. При открытии запросов на исправления у вас также будут [URL мгновенного предварительного просмотра](https://vercel.com/docs/deployments/environments#preview-environment-pre-production#preview-urls), которые позволят вам выявлять ошибки развертывания на ранней стадии и делиться предварительным просмотром проекта с членами команды для получения обратной связи. + +## Создание базы данных Postgres + +Чтобы создать базу данных, нажмите **Continue to Dashboard** и выберите вкладку **Storage** на дашборде вашего проекта. Выберите **Создать базу данных**. В зависимости от того, когда была создана ваша учетная запись Vercel, вы можете увидеть такие варианты, как Neon или Supabase. Выберите предпочтительного поставщика и нажмите **Продолжить**. + +![Экран Connect Store, показывающий вариант Postgres, а также конфигурации KV, Blob и Edge](create-database.png) + +Выберите регион и план хранения, если требуется. Регионом [по умолчанию](https://vercel.com/docs/functions/configuring-functions/region) для всех проектов Vercel является **Washington D.C (iad1)**, и мы рекомендуем выбрать его, если это возможно, чтобы уменьшить [задержку](https://developer.mozilla.org/en-US/docs/Web/Performance/Understanding_latency) для запросов данных. + +![Модальное окно создания базы данных, показывающее имя базы данных и регион](database-region.png) + +После подключения перейдите на вкладку `.env.local`, нажмите **Show secret** и **Copy Snippet**. Убедитесь, что вы раскрыли секреты перед копированием. + +![Вкладка .env.local, показывающая скрытые секреты базы данных](database-dashboard.png) + +Перейдите в редактор кода и переименуйте файл `.env.example` в `.env`. Вставьте в него скопированное содержимое из Vercel. + +!!!important "Важно:" + + Зайдите в файл `.gitignore` и убедитесь, что `.env` находится в игнорируемых файлах, чтобы предотвратить раскрытие секретов вашей базы данных при отправке на GitHub. + +## Посеять базу данных + +Теперь, когда ваша база данных создана, давайте загрузим в нее некоторые начальные данные. + +Мы включили API, к которому можно обратиться через браузер и который запустит скрипт посева, чтобы заполнить базу данных начальным набором данных. + +Скрипт использует **SQL** для создания таблиц и данные из файла `placeholder-data.ts` для их заполнения после создания. + +Убедитесь, что ваш локальный сервер разработки запущен с помощью `pnpm run dev` и перейдите по адресу в браузере. После завершения вы увидите в браузере сообщение «Database seeded successfully». После завершения вы можете удалить этот файл. + +!!!warning "Устранение неполадок:" + + - Перед копированием в файл `.env` обязательно раскройте секреты вашей базы данных. + - Скрипт использует `bcrypt` для хэширования пароля пользователя, если `bcrypt` не совместим с вашим окружением, вы можете обновить скрипт, чтобы использовать [`bcryptjs`](https://www.npmjs.com/package/bcryptjs) вместо него. + - Если вы столкнулись с какими-либо проблемами при загрузке базы данных и хотите запустить скрипт снова, вы можете удалить все существующие таблицы, выполнив `DROP TABLE tablename` в интерфейсе запросов к базе данных. Более подробную информацию см. в разделе [Выполнение запросов](https://nextjs.org/learn/dashboard-app/setting-up-your-database#executing-queries) ниже. Но будьте осторожны, эта команда удалит таблицы и все их данные. Это можно сделать в вашем примере, так как вы работаете с временными данными, но вы не должны выполнять эту команду в производственном приложении. + +## Выполнение запросов + +Давайте выполним запрос, чтобы убедиться, что все работает так, как ожидалось. Для запроса к базе данных мы будем использовать другой обработчик Router Handler, `app/query/route.ts`. Внутри этого файла вы найдете функцию `listInvoices()`, которая содержит следующий SQL-запрос. + +```sql +SELECT invoices.amount, customers.name +FROM invoices +JOIN customers ON invoices.customer_id = customers.id +WHERE invoices.amount = 666; +``` + +Откомментируйте файл, удалите блок `Response.json()` и перейдите по адресу в браузере. Вы должны увидеть, что возвращается счет `amount` и `name`. + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/shared-layout-page.png b/docs/libs/nextjs/app-router/shared-layout-page.png new file mode 100644 index 0000000..77bbe63 Binary files /dev/null and b/docs/libs/nextjs/app-router/shared-layout-page.png differ diff --git a/docs/libs/nextjs/app-router/shared-layout.png b/docs/libs/nextjs/app-router/shared-layout.png new file mode 100644 index 0000000..99a86f9 Binary files /dev/null and b/docs/libs/nextjs/app-router/shared-layout.png differ diff --git a/docs/libs/nextjs/app-router/static-and-dynamic-rendering.md b/docs/libs/nextjs/app-router/static-and-dynamic-rendering.md new file mode 100644 index 0000000..d538097 --- /dev/null +++ b/docs/libs/nextjs/app-router/static-and-dynamic-rendering.md @@ -0,0 +1,86 @@ +--- +description: Статический и динамический рендеринг +--- + +# Статический и динамический рендеринг + +В предыдущей главе вы получили данные для страницы обзора дашборда. Однако мы вкратце обсудили два ограничения текущей настройки: + +- Запросы данных создают непреднамеренный водопад. +- Дашборд статичен, поэтому любые обновления данных не будут отражаться на вашем приложении. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Что такое статический рендеринг и как он может повысить производительность вашего приложения. + - Что такое динамический рендеринг и когда его следует использовать. + - Различные подходы к тому, как сделать ваш дашборд динамичным. + - Смоделируйте медленную выборку данных и посмотрите, что произойдет. + +## Что такое статический рендеринг? + +При статическом рендеринге выборка и рендеринг данных происходят на сервере во время сборки (когда вы развертываете приложение) или во время [ревалидации данных](https://nextjs.org/docs/app/building-your-application/data-fetching/fetching-caching-and-revalidating#revalidating-data). + +Когда пользователь посещает ваше приложение, ему выдается кэшированный результат. У статического рендеринга есть несколько преимуществ: + +- **Быстрее веб-сайты** - Предрендеренный контент можно кэшировать и глобально распространять при развертывании на таких платформах, как Vercel. Это гарантирует, что пользователи по всему миру смогут получить доступ к содержимому вашего сайта быстрее и надежнее. +- **Снижение нагрузки на сервер** - Поскольку содержимое кэшируется, вашему серверу не нужно динамически генерировать содержимое для каждого запроса пользователя. Это позволяет снизить затраты на вычисления. +- **SEO** - Пререндеренный контент легче индексируется поисковыми системами, поскольку он уже доступен при загрузке страницы. Это может привести к повышению рейтинга в поисковых системах. + +Статический рендеринг полезен для пользовательских интерфейсов, не содержащих **данных** или **данных, которые используются всеми пользователями**, например, статические записи в блоге или страницы товаров. Он может не подойти для дашборда, содержащего персонализированные данные, которые регулярно обновляются. + +Противоположностью статического рендеринга является динамический рендеринг. + +## Что такое динамический рендеринг? + +При динамическом рендеринге контент отображается на сервере для каждого пользователя во время **запроса** (когда пользователь посещает страницу). У динамического рендеринга есть несколько преимуществ: + +- **Данные в реальном времени** - Динамический рендеринг позволяет вашему приложению отображать данные в реальном времени или часто обновляемые данные. Это идеально подходит для приложений, в которых данные часто меняются. +- **Содержимое для конкретного пользователя** - Легче обслуживать персонализированное содержимое, например дашборды или профили пользователей, и обновлять данные на основе взаимодействия с пользователем. +- Динамический рендеринг позволяет получить доступ к информации, которая может быть известна только во время запроса, например к файлам cookie или параметрам поиска URL. + +## Моделирование медленной выборки данных + +Приложение для дашборда, которое мы создаем, динамично. + +Однако остается одна проблема, о которой говорилось в предыдущей главе. Что произойдет, если один запрос данных будет выполняться медленнее, чем все остальные? + +Давайте смоделируем медленную выборку данных. В `app/lib/data.ts` откомментируйте `console.log` и `setTimeout` внутри `fetchRevenue()`: + +```ts title="/app/lib/data.ts" hl_lines="5-8 14-16" +export async function fetchRevenue() { + try { + // We artificially delay a response for demo purposes. + // Don't do this in production :) + console.log('Fetching revenue data...'); + await new Promise((resolve) => + setTimeout(resolve, 3000) + ); + + const data = await sql< + Revenue[] + >`SELECT * FROM revenue`; + + console.log( + 'Data fetch completed after 3 seconds.' + ); + + return data; + } catch (error) { + console.error('Database Error:', error); + throw new Error('Failed to fetch revenue data.'); + } +} +``` + +Теперь откройте в новой вкладке и обратите внимание на то, как долго загружается страница. В терминале вы также должны увидеть следующие сообщения: + +```sh +Fetching revenue data... +Data fetch completed after 3 seconds. +``` + +Здесь вы добавили искусственную 3-секундную задержку, чтобы имитировать медленную выборку данных. В результате теперь вся ваша страница заблокирована от отображения пользовательского интерфейса для посетителя, пока данные находятся в процессе получения. Это подводит нас к распространенной проблеме, которую приходится решать разработчикам: + +При динамическом рендеринге **ваше приложение работает настолько быстро, насколько медленнее всего вы получаете данные**. + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/app-router/streaming.md b/docs/libs/nextjs/app-router/streaming.md new file mode 100644 index 0000000..6c7f155 --- /dev/null +++ b/docs/libs/nextjs/app-router/streaming.md @@ -0,0 +1,447 @@ +--- +description: В предыдущей главе вы узнали о различных методах рендеринга в Next.js. Мы также обсудили, как медленное получение данных может повлиять на производительность вашего приложения. Давайте рассмотрим, как можно улучшить пользовательский опыт при медленных запросах данных. +--- + +# Потоковая передача + +В предыдущей главе вы узнали о различных методах рендеринга в Next.js. Мы также обсудили, как медленное получение данных может повлиять на производительность вашего приложения. Давайте рассмотрим, как можно улучшить пользовательский опыт при медленных запросах данных. + +!!!tip "Вот темы, которые мы рассмотрим" + + - Что такое потоковая передача и когда его можно использовать. + - Как реализовать потоковую передачу с помощью `loading.tsx` и `Suspense`. + - Что такое загрузочные скелетоны. + - Что такое Next.js Route Groups и когда их можно использовать. + - Где разместить границы React `Suspense` в вашем приложении. + +## Что такое потоковая передача? + +Потоковая передача - это техника передачи данных, которая позволяет разбить маршрут на более мелкие «куски» и постепенно передавать их с сервера на клиент по мере готовности. + +![Диаграмма, показывающая время при последовательном и параллельном получении данных](server-rendering-with-streaming.png) + +Потоковая передача позволяет предотвратить блокировку всей страницы медленными запросами данных. Это позволяет пользователю видеть части страницы и взаимодействовать с ними, не дожидаясь загрузки всех данных, прежде чем пользователю будет показан любой пользовательский интерфейс. + +![Диаграмма, показывающая время при последовательной и параллельной выборке данных](server-rendering-with-streaming-chart.png) + +Потоковая передача хорошо работает с компонентной моделью React, так как каждый компонент можно рассматривать как _чанк_. + +Существует два способа реализации потоковой передачи данных в Next.js: + +1. На уровне страницы, с помощью файла `loading.tsx` (который создает для вас ``). +2. На уровне компонентов, с помощью `` для более детального контроля. + +Давайте посмотрим, как это работает. + +## Потоковая передача всей страницы с помощью `loading.tsx`. + +В папке `/app/dashboard` создайте новый файл с именем `loading.tsx`: + +```ts title="/app/dashboard/loading.tsx" +export default function Loading() { + return
Loading...
; +} +``` + +Обновите , и теперь вы должны увидеть: + +![Страница дашборда с текстом 'Loading...'](loading-page.png) + +Здесь происходит несколько вещей: + +1. `loading.tsx` - это специальный файл Next.js, построенный поверх React Suspense. Он позволяет создавать резервный пользовательский интерфейс, который будет отображаться в качестве замены во время загрузки содержимого страницы. +2. Поскольку `` статичен, он показывается сразу. Пользователь может взаимодействовать с ``, пока загружается динамический контент. +3. Пользователю не нужно ждать окончания загрузки страницы, чтобы перейти на другую (это называется прерывистой навигацией). + +Поздравляем! Вы только что реализовали потоковую передачу. Но мы можем сделать еще больше для улучшения пользовательского опыта. Давайте покажем скелетон загрузки вместо текста `Loading...`. + +### Добавление скелетонов загрузки + +Загрузочный скелетон - это упрощенная версия пользовательского интерфейса. Многие веб-сайты используют их в качестве заполнителя (или запасного варианта), чтобы указать пользователям, что содержимое загружается. Любой пользовательский интерфейс, который вы добавите в `loading.tsx`, будет встроен как часть статического файла и отправлен первым. Затем остальной динамический контент будет передан с сервера на клиент. + +Внутри файла `loading.tsx` импортируйте новый компонент под названием ``: + +```ts title="/app/dashboard/loading.tsx" hl_lines="1 4" +import DashboardSkeleton from '@/app/ui/skeletons'; + +export default function Loading() { + return ; +} +``` + +Затем обновите , и теперь вы должны увидеть: + +![Дашборд со скелетами загрузки](loading-page-with-skeleton.png) + +### Исправление ошибки скелета загрузки с группами маршрутов + +Сейчас ваш загрузочный скелет будет применяться к счетам. + +Поскольку `loading.tsx` находится на уровень выше, чем `/invoices/page.tsx` и `/customers/page.tsx` в файловой системе, он также применяется к этим страницам. + +Мы можем изменить это с помощью [Route Groups](https://nextjs.org/docs/app/building-your-application/routing/route-groups). Создайте новую папку `/(overview)` внутри папки дашборда. Затем переместите файлы `loading.tsx` и `page.tsx` в эту папку: + +![Структура папки, показывающая, как создать группу маршрутов с помощью круглых скобок](route-group.png) + +Теперь файл `loading.tsx` будет применяться только к странице обзора дашборда. + +Группы маршрутов позволяют организовывать файлы в логические группы, не затрагивая структуру URL-путей. Когда вы создаете новую папку, используя круглые скобки `()`, ее название не будет включено в путь URL. Таким образом, `/dashboard/(overview)/page.tsx` превращается в `/dashboard`. + +Здесь вы используете группу маршрутов для того, чтобы `loading.tsx` применялся только к странице обзора дашборда. Однако вы также можете использовать группы маршрутов для разделения приложения на секции (например, маршруты `(маркетинг)` и маршруты `(магазин)`) или на команды для больших приложений. + +### Потоковая передача компонента + +До сих пор вы передавали всю страницу. Но вы также можете быть более детализированными и передавать определенные компоненты с помощью React Suspense. + +Suspense позволяет откладывать отрисовку частей приложения до тех пор, пока не будет выполнено какое-то условие (например, загружены данные). Вы можете обернуть свои динамические компоненты в Suspense. Затем передайте ему резервный компонент, который будет отображаться, пока динамический компонент загружается. + +Если вы помните медленный запрос данных, `fetchRevenue()`, то именно этот запрос замедляет работу всей страницы. Вместо того чтобы блокировать всю страницу, вы можете использовать Suspense для потоковой передачи только этого компонента и немедленного показа остальной части пользовательского интерфейса страницы. + +Для этого вам нужно будет переместить выборку данных в компонент, давайте обновим код, чтобы увидеть, как это будет выглядеть: + +Удалите все экземпляры `fetchRevenue()` и его данные из `/dashboard/(overview)/page.tsx`: + +```ts title="/app/dashboard/(overview)/page.tsx" hl_lines="5-8 11" +import { Card } from '@/app/ui/dashboard/cards'; +import RevenueChart from '@/app/ui/dashboard/revenue-chart'; +import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; +import { lusitana } from '@/app/ui/fonts'; +import { + fetchLatestInvoices, + fetchCardData, +} from '@/app/lib/data'; // remove fetchRevenue + +export default async function Page() { + const revenue = await fetchRevenue(); // delete this line + const latestInvoices = await fetchLatestInvoices(); + const { + numberOfInvoices, + numberOfCustomers, + totalPaidInvoices, + totalPendingInvoices, + } = await fetchCardData(); + + return ( + /* ... */ + ); +} +``` + +Затем импортируйте `` из React и оберните его вокруг ``. Вы можете передать ему компонент возврата под названием ``. + +```ts title="/app/dashboard/(overview)/page.tsx" hl_lines="9-10 51-55" +import { Card } from '@/app/ui/dashboard/cards'; +import RevenueChart from '@/app/ui/dashboard/revenue-chart'; +import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; +import { lusitana } from '@/app/ui/fonts'; +import { + fetchLatestInvoices, + fetchCardData, +} from '@/app/lib/data'; +import { Suspense } from 'react'; +import { RevenueChartSkeleton } from '@/app/ui/skeletons'; + +export default async function Page() { + const latestInvoices = await fetchLatestInvoices(); + const { + numberOfInvoices, + numberOfCustomers, + totalPaidInvoices, + totalPendingInvoices, + } = await fetchCardData(); + + return ( +
+

+ Dashboard +

+
+ + + + +
+
+ } + > + + + +
+
+ ); +} +``` + +Наконец, обновите компонент ``, чтобы получить свои собственные данные и удалить переданный ему параметр: + +```ts title="/app/ui/dashboard/revenue-chart.tsx" hl_lines="4 8-10" +import { generateYAxis } from '@/app/lib/utils'; +import { CalendarIcon } from '@heroicons/react/24/outline'; +import { lusitana } from '@/app/ui/fonts'; +import { fetchRevenue } from '@/app/lib/data'; + +// ... + +export default async function RevenueChart() { + // Make component async, remove the props + const revenue = await fetchRevenue(); // Fetch data inside the component + + const chartHeight = 350; + const { yAxisLabels, topLabel } = generateYAxis( + revenue + ); + + if (!revenue || revenue.length === 0) { + return ( +

+ No data available. +

+ ); + } + + return ( + // ... + ); +} +``` + +Теперь обновите страницу, вы должны увидеть информацию дашборда почти сразу, в то время как для `` будет показан скелет резервного копирования: + +![Страница дашборда со скелетоном графика выручки и загруженными компонентами Card и Latest Invoices](loading-revenue-chart.png) + +### Практика: Потоковая передача ``. + +Теперь ваша очередь! Отработайте то, чему вы только что научились, выполнив потоковую передачу компонента ``. + +Переместите `fetchLatestInvoices()` вниз со страницы на компонент ``. Заверните компонент в границу `` с фаллабетом ``. + +Когда все будет готово, разверните тумблер, чтобы увидеть код решения: + +???info "Reveal the solution" + + Dashboard Page: + + ```ts title="/app/dashboard/(overview)/page.tsx" hl_lines="5 9 56-60" + import { Card } from '@/app/ui/dashboard/cards'; + import RevenueChart from '@/app/ui/dashboard/revenue-chart'; + import LatestInvoices from '@/app/ui/dashboard/latest-invoices'; + import { lusitana } from '@/app/ui/fonts'; + import { fetchCardData } from '@/app/lib/data'; // Remove fetchLatestInvoices + import { Suspense } from 'react'; + import { + RevenueChartSkeleton, + LatestInvoicesSkeleton, + } from '@/app/ui/skeletons'; + + export default async function Page() { + // Remove `const latestInvoices = await fetchLatestInvoices()` + const { + numberOfInvoices, + numberOfCustomers, + totalPaidInvoices, + totalPendingInvoices, + } = await fetchCardData(); + + return ( +
+

+ Dashboard +

+
+ + + + +
+
+ } + > + + + } + > + + +
+
+ ); + } + ``` + + `` component. Remember to remove the props from the component: + + ```ts title="/app/ui/dashboard/latest-invoices.tsx" hl_lines="5 7-9" + import { ArrowPathIcon } from '@heroicons/react/24/outline'; + import clsx from 'clsx'; + import Image from 'next/image'; + import { lusitana } from '@/app/ui/fonts'; + import { fetchLatestInvoices } from '@/app/lib/data'; + + export default async function LatestInvoices() { + // Remove props + const latestInvoices = await fetchLatestInvoices(); + + return ( + // ... + ); + } + ``` + +## Группировка компонентов + +Отлично! Вы почти у цели, теперь вам нужно обернуть компоненты `` в Suspense. Вы можете получить данные для каждой отдельной карты, но это может привести к эффекту «выпрыгивания» при загрузке карт, что может визуально раздражать пользователя. + +Как же решить эту проблему? + +Чтобы создать эффект «ступенчатости», вы можете сгруппировать карточки с помощью компонента-обертки. Это означает, что сначала будет показан статический ``, затем карточки и т. д. + +В файле `page.tsx`: + +1. Удалите компоненты ``. +2. Удалите функцию `fetchCardData()`. +3. Импортируйте новый компонент **обертки** под названием ``. +4. Импортируйте новый **скелетон** компонента под названием ``. +5. Оберните `` в Suspense. + +```ts title="/app/dashboard/(overview)/page.tsx" hl_lines="1 6 18-20" +import CardWrapper from '@/app/ui/dashboard/cards'; +// ... +import { + RevenueChartSkeleton, + LatestInvoicesSkeleton, + CardsSkeleton, +} from '@/app/ui/skeletons'; + +export default async function Page() { + return ( +
+

+ Dashboard +

+
+ }> + + +
+ // ... +
+ ); +} +``` + +Затем перейдите в файл `/app/ui/dashboard/cards.tsx`, импортируйте функцию `fetchCardData()` и вызовите ее внутри компонента ``. Не забудьте откомментировать весь необходимый код в этом компоненте. + +```ts title="/app/ui/dashboard/cards.tsx" hl_lines="2 7-12" +// ... +import { fetchCardData } from '@/app/lib/data'; + +// ... + +export default async function CardWrapper() { + const { + numberOfInvoices, + numberOfCustomers, + totalPaidInvoices, + totalPendingInvoices, + } = await fetchCardData(); + + return ( + <> + + + + + + ); +} +``` + +Обновите страницу, и вы увидите, что все карты загружаются одновременно. Вы можете использовать этот шаблон, когда хотите, чтобы несколько компонентов загружались одновременно. + +## Решение о том, где расположить границы приостановки + +Место расположения границ Suspense зависит от нескольких факторов: + +1. Как вы хотите, чтобы пользователь воспринимал страницу во время ее загрузки. +2. Какому контенту вы хотите отдать предпочтение. +3. Если компоненты зависят от получения данных. + +Посмотрите на свою страницу дашборда, есть ли что-то, что вы сделали бы по-другому? + +Не волнуйтесь. Здесь нет правильного ответа. + +- Вы могли бы транслировать **всю страницу**, как мы сделали с `loading.tsx`... но это может привести к увеличению времени загрузки, если один из компонентов медленно получает данные. +- Можно транслировать **каждый компонент** по отдельности... но это может привести к тому, что пользовательский интерфейс будет «выскакивать» на экран по мере готовности. +- Можно также создать эффект _стаггирования_, передавая потоком **разделы страницы**. Но вам придется создавать компоненты-обертки. + +Место, где вы разместите границы Suspense, зависит от вашего приложения. В целом, хорошей практикой является перемещение поиска данных вниз к компонентам, которым они нужны, а затем обертывание этих компонентов в Suspense. Но нет ничего плохого в потоковой передаче секций или всей страницы, если это необходимо вашему приложению. + +Не бойтесь экспериментировать с Suspense и смотреть, что работает лучше, это мощный API, который поможет вам создать более восхитительный пользовательский опыт. + +## Заглядывая в будущее + +Потоковая передача и серверные компоненты дают нам новые способы обработки состояний получения и загрузки данных, в конечном итоге направленные на улучшение качества работы конечного пользователя. + +В следующей главе вы узнаете о Partial Prerendering, новой модели рендеринга Next.js, созданной с учетом потоковой обработки. + +:material-information-outline: Источник — diff --git a/docs/libs/nextjs/auth.md b/docs/libs/nextjs/auth.md deleted file mode 100644 index e44d731..0000000 --- a/docs/libs/nextjs/auth.md +++ /dev/null @@ -1,103 +0,0 @@ -# Аутентификация (Authentication) - -**Аутентификация** — это процесс определения того, кем является пользователь, а авторизация — процесс определения его полномочий, т. е. того, к чему пользователь имеет доступ. Next.js поддерживает несколько паттернов аутентификации. - -## Паттерны аутентификации - -Паттерн аутентификации определяет стратегию получения данных. Далее необходимо выбрать провайдера аутентификации, который поддерживает выбранную стратегию. Основных паттерна аутентификации два: - -- использование статической генерации для серверной загрузки состояния и получения данных пользователя на стороне клиента -- получение данных пользователя от сервера во избежание "вспышки" (flash) неаутентифицированного контента (имеется ввиду видимое пользователю переключение состояний приложения) - -## Аутентификация при статической генерации - -Next.js автоматически определяет, что страница является статической, если на этой странице отсутствуют блокирующие методы для получения данных. Это означает отсутствие на странице `getServerSideProps`. В этом случае на странице рендерится начальное состояние, полученное от сервера, а затем на стороне клиента запрашиваются данные пользователя. - -Одним из преимуществ использования данного паттерна является возможность доставки страниц из глобального CDN и их предварительная загрузка с помощью `next/link`. Это приводит к уменьшению времени до интерактивности (Time to Interactive, TTI). - -Рассмотрим пример страницы профиля пользователя. На этой странице сначала рендерится шаблон (скелет), а после выполнения запроса на получения данных пользователя, отображаются эти данные: - -```js -// pages/profile.js -import useUser from '../lib/useUser'; -import Layout from './components/Layout'; - -export default function Profile() { - // получаем данные пользователя на стороне клиента - const { user } = useUser({ redirectTo: '/login' }); - - // состояние загрузки, полученное от сервера - if (!user || user.isLoggedIn === false) { - return Загрузка...; - } - - // после выполнения запроса отображаются данные пользователя - return ( - -

Ваш профиль

-
{JSON.stringify(user, null, 2)}
-
- ); -} -``` - -## Аутентификация при рендеринге на стороне сервера - -Если на странице имеется асинхронная функция `getServerSideProps`, Next.js будет рендерить такую страницу при каждом запросе с использованием данных из этой функции. - -```js -export async function getServerSideProps(context) { - return { - props: {}, // будут переданы компоненту страницы как пропы - }; -} -``` - -Перепишем приведенный выше пример. При наличии сессии компонент `Profile` получит проп `user`. Обратите внимание на отсутствие шаблона: - -```js -// pages/profile.js -import withSession from '../lib/session'; -import Layout from '../components/Layout'; - -export const getServerSideProps = withSession( - async (req, res) => { - const user = req.session.get('user'); - - if (!user) { - return { - redirect: { - destination: '/login', - permanent: false, - }, - }; - } - - return { - props: { - user, - }, - }; - } -); - -export default function Profile({ user }) { - // отображаем данные пользователя, состояния загрузки не требуется - return ( - -

Ваш профиль

-
{JSON.stringify(user, null, 2)}
-
- ); -} -``` - -Преимуществом данного подхода является предотвращение вспышки неаутентифицированного контента перед выполнением перенаправления. Важно отметить, что запрос данных пользователя в `getServerSideProps` блокирует рендеринг до разрешения запроса. Поэтому во избежание создания узких мест и увеличения времени до первого байта (Time to Fist Byte, TTFB), следует убедиться в хорошей производительности сервиса аутентификации. - -## Провайдеры аутентификации - -Если у вас имеется база данных с пользователями, рассмотрите возможность использования одного из следующих решений: - -- [next-iron-session](https://github.com/vercel/next.js/tree/canary/examples/with-iron-session) — низкоуровневая закодированная сессия без состояния -- [next-auth](https://github.com/nextauthjs/next-auth-example) — полноценная система аутентификации со встроенными провайдерами (Google, Facebook, GitHub и т. д.), JWT, JWE, email/пароль, магическими ссылками и др. -- старый-добрый [passport](http://www.passportjs.org/): diff --git a/docs/libs/nextjs/basic/.pages b/docs/libs/nextjs/basic/.pages deleted file mode 100644 index d409114..0000000 --- a/docs/libs/nextjs/basic/.pages +++ /dev/null @@ -1,13 +0,0 @@ -title: Основные возможности -nav: - - pages.md - - data-fetching.md - - css.md - - layouts.md - - images.md - - fonts.md - - script.md - - static.md - - fast-refresh.md - - typescript.md - - env.md diff --git a/docs/libs/nextjs/basic/css.md b/docs/libs/nextjs/basic/css.md deleted file mode 100644 index 4b49b43..0000000 --- a/docs/libs/nextjs/basic/css.md +++ /dev/null @@ -1,154 +0,0 @@ -# Встроенная поддержка CSS - -## Импорт глобальных стилей - -Для добавления глобальных стилей соответствующую таблицу следует импортировать в файл `pages/_app.js` (обратите внимание на нижнее подчеркивание): - -```js -// pages/_app.js -import './style.css'; - -// Данный экспорт по умолчанию является обязательным -export default function App({ Component, pageProps }) { - return ; -} -``` - -Такие стили будут применяться ко всем страницам и компонентам в приложении. Обратите внимание: во избежание конфликтов глобальные стили могут импортироваться только в `pages/_app.js`. - -При сборке приложения все стили объединяются в один минифицированный CSS-файл. - -## Импорт стилей из директории node_modules - -Стили могут импортироваться из `node_modules`. - -Пример импорта глобальных стилей: - -```js -// pages/_app.js -import 'bootstrap/dist/css/bootstrap.min.css'; - -export default function App({ Component, pageProps }) { - return ; -} -``` - -Пример импорта стилей для стороннего компонента: - -```js -// components/Dialog.js -import { useState } from 'react'; -import { Dialog } from '@reach/dialog'; -import VisuallyHidden from '@reach/visually-hidden'; -import '@reach/dialog/styles.css'; - -export function MyDialog(props) { - const [show, setShow] = useState(false); - const open = () => setShow(true); - const close = () => setShow(false); - - return ( -
- - - -

Привет!

-
-
- ); -} -``` - -## Добавление стилей на уровне компонента - -Next.js из коробки поддерживает CSS-модули. CSS-модули должны иметь название `[name].module.css`. Они создают локальную область видимости для соответствующих стилей, что позволяет использовать одинаковые названия классов без риска возникновения коллизий. CSS-модуль импортируется как объект (обычно, именуемый `styles`), ключами которого являются названия соответствующих классов. - -Пример использования CSS-модулей: - -```css -/* components/Button/Button.module.css */ -.danger { - background-color: red; - color: white; -} -``` - -```js -// components/Button/Button.js -import styles from './Button.module.css'; - -export const Button = () => ( - -); -``` - -При сборке CSS-модули конкатенируются и разделяются на отдельные минифицированные CSS-файлы, что позволяет загружать только необходимые стили. - -## Поддержка SASS - -Next.js поддерживает файлы с расширением `.scss` и `.sass`. SASS также может использоваться на уровне компонентов (`.module.scss` и `.module.sass`). Для компиляции SASS в CSS необходимо установить sass: - -```sh -yarn add sass -``` - -Поведение компилятора SASS может быть кастомизировано в файле `next.config.js`, например: - -```js -const path = require('path'); - -module.exports = { - sassOptions: { - includePaths: [path.join(__dirname, 'styles')], - }, -}; -``` - -## CSS-в-JS - -В Next.js можно использовать любое решение CSS-в-JS. Простейшим примером является использование встроенных стилей: - -```js -export const Hi = ({ name }) => ( -

Привет, {name}!

-); -``` - -Next.js также имеет встроенную поддержку styled-jsx: - -```js -export const Bye = ({ name }) => ( -
-

Пока, {name}. Скоро увидимся!

- - -
-); -``` diff --git a/docs/libs/nextjs/basic/data-fetching.md b/docs/libs/nextjs/basic/data-fetching.md deleted file mode 100644 index 2453b14..0000000 --- a/docs/libs/nextjs/basic/data-fetching.md +++ /dev/null @@ -1,518 +0,0 @@ -# Получение данных (Data Fetching) - -Существует 3 функции для получения данных, необходимых для предварительного рендеринга: - -- `getStaticProps` (SSG): получение данных во время сборки -- `getStaticPaths` (SSG): определение динамических роутов для предварительного рендеринга страниц на основе данных -- `getServerSideProps` (SSR): получение данных при каждом запросе - -## getStaticProps - -Страница, на которой экспортируется асинхронная функция `getStaticProps`, предварительно рендерится с помощью возвращаемых этой функцией пропсов. - -```js -export async function getStaticProps(context) { - return { - props: {}, - }; -} -``` - -`context` — это объект со следующими свойствами: - -- `params` — параметры роута для страниц с динамической маршрутизацией. Например, если названием страницы является `[id].js`, params будет иметь вид `{ id: ... }` -- `preview` — имеет значение true, если страница находится в режиме предварительного просмотра -- `previewData` — набор данных, установленный с помощью `setPreviewData` -- `locale` — текущая локаль (если включено) -- `locales` — поддерживаемые локали (если включено) -- `defaultLocale` — дефолтная локаль (если включено) - -`getStaticProps` возвращает объект со следующими свойствами: - -- `props` — опциональный объект с пропами для страницы -- `revalidate` — опциональное количество секунд, по истечении которых происходит повторная генерация страницы. По умолчанию имеет значение `false` — повторная генерация выполняется только при следующей сборке -- `notFound` — опциональное логическое значение, позволяющее вернуть статус 404 и соответствующую страницу, например: - -```js -export async function getStaticProps(context) { - const res = await fetch('/data'); - const data = await res.json(); - - if (!data) { - return { - notFound: true, - }; - } - - return { - props: { - data, - }, - }; -} -``` - -!!! info "Обратите внимание" - - `notFound` не требуется в режиме `fallback: false`, поскольку в этом режиме предварительно рендерятся только пути, возвращаемые `getStaticPaths`. - - Также обратите внимание, что `notFound: true` означает возврат 404 даже в случае, если предыдущая страница была успешно сгенерирована. Это рассчитано на поддержку случаев удаления пользовательского контента. - -- `redirect` — опциональный объект, позволяющий выполнять перенаправления на внутренние и внешние ресурсы, который должен иметь форму `{ destination: string, permanent: boolean }`: - -```js -export async function getStaticProps(context) { - const res = await fetch('/data'); - const data = await res.json(); - - if (!data) { - return { - redirect: { - destination: '/', - permanent: false, - }, - }; - } - - return { - props: { - data, - }, - }; -} -``` - -!!! warning "" - - Замечание 1: в настоящее время перенаправления во время сборки не разрешаются. Такие перенаправления должны быть добавлены в `next.config.js`. - - Замечание 2: модули, импортируемые на верхнем уровне для использования в `getStaticProps`, не включаются в клиентскую сборку. Это означает, что серверный код, включая операции чтения из файловой системы или из базы данных, можно писать прямо в `getStaticProps`. - - Замечание 3: `fetch()` в `getStaticProps` следует использовать только при получении ресурсов из внешних источников. - -Случаи использования: - -- данные для рендеринга доступны во время сборки и не зависят от запроса пользователя -- данные приходят из безголовой (headless) CMS -- данные могут быть кешированы в открытом виде (не предназначены для конкретного пользователя) -- страница должна быть предварительно отрендерена (для SEO) и при этом должна быть очень быстрой — `getStaticProps` генерирует HTML и JSON файлы, которые могут быть кешированы с помощью CDN - -Использование с TypeScript - -```ts -import { GetStaticProps } from 'next'; - -export const getStaticProps: GetStaticProps = async ( - context -) => {}; -``` - -Для получения предполагаемых типов для пропов следует использовать `InferGetStaticPropsType`: - -```ts -import { InferGetStaticPropsType } from 'next'; - -type Post = { - author: string; - content: string; -}; - -export const getStaticProps = async () => { - const res = await fetch('/posts'); - const posts: Post[] = await res.json(); - - return { - props: { - posts, - }, - }; -}; - -export default function Blog({ - posts, -}: InferGetStaticPropsType) { - // посты будут иметь тип `Post[]` -} -``` - -### Инкрементальная статическая регенерация - -Статические страницы можно обновлять после сборки приложения. Инкрементальная статическая регенерация позволяет использовать статическую генерацию на уровне отдельных страниц без необходимости повторной сборки всего проекта. - -Пример: - -```js -const Blog = ({ posts }) => ( -
    - {posts.map((post) => ( -
  • {post.title}
  • - ))} -
-); - -// Данная функция вызывается во время сборки на сервере. -// Она может вызываться повторно как бессерверная функция -// при включенной инвалидации и поступлении нового запроса -export async function getStaticProps() { - const res = await fetch('/posts'); - const posts = await res.json(); - - return { - props: { - posts, - }, - // `Next.js` попытается регенерировать страницу: - // - при поступлении нового запроса - // - как минимум, один раз каждые 10 секунд - revalidate: 10, // в секундах - }; -} - -// Данная функция вызывается во время сборки на сервере. -// Она может вызываться повторно как бессерверная функция -// если путь не был сгенерирован предварительно -export async function getStaticPaths() { - const res = await fetch('/posts'); - const posts = await res.json(); - - // Получаем пути для предварительного рендеринга на основе постов - const paths = posts.map((post) => ({ - params: { id: post.id }, - })); - - // Только эти пути будут предварительно отрендерены во время сборки - // `{ fallback: 'blocking' }` будет рендерить страницы на сервере - // при отсутствии соответствующего пути - return { paths, fallback: 'blocking' }; -} - -export default Blog; -``` - -При запросе страницы, которая была предварительно отрендерена во время сборки, отображается кешированная страница. - -- Ответ на любой запрос к такой странице до истечения 10 секунд также мгновенно возвращается из кеша -- По истечении 10 секунд следующий запрос также получает в ответ кешированную версию страницы -- После этого в фоновом режиме запускается регенерация страницы -- После успешной регенерации кеш инвалидируется и отображается новая страница. При провале регенерации старая страница остается неизменной - -### Чтение файлов - -Для получения абсолютного пути к текущей рабочей директории следует использовать `process.cwd()`: - -```js -import { promises as fs } from 'fs'; -import { join } from 'path'; - -const Blog = ({ posts }) => ( -
    - {posts.map((post) => ( -
  • -

    {post.filename}

    -

    {post.content}

    -
  • - ))} -
-); - -// Данная функция вызывается на сервере, так что -// в ней можно напрямую обращаться к БД -export async function getStaticProps() { - const postsDir = join(process.cwd(), 'posts'); - const filenames = await fs.readdir(postsDir); - - const posts = filenames.map(async (filename) => { - const filePath = join(postsDir, filename); - const fileContent = await fs.readFile( - filePath, - 'utf-8' - ); - - // Обычно, здесь выполняется преобразование контента, - // например, разбор `markdown` в `HTML` - - return { - filename, - content: fileContent, - }; - }); - - return { - props: { - posts: await Promise.all(posts), - }, - }; -} -export default Blog; -``` - -### Технические подробности - -- Поскольку `getStaticProps` запускается во время сборки, она не может использовать данные из запроса, такие как параметры строки запроса (query params) или HTTP-заголовки (headers) -- `getStaticProps` запускается только на сервере, поэтому ее нельзя использовать для обращения к внутренним роутам -- при использовании `getStaticProps` генерируется не только HTML, но и файл в формате JSON. Данный файл содержит результаты выполнения `getStaticProps` и используется механизмом маршрутизации на стороне клиента для передачи пропов компонентам -- `getStaticProps` может использовать только в компоненте-странице. Это объясняется тем, что все данные, необходимые для рендеринга страницы, должны быть доступными -- в режиме для разработки `getStaticProps` вызывается при каждом запросе -- режим предварительного просмотра (preview mode) используется для рендеринга страницы при каждом запросе - -## getStaticPaths - -Страницы с динамической маршрутизацией, из которых экспортируется асинхронная функция `getStaticPaths`, будут предварительно сгенерированы для всех путей, возвращаемых этой функцией. - -```js -export async function getStaticPaths() { - return { - paths: [(params: {})], - fallback: true | false | 'blocking', - }; -} -``` - -### Ключ paths - -`paths` определяет, какие пути будут предварительно отрендерены. Например, если у нас имеется страница с динамической маршрутизацией, которая называется `pages/posts/[id].js`, и экспортируемая на этой странице `getStaticPaths` возвращает такой `paths`: - -```js -return { - paths: [{ params: { id: '1' } }, { params: { id: '2' } }], -}; -``` - -Тогда будут статически сгенерированы страницы `posts/1` и `posts/2` на основе компонента `pages/posts/[id].js`. - -Обратите внимание, что название каждого `params` должно совпадать с параметрами, используемыми на странице: - -- если названием страницы является `pages/posts/[postId]/[commentId]`, тогда `params` должен содержать `postId` и `commentId` -- если на странице используется перехватчик роутов, например, `pages/[...slug]`, `params` должен содержать `slug` в виде массива. Например, если такой массив будет выглядеть как `['foo', 'bar']`, то будет сгенерирована страница `/foo/bar` -- если на странице используется опциональный перехватчик роутов, применение `null`, `[]`, `undefined` или `false`, приведет к рендерингу роута верхнего уровня. Например, при применении `slug: false` к `pages/[[...slug]]`, будет сгенерирована страница `/` - -### Ключ fallback - -Если `fallback` имеет значение `false`, отсутствующий путь будет разрешаться страницей 404. - -Если `fallback` имеет значение `true`, поведение `getStaticProps` будет таким: - -- пути из `getStaticPaths` будут сгенерированы во время сборки с помощью `getStaticProps` - отсутствующий путь не будет разрешаться страницей 404. Вместо этого в ответ на запрос будет возвращена резервная страница -- в фоновом режиме выполняется генерация запрошенного HTML и JSON. Это включает в себя вызов `getStaticProps` -- браузер получает JSON для сгенерированного пути. Этот JSON используется для автоматического рендеринга страницы с обязательными пропами. Со стороны пользователя это выглядит как переключение между резервной и полной страницами -- новый путь добавляется в список предварительно отрендеренных страниц - -Обратите внимание: `fallback: true` не поддерживается при использовании `next export`. - -### Резервные страницы - -В резервной версии страницы: - -- пропы страницы будут пустыми -- определить, что рендерится резервная страница, можно с помощью роутера: `router.isFallback` будет иметь значение `true` - -```js -// pages/posts/[id].js -import { useRouter } from 'next/router'; - -function Post({ post }) { - const router = useRouter(); - - // Если страница еще не сгенерирована, будет отображаться это - // До тех пор, пока `getStaticProps` не закончит свою работу - if (router.isFallback) { - return
Загрузка...
; - } - - // рендеринг поста -} - -export async function getStaticPaths() { - return { - paths: [ - { params: { id: '1' } }, - { params: { id: '2' } }, - ], - fallback: true, - }; -} - -export async function getStaticProps({ params }) { - const res = await fetch(`/posts/${params.id}`); - const post = await res.json(); - - return { - props: { - post, - }, - revalidate: 1, - }; -} - -export default Post; -``` - -### В каких случаях может быть полезен `fallback: true`? - -`fallback: true` может быть полезен при очень большом количестве статических страниц, которые зависят от данных (например, очень большой интернет-магазин). Мы хотим предварительно рендерить все страницы, но понимаем, что сборка будет длиться целую вечность. - -Вместо этого мы генерируем небольшой набор статических страниц и используем `fallback: true` для остальных. При запросе отсутствующей страницы пользователь какое-то время будет наблюдать индикатор загрузки (пока `getStaticProps` делает свое дело), затем увидит саму страницу. И после этого новая страница будет возвращаться в ответ на каждый запрос. - -!!! warning "Обратите внимание" - - `fallback: true` не обновляет сгенерированные страницы. Для этого используется инкрементальная статическая регенерация. - -Если `fallback` имеет значение `blocking`, отсутствующий путь также не будет разрешаться страницей 404, но и перехода между резервной и нормальной страницами не будет. Вместо этого запрашиваемая страница будет сгенерирована на сервере и отправлена браузеру, а пользователь после некоторого ожидания сразу увидит готовую страницу - -### Случаи использования `getStaticPaths` - -`getStaticPaths` используется для предварительного рендеринга страниц с динамической маршрутизацией. - -### Использование с TypeScript - -```ts -import { GetStaticPaths } from 'next'; - -export const getStaticPaths: GetStaticPaths = async () => {}; -``` - -### Технические подробности - -- `getStaticPaths` должна использоваться совместно с `getStaticProps`. Она не может использоваться вместе с `getServerSideProps` -- `getStaticPaths` запускается только на сервере во время сборки -- `getStaticPaths` может экспортироваться только в компоненте-странице -- в режиме для разработки `getStaticPaths` запускается при каждом запросе - -## getServerSideProps - -Страница, из которой экспортируется асинхронная функция `getServerSideProps`, будет рендерится при каждом запросе с помощью возвращаемых этой функцией пропов. - -```js -export async function getServerSideProps(context) { - return { - props: {}, - }; -} -``` - -`context` — это объект со следующими свойствами: - -- `params`: см. `getStaticProps` -- `req`: объект HTTP `IncomingMessage` (входящее сообщение, запрос) -- `res`: объект HTTP-ответа -- `query`: объектное представление строки запроса -- `preview`: см. `getStaticProps` -- `previewData`: см. `getStaticProps` -- `resolveUrl`: нормализованная версия запрашиваемого URL, из которой удален префикс `_next/data` и включены значения оригинальной строки запроса -- `locale`: см. `getStaticProps` -- `locales`: см. `getStaticProps` -- `defaultLocale`: см. `getStaticProps` - -`getServerSideProps` должна возвращать объект с такими полями: - -- `props` — см. `getStaticProps` -- `notFound` — см. `getStaticProps` - -```js -export async function getServerSideProps(context) { - const res = await fetch('/data'); - const data = await res.json(); - - if (!data) { - return { - notFound: true, - }; - } - - return { - props: {}, - }; -} -``` - -- `redirect` — см. `getStaticProps` - -```js -export async function getServerSideProps(context) { - const res = await fetch('/data'); - const data = await res.json(); - - if (!data) { - return { - redirect: { - destination: '/', - permanent: false, - }, - }; - } - - return { - props: {}, - }; -} -``` - -Для `getServerSideProps` характерны те же особенности и ограничения, что и для `getStaticProps`. - -### Случаи использования getServerSideProps - -`getServerSideProps` следует использовать только при необходимости предварительного рендеринга страницы на основе данных, зависящих от запроса. - -### Использование getServerSideProps с TypeScript - -```ts -import { GetServerSideProps } from 'next'; - -export const getServerSideProps: GetServerSideProps = async () => {}; -``` - -Для получения предполагаемых типов для пропов следует использовать `InferGetServerSidePropsType`: - -```ts -import { InferGetServerSidePropsType } from 'next'; - -type Data = {}; - -export async function getServerSideProps() { - const res = await fetch('/data'); - const data = await res.json(); - - return { - props: { - data, - }, - }; -} - -function Page({ - data, -}: InferGetServerSidePropsType) { - // ... -} - -export default Page; -``` - -### Технические подробности - -- `getServerSideProps` запускается только на сервере -- `getServerSideProps` может экспортироваться только в компоненте-странице - -## Получение данных на стороне клиента - -Если на странице имеются часто обновляемые данные, но страница не нуждается в предварительном рендеринге (по соображениям, связанным с SEO), тогда можно запрашивать такие данные на стороне клиента. - -Команда Next.js рекомендует использовать для этого разработанный ими хук `useSWR`, который предоставляет такие возможности, как кеширование данных, инвалидация кеша, отслеживание фокуса, периодическое выполнение повторных запросов и т. д. - -```js -import useSWR from 'swr'; - -const fetcher = (url) => - fetch(url).then((res) => res.json()); - -function Profile() { - const { data, error } = useSWR('/api/user', fetcher); - - if (error) - return
При загрузке данных возникла ошибка
; - if (!data) return
Загрузка...
; - - return
Привет, {data.name}!
; -} -``` diff --git a/docs/libs/nextjs/basic/env.md b/docs/libs/nextjs/basic/env.md deleted file mode 100644 index 0af82da..0000000 --- a/docs/libs/nextjs/basic/env.md +++ /dev/null @@ -1,58 +0,0 @@ -# Переменные среды окружения - -Next.js имеет встроенную поддержку переменных среды окружения, что позволяет делать следующее: - -- использовать `.env.local` для загрузки переменных -- экстраполировать переменные в браузер с помощью префикса `NEXT_PUBLIC_` - -Предположим, что у нас имеется такой файл `.env.local`: - -``` -DB_HOST=localhost -DB_USER=myuser -DB_PASS=mypassword -``` - -Это приведет к автоматической загрузке `process.env.DB_HOST`, `process.env.DB_USER` и `process.env.DB_PASS` в среду выполнения Node.js, позволяя использовать их в методах получения данных и интерфейсе маршрутизации: - -```js -// pages/index.js -export async function getStaticProps() { - const db = await myDB.connect({ - host: process.env.DB_HOST, - username: process.env.DB_USER, - password: process.env.DB_PASS, - }); - - // ... -} -``` - -Next.js позволяет использовать переменные внутри файлов `.env`: - -``` -HOSTNAME=localhost -PORT=8080 -HOST=http://$HOSTNAME:$PORT -``` - -Для того, чтобы передать переменную среды окружения в браузер к ней нужно добавить префикс `NEXT_PUBLIC_`: - -``` -NEXT_PUBLIC_ANALYTICS_ID=abcdefghijk -``` - -```js -// pages/index.js -import setupAnalyticsService from '../lib/my-analytics-service'; - -setupAnalyticsService(process.env.NEXT_PUBLIC_ANALYTICS_ID); - -function HomePage() { - return

Привет, народ!

; -} - -export default HomePage; -``` - -В дополнение к `.env.local` можно создавать файлы `.env` (для обоих режимов), `.env.development` (для режима разработки) и `.env.production` (для производственного режима). Обратите внимание: `.env.local` всегда имеет приоритет над другими файлами, содержащими переменные среды окружения. diff --git a/docs/libs/nextjs/basic/fast-refresh.md b/docs/libs/nextjs/basic/fast-refresh.md deleted file mode 100644 index c562021..0000000 --- a/docs/libs/nextjs/basic/fast-refresh.md +++ /dev/null @@ -1,5 +0,0 @@ -# Обновление в режиме реального времени - -Next.js поддерживает обновление компонентов в режиме реального времени с сохранением локального состояния в большинстве случаев (это относится только к функциональным компонентам и хукам). Состояние компонента также сохраняется при возникновении ошибок (не связанных с рендерингом). - -Для перезагрузки компонента достаточно в любом месте добавить `// @refresh reset`. diff --git a/docs/libs/nextjs/basic/fonts.md b/docs/libs/nextjs/basic/fonts.md deleted file mode 100644 index 57e62c7..0000000 --- a/docs/libs/nextjs/basic/fonts.md +++ /dev/null @@ -1,82 +0,0 @@ -# Оптимизация шрифтов - -Next.js автоматически встраивает шрифты в CSS во время сборки: - -```html -// было - - -// стало - -``` - -Для добавления на страницу шрифта используется компонент `Head`, импортируемый из `next/head`: - -```js -// pages/index.js -import Head from 'next/head'; - -export default function IndexPage() { - return ( -
- - - -

Привет, народ!

-
- ); -} -``` - -Для добавления шрифта в приложение следует создать кастомный документ: - -```js -// pages/_document.js -import Document, { - Html, - Head, - Main, - NextScript, -} from 'next/document'; - -class MyDoc extends Document { - render() { - return ( - - - - - -
- - - - ); - } -} -``` - -Автоматическую оптимизацию шрифтов можно отключить: - -```js -// next.config.js -module.exports = { - optimizeFonts: false, -}; -``` diff --git a/docs/libs/nextjs/basic/images.md b/docs/libs/nextjs/basic/images.md deleted file mode 100644 index e9c1959..0000000 --- a/docs/libs/nextjs/basic/images.md +++ /dev/null @@ -1,70 +0,0 @@ -# Компонент Image и оптимизация изображений - -Компонент `Image`, импортируемый из `next/image`, является расширением HTML-тега [`img`](https://hcdev.ru/html/img/), предназначенным для современного веба. Он включает несколько встроенных оптимизаций, позволяющих добиться хороших показателей Core Web Vitals. Эти оптимизации включают в себя следующее: - -- улучшение производительности -- обеспечение визуальной стабильности -- ускорение загрузки страницы -- обеспечение гибкости (масштабируемости) изображений - -Пример использования локального изображения: - -```js -import Image from 'next/image'; -import imgSrc from '../public/some-image.png'; - -export default function Home() { - return ( - <> -

Главная страница

- - - ); -} -``` - -Пример использования удаленного изображения: - -!!! warning "" - - Обратите внимание на необходимость установки ширины и высоты изображения - -```js -import Image from 'next/image'; - -export default function Home() { - return ( - <> -

Главная страница

- - - ); -} -``` - -## Определение размеров изображения - -`Image` ожидает получение ширины и высоты изображения: - -- в случае статического импорта (локальное изображение) ширина и высота вычисляются автоматически -- ширина и высота могут указываться с помощью соответствующих пропов -- если размеры изображения неизвестны, можно использовать проп `layout` со значением `fill` - -Существует 3 способа решения проблемы неизвестных размеров изображения: - -- использование режима макетирования `fill`: этот режим позволяет управлять размерами изображения с помощью родительского элемента. В этом случае размеры родительского элемента определяются с помощью CSS, а размеры изображения с помощью свойств [`object-fit`](https://hcdev.ru/css/object-fit/) и [`object-position`](https://hcdev.ru/css/object-position/): -- нормализация изображений: если источник изображений находится под нашим контролем, мы можем добавить изменение размеров изображения при его возвращении в ответ на запрос -- модификация вызовов к API: в ответ на запрос может включаться не только само изображение, но и его размеры - -Правила стилизации изображений: - -- выбирайте правильный режим макетирования -- используйте `className` — он устанавливается соответствующему элементу `img`. Обратите внимание: проп `style` не передается -- при использовании `layout="fill"` родительский элемент должен иметь `position: relative` -- при использовании `layout="responsive"` родительский элемент должен иметь `display: block` diff --git a/docs/libs/nextjs/basic/layouts.md b/docs/libs/nextjs/basic/layouts.md deleted file mode 100644 index 2c1b6e6..0000000 --- a/docs/libs/nextjs/basic/layouts.md +++ /dev/null @@ -1,144 +0,0 @@ -# Макеты (Layouts) - -Разработка React-приложения предполагает разделение страницы на отдельные компоненты. Многие компоненты используются на нескольких страницах. Предположим, что на каждой странице используется панель навигации и подвал: - -```js -// components/layout.js -import Navbar from './navbar'; -import Footer from './footer'; - -export default function Layout({ children }) { - return ( - <> - -
{children}
-
- - ); -} -``` - -## Примеры - -### Единственный макет - -Если в приложении используется только один макет, мы можем создать кастомное приложение (custom app) и обернуть приложение в макет. Поскольку компонент-макет будет переиспользоваться при изменении страниц, его состояние (например, значения инпутов) будет сохраняться: - -```js -// pages/_app.js -import Layout from '../components/layout'; - -export default function App({ Component, pageProps }) { - return ( - - - - ); -} -``` - -### Макеты на уровне страниц - -Свойство `getLayout` страницы позволяет возвращать компонент для макета. Это позволяет определять макеты на уровне страниц. Возвращаемая функция позволяет конструировать вложенные макеты: - -```js -// pages/index.js -import Layout from '../components/layout'; -import Nested from '../components/nested'; - -export default function Page() { - return { - // ... - }; -} - -Page.getLayout = (page) => ( - - {page} - -); -``` - -```js -// pages/_app.js -export default function App({ Component, pageProps }) { - // использовать макет, определенный на уровне страницы, при наличии такового - const getLayout = Component.getLayout || ((page) => page); - - return getLayout(); -} -``` - -При переключении страниц состояние каждой из них (значения инпутов, позиция прокрутки и т. п.) будет сохраняться. - -### Использование с TypeScript - -При использовании TypeScript сначала создается новый тип для страницы, включающей `getLayout`. Затем следует создать новый тип для `AppProps`, который перезаписывает свойство `Component` для обеспечения возможности использования созданного ранее типа: - -```ts -// pages/index.tsx -import type { ReactElement } from 'react'; -import Layout from '../components/layout'; -import Nested from '../components/nested'; - -export default function Page() { - return { - // ... - }; -} - -Page.getLayout = (page: ReactElement) => ( - - {page} - -); -``` - -```ts -// pages/_app.tsx -import type { ReactElement, ReactNode } from 'react'; -import type { NextPage } from 'next'; -import type { AppProps } from 'next/app'; - -type NextPageWithLayout = NextPage & { - getLayout?: (page: ReactElement) => ReactNode; -}; - -type AppPropsWithLayout = AppProps & { - Component: NextPageWithLayout; -}; - -export default function App({ - Component, - pageProps, -}: AppPropsWithLayout) { - const getLayout = Component.getLayout ?? ((page) => page); - - return getLayout(); -} -``` - -### Получение данных - -Данные в макете можно получать на стороне клиента с помощью `useEffect` или утилит вроде `SWR`. Поскольку макет — это не страница, в нем в настоящее время нельзя использовать `getStaticProps` или `getServerSideProps`: - -```js -import useSWR from 'swr'; -import Navbar from './navbar'; -import Footer from './footer'; - -export default function Layout({ children }) { - const { data, error } = useSWR('/data', fetcher); - - if (error) return
Ошибка
; - if (!data) return
Загрузка...
; - - return ( - <> - -
{children}
-
- - ); -} -``` diff --git a/docs/libs/nextjs/basic/pages.md b/docs/libs/nextjs/basic/pages.md deleted file mode 100644 index 3261284..0000000 --- a/docs/libs/nextjs/basic/pages.md +++ /dev/null @@ -1,146 +0,0 @@ -# Страницы (Pages) - -**Страница** — это компонент React, который экспортируется из файла с расширением `.js`, `.jsx`, `.ts` или `.tsx`, находящегося в директории `pages`. Каждая страница ассоциируется с маршрутом (роутом) по названию. Например, страница `pages/about.js` будет доступна по адресу `/about`. Обратите внимание, что страница должна экспортироваться по умолчанию (`export default`): - -```js -export default function About() { - return
О нас
; -} -``` - -Маршрут для страницы `pages/posts/[id].js` будет динамическим, т. е. такая страница будет доступна по адресам `posts/1`, `posts/2` и т. д. - -По умолчанию все страницы рендерятся предварительно (pre-rendering). Это приводит к лучшей производительности и SEO. Каждая страница ассоциируется с минимальным количеством JS. При загрузке страницы запускается JS-код, который делает ее интерактивной (данный процесс называется гидратацией — hydration). - -Существует 2 формы предварительного рендеринга: генерация статической разметки (static generation, SSG, рекомендуемый подход) и рендеринг на стороне сервера (server-side rendering, SSR). Первая форма предусматривает генерацию HTML во время сборки и его повторное использование при каждом запросе. Вторая — генерацию разметки при каждом запросе. Генерация статической разметки является рекомендуемым подходом по причинам производительности. - -Кроме этого можно использовать рендеринг на стороне клиента (client-side rendering), когда определенные части страницы рендерятся клиентским JS. - -## SSG - -Генерироваться могут как страницы с данными, так и страницы без данных. - -Без данных - -```js -export default function About() { - return
О нас
; -} -``` - -Существует 2 возможных сценария, при которых может потребоваться генерация статической страницы с данными: - -1. Контент страницы зависит от внешних данных: используется `getStaticProps` -2. Пути (`paths`) страницы зависят от внешних данных: используется `getStaticPaths` (как правило, совместно с `getStaticProps`) - -### Контент страницы зависит от внешних данных - -Предположим, что страница блога получает список постов от CMS: - -```js -// TODO: запрос `posts` - -export default function Blog({ posts }) { - return ( -
    - {posts.map((post) => ( -
  • {post.title}
  • - ))} -
- ); -} -``` - -Для получения данных, необходимых для предварительного рендеринга, из файла должна экспортироваться асинхронная функция `getStaticProps`. Данная функция вызывается во время сборки и позволяет передавать полученные данные странице в виде `props`: - -```js -export default function Blog({ posts }) { - // ... -} - -export async function getStaticProps() { - const posts = await ( - await fetch('https://example.com/posts') - )?.json(); - - // обратите внимание на сигнатуру - return { - props: { - posts, - }, - }; -} -``` - -### Пути страницы зависят от внешних данных - -Для обработки предварительного рендеринга статической страницы, пути которой зависят от внешних данных, из динамической страницы (например, `pages/posts/[id].js`) должна экспортироваться асинхронная функция `getStaticPaths`. Данная функция вызывается во время сборки и позволяет определить пути для пререндеринга: - -```js -export default function Post({ post }) { - // ... -} - -export async function getStaticPaths() { - const posts = await ( - await fetch('https://example.com/posts') - )?.json(); - - // обратите внимание на структуру возвращаемого массива - const paths = posts.map((post) => ({ - params: { id: post.id }, - })); - - // `fallback: false` означает, что для ошибки 404 используется другой маршрут - return { - paths, - fallback: false, - }; -} -``` - -На странице `pages/posts/[id].js` также должна экспортироваться функция `getStaticProps` для получения данных поста с указанным `id`: - -```js -export default function Post({ post }) { - // ... -} - -export async function getStaticPaths() { - // ... -} - -export async function getStaticProps({ params }) { - const post = await ( - await fetch(`https://example.com/posts/${params.id}`) - ).json(); - - return { - props: { - post, - }, - }; -} -``` - -## SSR - -Для обработки рендеринга страницы на стороне сервера из файла должна экспортироваться асинхронная функция `getServerSideProps`. Данная функция будет вызываться при каждом запросе страницы. - -```js -function Page({ data }) { - // ... -} - -export async function getServerSideProps() { - const data = await ( - await fetch('https://example.com/data') - )?.json(); - - return { - props: { - data, - }, - }; -} -``` diff --git a/docs/libs/nextjs/basic/script.md b/docs/libs/nextjs/basic/script.md deleted file mode 100644 index 35a7554..0000000 --- a/docs/libs/nextjs/basic/script.md +++ /dev/null @@ -1,117 +0,0 @@ -# Компонент Script - -Компонент `Script` позволяет разработчикам определять приоритет загрузки сторонних скриптов, что экономит время и улучшает производительность. - -Приоритет загрузки скрипта определяется с помощью пропа `strategy`, который принимает одно из следующих значений: - -- `beforeInteractive`: предназначено для важных скриптов, которые должны быть загружены и выполнены до того, как страница станет интерактивной. К таким скриптам относятся, например, обнаружение ботов и запрос разрешений. Такие скрипты внедряются в первоначальный HTML и запускаются перед остальным JS -- `afterInteractive`: для скриптов, которые могут загружаться и выполняться после того, как страница стала интерактивной. К таким скриптам относятся, например, менеджеры тегов и аналитика. Такие скрипты выполняются на стороне клиента и запускаются после гидратации -- `lazyOnload`: для скриптов, которые могут быть загружены в период простоя. К таким скриптам относятся, например, поддержка чатов и виджеты социальных сетей - -!!! warning "Обратите внимание" - - `Script` поддерживает встроенные скрипты со стратегиями `afterInteractive` и `lazyOnload` - -встроенные скрипты, обернутые в `Script`, должны иметь атрибут `id` для их отслеживания и оптимизации - -## Примеры - -!!! warning "Обратите внимание" - - Компонент `Script` не должен помещаться внутрь компонента `Head` или кастомного документа. - -Загрузка полифилов - -```js -import Script from 'next/script'; - -export default function Home() { - return ( - <> - - -// или - - - - + + {% set title = social.name %} + {% if not title and "//" in social.link %} + {% set _, url = social.link.split("//") %} + {% set title = url.split("/")[0] %} + {% endif %} + + {% include ".icons/" ~ social.icon ~ ".svg" %} + + {% endfor %} + diff --git a/requirements.txt b/requirements.txt index c50982d..97be250 100644 --- a/requirements.txt +++ b/requirements.txt @@ -3,14 +3,15 @@ Jinja2 livereload Markdown MarkupSafe +PyYAML +six +tornado +pillow +cairosvg mkdocs pymdown-extensions mkdocs-material mkdocs-awesome-pages-plugin mkdocs-minify-plugin mkdocs-glightbox -PyYAML -six -tornado -pillow -cairosvg +mkdocs-video diff --git a/runtime.txt b/runtime.txt index 2c07333..24ee5b1 100644 --- a/runtime.txt +++ b/runtime.txt @@ -1 +1 @@ -3.11 +3.13 diff --git a/scripts/__pycache__/build_xstate_migration_ru.cpython-314.pyc b/scripts/__pycache__/build_xstate_migration_ru.cpython-314.pyc new file mode 100644 index 0000000..0110dc5 Binary files /dev/null and b/scripts/__pycache__/build_xstate_migration_ru.cpython-314.pyc differ diff --git a/scripts/build_xstate_migration_ru.py b/scripts/build_xstate_migration_ru.py new file mode 100644 index 0000000..964e09b --- /dev/null +++ b/scripts/build_xstate_migration_ru.py @@ -0,0 +1,335 @@ +#!/usr/bin/env python3 +""" +Скачивает MDX из statelyai/docs (migration / cheatsheet), конвертирует в MkDocs Markdown +и переводит прозу на русский (код в fenced blocks не трогаем). + + python scripts/build_xstate_migration_ru.py # migration.md + python scripts/build_xstate_migration_ru.py cheatsheet # cheatsheet.md + python scripts/build_xstate_migration_ru.py cheatsheet --no-translate +""" +from __future__ import annotations + +import re +import sys +import textwrap +import time +import urllib.request +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +DOCS_BASE = "https://raw.githubusercontent.com/statelyai/docs/main/content/docs" + +MIGRATION_URL = f"{DOCS_BASE}/migration.mdx" +MIGRATION_OUT = ROOT / "docs/libs/xstate.5/migration.md" +LINK_MAP_MIGRATION = { + r"](actors)": "](actors.md)", + r"](input)": "](input.md)", + r"](system)": "](system.md)", + r"](persistence)": "](persistence.md)", + r"](output)": "](output.md)", + r"](inspection)": "](inspection.md)", + r"](studio)": "](https://stately.ai/docs/studio)", + r"](xstate-vscode-extension)": "](https://marketplace.visualstudio.com/items?itemName=statelyai.stately-vscode)", +} + +CHEATSHEET_URL = f"{DOCS_BASE}/cheatsheet.mdx" +CHEATSHEET_OUT = ROOT / "docs/libs/xstate.5/cheatsheet.md" +LINK_MAP_CHEATSHEET = { + r"](installation)": "](xstate.md)", + r"](actor-model)": "](actor-model.md)", + r"](/docs/actors#actors-as-promises)": "](actors.md#frompromise)", + r"](/docs/actors#fromtransition)": "](actors.md#fromtransition)", + r"](/docs/actors#fromobservable)": "](actors.md#fromobservable)", + r"](/docs/actors#fromcallback)": "](actors.md#fromcallback)", + r"](parent-states)": "](parent-states.md)", + r"](actions)": "](actions.md)", + r"](guards)": "](guards.md)", + r"](invoke)": "](invoke.md)", + r"](spawn)": "](spawn.md)", + r"](input)": "](input.md)", + r"](input.mdx#invoking-actors-with-input)": "](input.md)", +} + +# обратная совместимость +URL = MIGRATION_URL +OUT = MIGRATION_OUT +LINK_MAP = LINK_MAP_MIGRATION + +TAB_LABEL_RU = { + "XState v5": "XState v5", + "XState v4": "XState v4", + "XState v5 (context)": "XState v5 (контекст)", + "XState v4 arguments": "XState v4 (аргументы)", + "XState v4 function": "XState v4 (функция)", +} + + +def fetch_url(url: str) -> str: + with urllib.request.urlopen(url, timeout=120) as r: + return r.read().decode("utf-8") + + +def fetch_source() -> str: + return fetch_url(URL) + + +def apply_link_map(s: str, link_map: dict[str, str]) -> str: + for a, b in link_map.items(): + s = s.replace(a, b) + s = s.replace( + "](../../blog/2023-12-01-xstate-v5)", + "](https://stately.ai/blog/2023-12-01-xstate-v5)", + ) + return s + + +def strip_code_directives(s: str) -> str: + s = re.sub(r"```(\w+) twoslash", r"```\1", s) + s = re.sub(r"^\s*// \[!code highlight:\d+\]\s*\n", "", s, flags=re.MULTILINE) + return s + + +def replace_callouts(s: str) -> str: + def one_callout(m: re.Match[str]) -> str: + typ = (m.group("type") or "").strip() + body = m.group("body").strip() + body_ind = textwrap.indent(body, " ") + if typ == "warning": + title = "Критическое изменение" + if body == "Breaking change": + body_ind = " Несовместимое изменение." + return f'!!! warning "{title}"\n\n{body_ind}\n\n' + return f'!!! note "Примечание"\n\n{body_ind}\n\n' + + pat = re.compile( + r"[^\"]+)\")?\s*>\s*(?P.*?)\s*", + re.DOTALL, + ) + return pat.sub(one_callout, s) + + +def parse_tab_label(open_tag: str) -> str: + m = re.search(r'label="([^"]*)"', open_tag) + if not m: + return "Tab" + raw = m.group(1) + return TAB_LABEL_RU.get(raw, raw) + + +def find_tabs_block_end(s: str, start: int) -> int: + idx = start + depth = 0 + while idx < len(s): + if s.startswith(""): + depth += 1 + gt = s.find(">", idx) + if gt == -1: + return len(s) + idx = gt + 1 + continue + if s.startswith("", idx): + depth -= 1 + idx += len("") + if depth == 0: + return idx + continue + idx += 1 + return len(s) + + +def replace_tabs(s: str) -> str: + out: list[str] = [] + i = 0 + while True: + start = s.find("") + last_close = block.rfind("") + if first_gt == -1 or last_close == -1: + out.append(block) + i = end + continue + inner = block[first_gt + 1 : last_close].strip() + parts: list[tuple[str, str]] = [] + pos = 0 + while pos < len(inner): + tm = re.search(r"]*>", inner[pos:]) + if not tm: + break + label = parse_tab_label(tm.group(0)) + cstart = pos + tm.end() + tend = inner.find("", cstart) + if tend == -1: + parts.append((label, inner[cstart:])) + break + parts.append((label, inner[cstart:tend])) + pos = tend + len("") + tabbed: list[str] = [] + for label, content in parts: + tabbed.append(f'=== "{label}"\n') + c = content.strip("\n") + if c: + tabbed.append("\n") + tabbed.append(c) + tabbed.append("\n\n") + out.append("".join(tabbed)) + i = end + return "".join(out) + + +def protect_lines(text: str) -> tuple[str, list[str]]: + vault: list[str] = [] + lines = text.split("\n") + out: list[str] = [] + for line in lines: + if line.startswith("=== ") or re.match(r"^!!!\s+\w+\s+", line): + vault.append(line) + out.append(f"@@V{len(vault) - 1}@@") + else: + out.append(line) + return "\n".join(out), vault + + +def restore_lines(text: str, vault: list[str]) -> str: + for i, v in enumerate(vault): + text = text.replace(f"@@V{i}@@", v) + return text + + +def protect_frontmatter(text: str) -> tuple[str, str | None]: + m = re.match(r"^---\n.*?\n---\n", text, re.DOTALL) + if not m: + return text, None + fm = m.group(0) + return text[len(fm) :], fm + + +def _translate_blob(tr: object, blob: str) -> str: + if not blob.strip(): + return blob + ends_nl = blob.endswith("\n") + t = tr.translate(blob.rstrip("\n")) # type: ignore[union-attr] + if ends_nl and not t.endswith("\n"): + t += "\n" + return t + + +def translate_markdown(text: str, target: str = "ru") -> str: + from deep_translator import GoogleTranslator + + tr = GoogleTranslator(source="en", target=target) + text, vault = protect_lines(text) + buf: list[str] = [] + buf_len = 0 + out_chunks: list[str] = [] + for line in text.split("\n"): + piece = line + "\n" + if buf_len + len(piece) > 4200 and buf: + out_chunks.append(_translate_blob(tr, "".join(buf))) + time.sleep(0.08) + buf = [piece] + buf_len = len(piece) + else: + buf.append(piece) + buf_len += len(piece) + if buf: + out_chunks.append(_translate_blob(tr, "".join(buf))) + time.sleep(0.08) + return restore_lines("".join(out_chunks), vault) + + +def repair_translated_mkdocs(s: str) -> str: + """Восстанавливает разметку после машинного перевода (переносы, ссылки).""" + s = re.sub(r"\] +\(", "](", s) + s = re.sub(r"```===", "```\n\n===", s) + s = re.sub(r"```###", "```\n\n###", s) + s = re.sub(r"```## ", "```\n\n## ", s) + s = re.sub(r"```\*\*", "```\n\n**", s) + s = re.sub(r"\n ```-\s*", "\n```\n\n- ", s) + # пункт списка сразу после закрывающего ```bash (без пустой строки) + s = re.sub(r"(обязательно)\.\n```bash", r"\1.\n\n```bash", s) + # закрывающие ``` слиплись с русским абзацем + s = re.sub(r"```([\u0400-\u04FF])", r"```\n\n\1", s) + # закрывающие ``` слиплись со ссылкой [... + s = re.sub(r"```\[", "```\n\n[", s) + # заголовок ## сразу после блока кода + s = re.sub(r"```\n## ", "```\n\n## ", s) + return s + + +def translate_chunks(text: str, target: str = "ru") -> str: + try: + from deep_translator import GoogleTranslator # noqa: F401 + except ImportError: + print("Установите: pip install deep-translator", file=sys.stderr) + sys.exit(1) + + parts = re.split(r"(```[\s\S]*?```)", text) + out: list[str] = [] + for part in parts: + if part.startswith("```"): + out.append(part) + continue + rest, fm = protect_frontmatter(part) + if fm: + out.append(fm) + part_tr = translate_markdown(rest, target=target) + out.append(part_tr) + else: + out.append(translate_markdown(part, target=target)) + return "".join(out) + + +def main() -> None: + skip_translate = "--no-translate" in sys.argv + cheatsheet = "cheatsheet" in sys.argv[1:] + + if cheatsheet: + url = CHEATSHEET_URL + out = CHEATSHEET_OUT + link_map = LINK_MAP_CHEATSHEET + title_pat = r"^---\s*\ntitle:\s*'Cheatsheet'\s*\n---" + title_sub = "---\ntitle: Шпаргалка XState v5\n---" + else: + url = MIGRATION_URL + out = MIGRATION_OUT + link_map = LINK_MAP_MIGRATION + title_pat = r"^---\s*\ntitle:\s*'Migrating from XState v4 to v5'\s*\n---" + title_sub = "---\ntitle: Миграция с XState v4 на v5\n---" + + raw = fetch_url(url) + raw = apply_link_map(raw, link_map) + raw = strip_code_directives(raw) + raw = replace_callouts(raw) + raw = replace_tabs(raw) + if cheatsheet: + raw = raw.replace( + "## Creating transition logic\n", + "## Creating transition logic {#creating-transition-logic}\n", + 1, + ) + raw = re.sub(title_pat, title_sub, raw, count=1, flags=re.MULTILINE) + if skip_translate: + final = raw + else: + final = repair_translated_mkdocs(translate_chunks(raw)) + if cheatsheet and "{#creating-transition-logic}" not in final: + final = re.sub( + r"^(## [^\n]*переход[^\n]*логик[^\n]*)\n", + r"\1 {#creating-transition-logic}\n", + final, + count=1, + flags=re.MULTILINE, + ) + out.parent.mkdir(parents=True, exist_ok=True) + out.write_text(final, encoding="utf-8") + print("Wrote", out, "chars", len(final), "(no-translate)" if skip_translate else "") + + +if __name__ == "__main__": + main()
+ + +