NEST-NEXT: Best Practices — Часть 2

—

от автора

Это вторая часть статьи о применении комбинации технологий nest.js и NEXT.js. В первой части был создан и настроен проект, а также выбран способ отправки данных для SSR, в результате чего проект уже удовлетворял большинство потребностей при разработке простого сайта. В этой статье можно узнать о том, как выжать максимум пользы из nest-next: HMR, CDN, удобный SSR и разворачивание «за слешом».

Оглавление

Вступление

В данной статье можно узнать про продвинутые механики в nest-next и про то, как их можно применять с пользой. Напомню, что в первой части статьи уже был создан и настроен проект, поверх которого будут производиться изменения здесь. Готовое приложение доступно в репозитории на GitHub — https://github.com/yakovlev-alexey/nest-next-example — последовательность коммитов в целом совпадает с ходом статьи.

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

Разворачивание на CDN

Начнем с простого. NEXT.js «из коробки» поддерживает разворачивание на CDN. Для этого достаточно добавить assetPrefix в конфигурацию next.config.js при сборке. После ее завершения нужно загрузить содержимое ./.next/static на CDN — сервер NEXT.js автоматически будет подключать нужные ресурсы оттуда. Важным моментом будет то, что assetPrefix должен присутствовать в конфигурации и при запуске продакшен сервера nest.

HMR и кэширование инстанса сервера NEXT.js

В текущий момент приложение в режиме разработки работает быстро, перекомпиляция скорее всего занимает не более 3 секунд, а страницы грузятся почти мгновенно. Здорово, если сборка будет оставаться такой же быстрой по мере развития проекта, но это маловероятно. Особенно неприятной проблемой может стать раздутый клиент: при каждом перезапуске сервера nest (при изменении любого файла на сервере) создается новый сервер NEXT.js, который заново собирает клиентский бандл. Решение простое: нужно подключить HMR в nest и кэшировать инстанс сервера NEXT.js. Начнем по порядку.

Подключение HMR в nest

Проследуем официальному рецепту по добавлению Hot Reload в nest из документации. Я не буду копировать в статью все содержимое рецепта, но обращу внимание на некоторые детали.

Так, нам не сгодится имеющаяся команда запуска сервера, так как она будет пытаться использовать базовый tsconfig.json, в то время как мы хотим использовать tsconfig.server.json. Отразим это в команде запуска:

// ./package.json "start:dev": "nest build --webpack --webpackPath webpack-hmr.config.js --path tsconfig.server.json --watch"

Следует обратить внимание на то, что название передается в сыром виде: ts-loader в таком случае будет искать этот файл рекурсивно в родительских папках. Если бы мы передали относительный путь, то пришлось бы указывать его относительно исполняемого файла сервера.

Далее взглянем на подключение Hot Reload в main.ts.

// ./src/server/main.ts import { NestFactory } from  '@nestjs/core'; import { PORT } from  'src/shared/constants/env'; import { AppModule } from  './app.module';  declare const module: any;  async function bootstrap() {   const app = await NestFactory.create(AppModule);   await app.listen(PORT);    if (module.hot) {     module.hot.accept();     module.hot.dispose(() => app.close());   } } bootstrap();

Здесь все в порядке, но обратим внимание на то, как мы обрабатываем очистку при завершении работы старого модуля: мы вызываем метод app.close() — тем самым полностью закрываем все приложения и все сервисы.

Проверим работу сервера после подключения HMR. Запустим сервер и попробуем изменить что-то в app.controller.ts. Важным моментом является то, что сервер не перезапустится, если в результате компиляции не было обнаружено изменений. Если же изменениия были, то время запуска сервера должно упасть до ~1-2 секунд в маленьких проектах и не более 4-5 секунд в больших — существенная разница, которая сильно улучшает качество жизни разработчикам.

Если какие-то модули на сервере nest использовали динамический import/require, то велика вероятность, что после подключения Webpack HMR этот код перестанет работать из-за бандлинга. В таком случае, есть вариант запускать компилятор TypeScript параллельно с работой Webpack nest. Результаты компиляции лучше класть в отдельную папку (например, build) и указать при запуске сервера переменную окружения NODE_PATH=./build, чтобы динамический импорт модулей искал их в нужном месте.

Кэширование инстанса сервера NEXT.js

