Как использовать SSR с Gatsby


Введение

В Gatsby 4 появились новые методы рендеринга. DSG (Deferred Static Generation) для рендеринга статей блога на ходу и SSR (Server Side Rendering). Однако, документации и хороших примеров использования этой новой технологии было мало, особенно об ограничениях этой технологии. В начале этого года я взял на себя ответственность за разработку веб-страниц своей компании и перевел многие страницы на использование SSR. Процесс был запутанным и разочаровывающим, и я хочу помочь всем разработчикам в моей ситуации. Эта статья будет посвящена SSR возможностям Gatsby, однако большинство решений будет применимо и к DSG.

Почему я вообще хочу использовать SSR?

Медиа-парень в моей компании хотел упростить публикацию изменений в определенных частях сайта и хотел, чтобы изменения публиковались без задержек. И я понимаю, почему он хочет не ждать 10-15 минут, пока изменения будут опубликованы. Зачем ждать сборки, если изменения можно опубликовать мгновенно. Я не стал переводить всю страницу на SSR, так как только часть страницы выигрывает от использования этой технологии. В общем, мой первый совет — определить страницы, которые больше всего выигрывают от SSR, затем определить приоритетность этих страниц вместе с владельцем продукта. Второй совет — переходить на SSR постепенно, по одной странице за раз.

Кодирование начинается

Получение данных

Если у вас есть существующее решение, то, скорее всего, у вас уже есть какой-то метод получения данных. Это может быть что-то вроде запросов к странице или что-то подобное. И мы собираемся сохранить большинство этих запросов. В моем проекте, большая часть данных, необходимых для LayoutComponent, навигации и т.д., работала отлично и будет редко меняться, что не требует SSR. Статьи в блоге и доступные посты — это совсем другая история. Для этого нам нужно получать данные на ходу, и для этого мы не можем использовать существующую graphql-интеграцию в gatsby. Мы должны сделать что-то самостоятельно.

Установка зависимостей

В моем случае мне нужно было получить данные из Sanity (безголовая CMS). А Sanity предоставляет хороший graphql-интерфейс. Поэтому нам нужно установить graphql-клиент в дополнение к существующему gatsby-source-sanity.

yarn add @apollo/client; npm install @apollo/client

Давайте получим некоторые данные

Сначала нам нужно настроить graphql-запрос, способный получать данные. Это не сложнее, чем сделать следующее.

src/server-side/client.js

import { ApolloClient, InMemoryCache } from ‘@apollo/client’;
import config from ‘../config’;

export const client = new ApolloClient({
  uri: `https://${config.SANITY_PROJECT_ID}.apicdn.sanity.io/v1/graphql/${config.SANITY_DATASET}/${config.SANITY_TAG}`,
  cache: new InMemoryCache(),
  headers: {
    Authorization: `Bearer ${config.SANITY_TOKEN}`,
  },
});
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь наступает печальная часть истории. Запросы, которые вы построили с помощью gatsby-graphql, больше не будут работать. Будут некоторые изменения, но если вы переписали свои запросы, то использование клиента не составит труда. Как показано в примере ниже, мне нравится размещать логику получения данных в отдельном файле.

import { gql } from '@apollo/client';
import { createGatsbyImages } from '../server-side/imageCreator';
import { client } from '../server-side/client';
export async function getBlogDataServerSide() {
  const response = await client.query({
    fetchPolicy: 'no-cache',
    query: gql`
      {
       Write something clever here ;)
      }
    `,
  });
  response.data.articles.forEach((article) => {
    createGatsbyImages(article);
  });
  return response.data;
}
Вход в полноэкранный режим Выйти из полноэкранного режима

И тогда в вашем компоненте будет что-то похожее на это:

const Blog = ({ data, serverData }) => {
  return (
    <div>Some blog article templating</div>
  );
};

export default Blog;

export async function getServerData() {
  try {
    return {
      props: { articles: await getBlogDataServerSide() },
      status: 200,
    };
  } catch {
    return {
      articles: [],
      status: 500,
    };
  }
}
export const query = graphql`
{
   You might have some sort of page query here
 }
`;
Вход в полноэкранный режим Выход из полноэкранного режима

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

Добавление поддержки gatsby-image

Поскольку мы больше не используем источник gatsby, нам нужно будет создать собственные объекты gatsby-image. Возможно, вы захотите создать нечто подобное для другой CMS. Прочитав исходный код gatsby-source-sanity, я придумал следующий скрипт.

