Как стать автором
Обновить

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

Время на прочтение16 мин
Количество просмотров8.2K

Это вторая часть статьи о применении комбинации технологий 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. Также надеюсь, что и опытные пользователи этой комбинации инструментов нашли для себя в статье что-то новое.

Теги:
Хабы:
Всего голосов 4: ↑4 и ↓0+4
Комментарии4

Публикации

Истории

Работа

Ближайшие события