Тем не менее, у нас остается большой ботлнек при перезапуске в виде пересоздания сервера NEXT.js. В разросшихся проектах это может занимать несколько секунд при инициализации сервера и десятки секунд при первой загрузке страницы — не хочется ждать такое время просто из-за изменения символа в каком-то контроллере на сервере nest.

Чтобы побороть эту проблему, будем кэшировать RenderModule из nest-next между перезагрузками модуля app.module.ts. Для этого сменим инициализацию AppModule на динамическую.

// ./src/server/app.module.ts import { DynamicModule, Module } from '@nestjs/common'; import Next from 'next'; import { RenderModule } from 'nest-next'; import { NODE_ENV } from 'src/shared/constants/env'; import { AppController } from './app.controller'; import { AppService } from './app.service';  declare const module: any;  @Module({}) export class AppModule {     public static initialize(): DynamicModule {         /* При инициализации модуля попробуем извлечь инстанс RenderModule             из персистентных данных между перезагрузками модуля */         const renderModule =             module.hot?.data?.renderModule ??             RenderModule.forRootAsync(Next({ dev: NODE_ENV === 'development' }), {                 viewsDir: null,             });                  if (module.hot) {             /* При завершении работы старого модуля                 будем кэшировать инстанс RenderModule */             module.hot.dispose((data: any) => {                 data.renderModule = renderModule;             });         }                  return {             module: AppModule,             imports: [renderModule],             controllers: [AppController],             providers: [AppService],         };     } }

Обновим main.ts: теперь для подключения AppModule нам нужно вызывать метод initialize().

// ./src/server/main.ts import { NestFactory } from '@nestjs/core'; import { AppModule } from './app.module';  const app = await NestFactory.create(AppModule.initialize());

Чтобы убедиться в корректной работе наших улучшений, последим за выводом в терминал при перезагрузке сервера nest. В случае успеха мы больше не увидим надпись compiling... при перезагрузках — это значит, что кэш сборки NEXT.js не сбрасывается и мы выиграли еще больше времени для разработки без ожидания сборки.

Передача данных клиенту при SSR

Изначально перед нами стоял вопрос: как с наименьшими потерями избавиться от необходимости поднимать два сервиса для работы nest и NEXT.js — кажется, удалось избавиться от всех неудобств, которые могла принести провязка nest-next. Теперь же хочется получить реальные преимущества от такого взаимодействия.

Ранее мы отказались от варианта передачи данных, необходимых для страницы, на клиент с помощью возвращаемого значения функции-контроллера. Мы также обнаружили, что с помощью интерсепторов мы можем удобным образом подкладывать данные во все запросы на страницы в nest, не ломая клиентские переходы. В голову приходит мысль: а почему бы не использовать интерсепторы, чтобы помещать на клиент какие-то общие данные, необходимые для всех страниц? Это могут быть различные конфигурации, флаги, переводы и так далее.

Действительно, давайте этим займемся.

Создание конфига

Для начала создадим простенький конфиг — не будем разрабатывать отдельные модуль или сервис nest, а обойдемся простым файлом. В конфиг поместим фичефлаги, которые будем обрабатывать на клиенте. В качестве первого фичефлага возьмем blog_link — будем менять отображение ссылки на блог на главной странице в зависимости от состояния этого флага.

// ./src/server/config.ts const CONFIG = {     features: {         blog_link: true,     }, };  export { CONFIG };

Создадим интерсептор ConfigInterceptor для подкладывания конфига в возвращаемое значение контроллеров и подключим его.

// ./src/server/config.interceptor.ts import {     Injectable,     NestInterceptor,     ExecutionContext,     CallHandler, } from '@nestjs/common'; import { Observable } from 'rxjs'; import { map } from 'rxjs/operators'; import { CONFIG } from './config';  @Injectable() export class ConfigInterceptor implements NestInterceptor {     intercept(context: ExecutionContext, next: CallHandler): Observable<unknown> {         return next.handle().pipe(             map((data) => ({                 ...data,                 config: CONFIG,             })),         );     } }
// ./src/server/app.controller.ts import { ConfigInterceptor } from  './config.interceptor';  // ...  @Get('/') @Render('index') @UseInterceptors(ParamsInterceptor, ConfigInterceptor) public home() {     return {}; }  @Get(':id') @Render('[id]') @UseInterceptors(ParamsInterceptor, ConfigInterceptor) public blogPost() {     return {}; }

Для проверки работы интерсептора можем добавить console.log(ctx.query) в GSSP.