import { getGatsbyImageData } from 'gatsby-source-sanity';
import config from '../config';

function resolveNodeType(asset) {
  if (asset._ref) {
    return asset._ref;
  }

  if (asset.id) {
    return { _id: asset.id };
  }

  return asset.url;
}

function imageCreator(asset) {
  const node = resolveNodeType(asset);
  let assets = {};

  if (asset.metadata?.dimensions) {
    assets = {
      ...asset.metadata.dimensions,
    };
  }

  return getGatsbyImageData(node, assets, {
    projectId: config.SANITY_PROJECT_ID,
    dataset: config.SANITY_DATASET,
  });
}

export function createGatsbyImages(element) {
  if (!element) return;
  Object.keys(element).forEach((subElement) => {
    if (typeof element[subElement] === 'object') {
      createGatsbyImages(element[subElement]);
      return;
    }

    if (Array.isArray(element)) {
      element.forEach((childElement) => {
        createGatsbyImages(childElement);
      });
      return;
    }

    if (
      subElement === '__typename' &&
      element[subElement] === 'Image' &&
      element.asset
    ) {
      element.asset.gatsbyImageData = imageCreator(element.asset);
      return;
    }
    if (
      subElement === '_type' &&
      element[subElement] === 'image' &&
      element.asset
    ) {
      element.asset.gatsbyImageData = imageCreator(element.asset);
      return;
    }
  });
}
Вход в полноэкранный режим Выход из полноэкранного режима

Добавление динамических маршрутов

Одним из слабых мест в документации Gatsby является объяснение различных вариантов маршрутизации в сочетании с SSR. Однако, вы здесь не для разглагольствований. Вы здесь для решения. При публикации новой статьи нам нужно иметь маршруты со слизнями в них. Чтобы это работало, мы будем использовать то, что в Gatsby называется fallback routes. В принципе, если вам нужна страница с именем /blogg/:slug, вы можете сделать следующий файл /src/pages/[slug].js. Тогда параметр slug будет доступен в props.params['slug']. Проще простого.

export async function getServerData(props) {
  const slug = props.params['slug'];
  // More ssr-code
}
Вход в полноэкранный режим Выход из полноэкранного режима

Подождите! Что случилось с нашей картой сайта?

Если у вас есть плагин, генерирующий карту сайта для вашей страницы, вы увидите, что статьи больше не отображаются. Причина в том, что поскольку на самом деле нет фактического маршрута, то и подбирать маршрут будет не из чего.
Большинство плагинов, которые я видел, имеют возможность запрашивать маршруты вручную.

Время для развертывания

Если вы развертывали свою веб-страницу самостоятельно, вам придется внести некоторые изменения в окружение. Самое наивное, что вы можете сделать, это попытаться использовать gatsby serve в качестве производственного сервера. Gatsby хочет, чтобы вы платили за их хостинг-услуги, и не предоставит вам сервер, способный работать бесплатно. gatsby serve медленный и имеет тенденцию добавлять дополнительные редиректы, которые полностью убивают ваш SEO результат. Нам нужно сделать что-то другое. В моей компании мы предпочитаем развертывать наши сервисы в нашем собственном кластере kubernetes. Это практически полное излишество и более сложная задача, чем платить за облако gatsby. Однако это дает мне и моим коллегам возможность изучить kubernetes, docker и helm. Другими причинами выбора самостоятельного хостинга может быть снижение стоимости хостинга. Еще одной причиной может быть то, что вам не разрешено использовать хостинг за пределами своей страны. Многие норвежские правительственные службы не могут быть размещены в публичных облаках.

Я бы предложил использовать архитектуру Varnish + Nginx + Fastify. Varnish для кэширования, nginx для простого проксирования и добавления gzip и edge-read-ness и fastify для обслуживания нашего приложения.

Добавляем fastify на нашу страницу

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

Затем добавьте плагин в gatsby-config.js.

plugins: [
    {
      resolve: `gatsby-plugin-fastify-klyngen`,
      options: {
        /* discussed below */
        features: {},
      },
    },
    ...otherPlugins
  ]
Вход в полноэкранный режим Выйдите из полноэкранного режима