Не следует помещать в возвращаемое значение скрытую информацию вроде токенов, адресов и так далее — ctx.query так же сериализуется и при отправке на клиент. Следовательно, не рекомендуется отправлять через ctx.query в GSSP NEXT.js в том числе и объемные данные — для этого можно использовать объект запроса req из Express.

AOP для GSSP

Теперь необходимо добавить одну и ту же обработку для всех методов GSSP — конфиг нужно поместить в возвращаемое значение этого метода. Создадим обертку buildServerSideProps для этого.

// ./src/client/ssr/buildServerSideProps.ts import { ParsedUrlQuery } from  'querystring'; import { Config } from  'src/shared/types/config'; import {     GetServerSideProps,     GetServerSidePropsContext, } from  'src/shared/types/next';    type StaticProps = {     features: Config['features']; };    type StaticQuery = {     config: Config; };    const buildServerSideProps = <P, Q extends ParsedUrlQuery = ParsedUrlQuery>(     getServerSideProps: (ctx: GetServerSidePropsContext<Q>) => Promise<P>, ): GetServerSideProps<Partial<StaticProps> & P, Partial<StaticQuery> & Q> => {     return async (ctx) => {         const { features } = ctx.query.config || {};                    const props = await getServerSideProps(ctx);                    return {             props: {                 ...props,                 features,             },         };     }; };  export { buildServerSideProps };

Как можно заметить, для удобства мы получаем в параметры не полноценный метод GSSP, а его упрощенную версию, которая возвращает сразу поле props, не вложенное в объект. Это лишает нас возможности возвращать поле redirect. Если возникает такая необходимость, поменяйте метод buildServerSideProps соответствующим образом.

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

// ./src/shared/types/config.ts export type Config = {     features: Record<string, boolean>; };  // ./src/server/config.ts import type { Config } from 'src/shared/types/config';  const CONFIG: Config = {     features: {         blog_link: true,     }, };

Также нас не устраивает встроенный в NEXT.js тип GetServerSideProps — нельзя перезаписать тип Query, он всегда должен удовлетворять требованию ParsedUrlQuery. Добавим собственные тайпинги.

// ./src/shared/types/next.ts import {     GetServerSidePropsResult,     GetServerSidePropsContext as GetServerSidePropsContextBase, } from  'next'; import { ParsedUrlQuery } from  'querystring';    export type GetServerSidePropsContext<Q = ParsedUrlQuery> = Omit<     GetServerSidePropsContextBase,     'query' > & { query: Q };    export type GetServerSideProps<P, Q = ParsedUrlQuery> = (     ctx: GetServerSidePropsContext<Q>, ) => Promise<GetServerSidePropsResult<P>>;

Обновим наши страницы, чтобы они использовали эту обертку.

// ./src/pages/index.tsx export const getServerSideProps = buildServerSideProps<THomeProps>(async () => {     const blogPosts = await fetch('/api/blog-posts');           return { blogPosts }; });  // ./src/pages/[id].tsx export const getServerSideProps = buildServerSideProps<TBlogProps, TBlogQuery>(     async (ctx) => {         const id = ctx.query.id;                    const post = await fetch(`/api/blog-posts/${id}`);                    return { post };     }, );

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

Доступ к контексту приложения

Уже сейчас мы можем получить доступ к features из наших страниц. Но это может быть неудобно — чтобы получить доступ к данным о фиче-флаге или другой конфигурации в каком-то компоненте, нам придется применить prop-drilling. Чтобы этого избежать, создадим контекст для данных приложения.

// ./src/shared/types/app-data.ts import { Config } from './config';  export type AppData = {     features: Config['features']; };
// ./src/client/ssr/appData.ts import { createContext } from 'react'; import { AppData } from 'src/shared/types/app-data';  const AppDataContext = createContext<AppData>({} as AppData);  export { AppDataContext };

Чтобы не повторять подключение контекста в каждом экране, вынесем эту логику в компонент _app.tsx. Для удобства я реализую этот компонент через класс, но это необязательно.

// ./src/pages/_app.tsx import NextApp, { AppProps } from 'next/app'; import { AppDataContext } from 'src/client/ssr/appData'; import { AppData } from 'src/shared/types/app-data';  class App extends NextApp<AppProps> {     appData: AppData;          constructor(props: AppProps) {         super(props);                  this.appData = props.pageProps.appData || {};     }      render() {         const { Component, pageProps } = this.props;              return (             <AppDataContext.Provider value={this.appData}>                 <Component {...pageProps} />             </AppDataContext.Provider>         );     } }  export default App;

Немного поменяем buildServerSideProps:

// ./src/client/ssr/buildServerSideProps.ts import { AppData } from 'src/shared/types/app-data';  // ...  type StaticProps = {     appData: Partial<AppData>; };  // ...  return {     props: {         ...props,         appData: {             features,         },     }, };

И вынесем доступ к AppDataContext в отдельный хук.

// ./src/client/ssr/useAppData.ts import { useContext } from 'react'; import { AppDataContext } from './appData';  const useAppData = () => {     return useContext(AppDataContext); };  export { useAppData };

Доступ к частям контекста

Наконец, реализуем хук useFeature и применим его на Home странице.

// ./src/client/hooks/useFeature.ts import { useAppData } from 'src/client/ssr/useAppData';  const useFeature = (feature: string, defaultValue = false) => {     return useAppData().features[feature] || defaultValue; };  export { useFeature };  // ./src/pages/index.tsx const Home: FC<THomeProps> = ({ blogPosts }) => {     const linkFeature = useFeature('blog_link');          return (         <div>             <h1>Home</h1>             {blogPosts.map(({ title, id }) => (                 <div key={id}>                     {linkFeature ? (                         <>                             {title}                             <Link href={`/${id}`}> Link</Link>                         </>                     ) : (                         <Link href={`/${id}`}>{title}</Link>                     )}                 </div>             ))}         </div>     ); };

Проверим в браузере: теперь по адресу localhost:3000 мы должны увидеть измененный формат ссылки — надпись Link рядом будет ссылкой, в то время как название просто текстом. Изменим значение в конфиге, дождемся перезапуска сервера и повторим эксперимент — теперь мы видим старый формат списка.

Работа клиентских переходов

Внимательный разработчик после добавления такой возможности попробует осуществить клиентский переход: ведь мы сильно поменяли метод GSSP, нужно проверить его работу. Опасения подтвердятся — браузер перезагружает страницу при переходе, а в терминале сервера ошибка: невозможно сериализовать поле appData.features — оно ожидаемо приходит undefined при клиентском переходе. Связано это с тем, что контроллер nest не вызывается, соответственно, интерсептор не подкладывает значение конфига в GSSP.

Применяйте такое прокидывание данных в GSSP только для SSR, чтобы разово разместить данные на клиенте, а в методах GSSP не закладывайтесь на их присутствие в запросе. Такими данными могут быть фиче-флаги, переводы, прочие конфиги.

Чтобы избавиться от такой ошибки, обернем возвращаемое значение buildServerSideProps в рекурсивный метод очистки несериализуемых значений.

// ./src/client/ssr/filterUnserializable.ts const filterUnserializable = (     obj: Record<string, unknown>,     filteredValues: unknown[] = [undefined], ): Record<string, unknown> => {     return Object.keys(obj).reduce<Record<string, unknown>>((ret, key) => {         if (typeof obj[key] === 'object' && obj[key] !== null) {             return {                 ...ret,                 [key]: filterUnserializable(obj[key] as Record<string, unknown>),             };         } else if (!filteredValues.includes(obj[key])) {             return { ...ret, [key]: obj[key] };         }                  return ret;     }, {}); };  export { filterUnserializable };  // ./src/client/ssr/buildServerSideProps import  {  filterUnserializable  }  from  './filterUnserializable';  // ...  return {     props: {         ...(await getServerSideProps(ctx)),         appData: filterUnserializable({ features }) as StaticProps['appData'],     }, }; 

Убедимся в работе переходов — действительно, теперь все в порядке.

Возможность легко передавать на клиент арбитрарные данные для работы приложения является одним из самых сильных преимуществ при работе с nest-next. При этом источником этих данных может служить что угодно, в том числе значения, которые express-middleware подкладывает в req, что может быть удобно при разработке в энтерпрайзе с уже имеющимися решениями. Изменения в фиче-флагах/переводах/конфигурации, вносимые в CMS, могут быть отображены на клиенте почти мгновенно.

Разворачивание «за слешом»

Предположим, что разрабатываемый сервис в продакшене разворачивается «за слешом». Например, мы делаем документацию для какого-то проекта и сервис будет находится за прокси с адресом /docs. Или, перекладывая на текущее приложение — отдел с блогом — нашим префиксом будет /blog.