Чтобы найти хороший сервер для обслуживания нашей страницы, мне пришлось использовать существующий сервер и настроить его. gatsby-plugin-fastify нуждался в некоторой любви. Я сделаю pull-request в их репозиторий со своими улучшениями. Пока не появится новая версия, я форкнул их репозиторий и сделал свою собственную версию. Но я настоятельно рекомендую использовать оригинальный пакет, поскольку его поддерживает более многочисленная команда, а я не намерен поддерживать сервер fastify очень долго.

Но как насчет производительности?

Было бы неплохо, если бы существовал механизм кэширования статей и другого содержимого. А затем аннулировать кэш при изменении содержимого. Varnish отлично справляется с этой задачей. Изначально Varnish был создан крупнейшей газетой Норвегии, и как гордый норвежец, я считаю, что varnish — идеальное решение. В данном случае, при использовании здравомыслия, это хорошее решение. Я поговорил с инженером dev-ops, чтобы узнать, как бы он решил эту проблему, и получил рекомендацию разместить страницу в CDN. В моем случае это было немного сложно, поскольку не было простого способа интегрировать Sanity с подходящей CDN.

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

vcl 4.0;

backend default {
    .host = "localhost";
    .port = "8080";
}

sub vcl_recv {
    if (req.method == "PATCH") {
        ban("req.url ~ .");
        return (synth(200, "Full cache cleared"));
    }
    unset req.http.Cookie;
}

sub vcl_backend_response {
    set beresp.ttl = 2w;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Самая важная строка в конфигурации — unset req.http.Cookie; Это указывает varnish кэшировать содержимое, даже если у вас есть cookie. Отсутствие кэша при наличии cookie — это умное значение по умолчанию, но оно не даст вам никакого кэша при использовании google analytics и подобных продуктов.

Конфигурация Nginx

Чтобы связать все вместе, нам понадобится простой nginx-config.

server {
  listen 8080;
  gzip on;
  gzip_vary on;
  gzip_min_length 1024;
  gzip_proxied expired no-cache no-store private auth;
  gzip_types text/plain text/css text/xml text/javascript application/x-javascript application/javascript application/xml;
  gzip_disable "MSIE [1-6].";

  absolute_redirect off;

  error_page 404 /404.html;

  rewrite ^([^.?]*[^/])$ $1/ permanent;

  location / {
    add_header Cache-Control "public";
    proxy_pass http://localhost:9000;
    proxy_set_header Host $host;
    proxy_set_header X-Forwarded-For $remote_addr;
    proxy_intercept_errors on;
    recursive_error_pages on;
  }
}
Вход в полноэкранный режим Выход из полноэкранного режима

И давайте завершим все это с помощью докер-образа

FROM nginx:1.20.2-alpine as dev

# Install packages and dependencies
RUN apk update && apk add --no-cache supervisor python3 make gcc g++ && apk add --update nodejs yarn varnish

# Build
WORKDIR /app
RUN mkdir -p /app/packages/website
RUN mkdir -p /app/.yarn/releases

COPY package.json yarn.lock .yarnrc.yml /app/
COPY .yarn/releases /app/.yarn/releases/
COPY .yarn/plugins /app/.yarn/plugins/
COPY packages/website /app/packages/website/
COPY packages/shared-components /app/packages/shared-components/

RUN yarn
RUN yarn workspace website run disable-telemetry

# The build step shouldn't be cached since it's non determenistic
# As such we add the next line to try and do a cache bust
# Recommended by: https://stackoverflow.com/a/58801213/359825
#ADD "https://www.random.org/cgi-bin/randbyte?nbytes=10&format=h" skipcache

# Ensure that proper .env files exists before building
RUN test -f "/app/packages/website/.env.production"
RUN yarn workspace website run build

# Configuring NginX
COPY website.nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80

# Configure varnish
COPY default.vcl /etc/varnish/

# Configure supervisor
COPY supervisord.conf /app/supervisord.conf
CMD ["supervisord","-c","/app/supervisord.conf"]

Вход в полноэкранный режим Выход из полноэкранного режима

Завершение работы

Надеюсь, это будет полезно для других людей, использующих gatsby. Возможно, это не идеальное решение, но оно хорошо работает. Если вы хотите увидеть полное решение, наш сайт с открытым исходным кодом доступен по адресу Alv Website. Заполните раздел комментариев вопросами и отзывами. Мы будем благодарны.

Оцените статью
devanswers.ru
Добавить комментарий