Что нужно сделать, чтобы поддержать такую возможность? Необходимо добавить префикс во все переходы (ссылки), а также к запросам к серверу, но только с клиента (не в GSSP). Статика будет лежать на CDN, для нас не будет это проблемой. Кажется, все это в наших силах, у нас даже есть механизм подкладывания данных на клиент при SSR.

Но тут мы вспоминаем, что NEXT.js при переходах делает запрос во внутренний эндпоинт, который исполняет GSSP на сервере и возвращает сериализованные данные для следующей страницы. На этом наши полномочия заканчиваются, клиентские переходы будут неизбежно сломаны. А если мы не будем пользоваться CDN, то сломается и вся статика — это совершенно не годится.

В документации NEXT.js мы обнаружим параметр basePath, который «из коробки» добавляет всем клиентским переходам необходимый префикс, а также добавляет его во все запросы внутренним эндпоинтам сервера NEXT.js. Отлично, план есть, приступаем.

Создание proxy для разработки

Прежде чем мы начнем добавлять поддержку basePath в проект, нам следует написать простенький proxy-сервер, чтобы проверить работу нашего сайта. Воспользуемся Docker и nginx. Создадим нужный конфиг для nginx.

# ./nginx.conf server {     listen 8080;      location /blog/ {         proxy_pass http://localnode:3000/;         proxy_set_header Host $host;         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;         proxy_set_header X-Real-IP $remote_addr;     } } 

Важно понимать, что proxy будет запускаться в Docker-контейнере с собственной сетью, поэтому для проксирования запроса к нашему серверу нам потребуется адрес хост-машины: будем помещать его в контейнер через имя localnode.

В скрипты в package.json добавим скрипт start:proxy для запуска контейнера со следующей командой:

docker run --name nest-next-example-proxy \     -v $(pwd)/nginx.conf:/etc/nginx/conf.d/default.conf:ro \     --add-host localnode:$(ifconfig en0 | grep inet | grep -v inet6 | awk '{print $2}') \     -p 8080:8080 \     -d nginx

Добавление basePath в конфигурацию

Добавим поддержку basePath в конфигурацию сервера. Обновим типы и будем брать значение из переменной окружения BASE_PATH.

// ./src/shared/types/config.ts export type Config = {     features: Record<string, boolean>;     basePath: string; };  // ./src/server/config.ts import { Config } from 'src/shared/types/config';  const CONFIG: Config = {     features: {         blog_link: true,     },     basePath: process.env.BASE_PATH || '', };  export { CONFIG };

Создадим next.config.js — файл конфигурации NEXT.js. Поместим туда такое же поле.

// ./next.config.js module.exports = {     basePath: process.env.BASE_PATH, };

Proxy и basePath

Проверим работу сервиса. Перезапустим сервер разработки и вновь зайдем на localhost:8080/blog. Снова запрос доходит до сервера, но не удается запросить статику NEXT.js. Сервер NEXT.js ожидает, что запрос придет с соответствующим basePath в начале req.url. Мы же при проксировании отрезаем эту часть запроса. Добавим отдельное правило для проксирования запросов на /blog/_next без перезаписи адреса запроса.

server {     listen 8080;          location /blog/_next/ {         proxy_pass http://localnode:3000;         proxy_set_header Host $host;         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;         proxy_set_header X-Real-IP $remote_addr;     }          location /blog/ {         proxy_pass http://localnode:3000/;         proxy_set_header Host $host;         proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;         proxy_set_header X-Real-IP $remote_addr;     } } 

Перезапускаем контейнер Docker и снова проверяем сервис в браузере. К большому сожалению, опять не работает. Здесь мы встречаем ограничение реализации модуля nest-next.

Проблема связана с тем, что nest-next устанавливает фильтр nest, который перенаправляет необработанные контроллерами запросы обработчикам NEXT.js. В исходном коде фильтра вызывается обработчик ошибки во всех запросах, которые не начинаются на /_next. Получается, сервер NEXT.js ожидает запрос, начинающийся на basePath, а nest-next проксирует только запросы, начинающиеся на /_next.

Мною был открыт PR на добавление поддержки basePath в nest-next. Он был вмерджен автором пакета, но новая версия не была собрана. Пока нет версии, можно загрузить собранную версию с GitHub следующим способом.

yarn upgrade nest-next@https://github.com/yakovlev-alexey/nest-next/tarball/base-path-dist

Этот тег base-path-dist содержит в себе собранный пакет только с необходимыми файлами.

После обновления версии пакета проверим в браузере вебсайт: наконец-то мы видим знакомую страницу Home уже по адресу localhost:8080/blog. Проверим переход — тоже работает!

Обертка над fetch

Остается только добавлять basePath к запросам из fetch. Сейчас может показаться, что в этом нет необходимости, можно просто использовать уже имеющуюся обертку над fetch в GSSP, а в остальных случаях обычный fetch. Но что, если логика запросов находится в стейт-менеджере и может вызываться как на сервере, так и на клиенте? В таком случае нам действительно нужно в зависимости от среды исполнения выполнять дополнительные проверки на адрес запроса.

Первым делом, немного отрефакторим buildServerSideProps. Обновим тайпинги для AppData и вынесем метод по извлечению appData из ctx.query в отдельный метод.

// ./src/shared/types/app-data.ts import { Config } from './config';  export type AppData = Pick<Config, 'basePath' | 'features'>;  // ./src/client/ssr/extractAppData.ts import { GetServerSidePropsContext } from 'src/shared/types/next'; import { AppData } from 'src/shared/types/app-data'; import { filterUnserializable } from './filterUnserializable'; import { StaticQuery } from './buildServerSideProps';    const extractAppData = (     ctx: GetServerSidePropsContext<Partial<StaticQuery>>, ) => {     const { features, basePath } = ctx.query.config || {};            return filterUnserializable({ features, basePath }) as Partial<AppData>; };    export { extractAppData };

Подключим новый хелпер в buildServerSideProps.

// ./src/client/ssr/buildServerSideProps.ts import { extractAppData } from './extractAppData';  // ...  const buildServerSideProps = <P, Q extends ParsedUrlQuery = ParsedUrlQuery>(     getServerSideProps: (ctx: GetServerSidePropsContext<Q>) => Promise<P>, ): GetServerSideProps<Partial<StaticProps> & P, Partial<StaticQuery> & Q> => {     return async (ctx) => {         const props = await getServerSideProps(ctx);                  return {             props: {                 ...props,                 appData: extractAppData(ctx),             },         };     }; };  export { buildServerSideProps };

Наконец, у нас есть доступ к basePath на клиенте. Осталось добавить поддержку этого значения в fetch. Я не буду заморачиваться с хитрым решением, а просто превращу наш envAwareFetch из чистой функции в функцию с побочными эффектами. Отобразим изменения в fetch.ts.

// ./src/shared/utils/fetch.ts import { isServer, PORT } from '../constants/env';  type FetchContext = {     basePath: string; };  const context: FetchContext = {     basePath: '', };  const initializeFetch = (basePath: string) => {     context.basePath = basePath; };  const getFetchUrl = (url: string) => {     if (isServer) {         // на сервере не нужно добавлять basePath - запрос делается не через proxy         return url.startsWith('/') ? `http://localhost:${PORT}${url}` : url;     }          return url.startsWith('/') ? context.basePath + url : url; };  const envAwareFetch = (url: string, options?: Partial<RequestInit>) => {     const fetchUrl = getFetchUrl(url);          return fetch(fetchUrl, options).then((res) => res.json()); };  export { envAwareFetch as fetch, initializeFetch }; 

Остается только инициализировать fetch с помощью initializeFetch. Кажется, что это можно было бы сделать в GSSP, но этот метод исполняется только на сервере, а нам необходимо добавлять basePath как раз на клиенте, поэтому в качестве места вызова метода я выбрал конструктор _app.tsx.

// ./src/pages/_app.tsx constructor(props: AppProps) {     super(props);      this.appData = props.pageProps.appData || {};      initializeFetch(this.appData.basePath); } 

Чтобы проверить, можем добавить запрос в эффект на клиенте в index.tsx.

Таким образом можно задеплоить nest-next приложение «за слешом», не теряя преимуществ ни одного из фреймворков.

Заключение

Работая в симбиозе, nest и NEXT.js являются очень сильным инструментом разработки веб-приложений и сервисов. Надеюсь, что данная статья поможет желающим попробовать эти фреймворки вместе войти в разработку с максимальной эффективностью несмотря на скудность официальной документации nest-next. Также надеюсь, что и опытные пользователи этой комбинации инструментов нашли для себя в статье что-то новое.


ссылка на оригинал статьи https://habr.com/ru/articles/571032/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *