Как сконфигурировать NextJS сервер с полной поддержкой кэширования в Redis

Моя цель - предложение широкого ассортимента товаров и услуг на постоянно высоком качестве обслуживания по самым выгодным ценам.
  1. Next.js — JS фреймворк, созданный поверх React.js для создания веб-приложения с поддержкой функционала отрисовки приложения на стороне сервера

  2. Redis (Remote Dictionary Server)- это быстрое хранилище данных типа «ключ‑значение» в памяти, активно используемое в разработке с целью повышения производительности сервисов

  3. Кэш Redis позволяет максимально эффективно применять горизонтальное масштабирование вашего приложения, поскольку заставляет все инстансы приложения смотреть в единый источник данных, вместо использования локального стейта

  4. В рамках данного гайда мы соберем собственный вариант сервера для фреймворка NextJS, добавив кэширование сгенерированых страниц и json файлов, содержащих пропсы этих страниц ( для ускорения навигации за счет префетчей)

Шаг 1. Установка NextJS

yarn create next-app

Шаг 2. Установка пакетов:

yarn add express redis node-gzip

Шаг 3. Создаем в корне проекта файл server.js

const express = require('express')
const next = require('next')
const { promisify } = require('util')
const {gzip} = require('node-gzip');

const client = redis.createClient('redis://localhost:6379')
client.get = promisify(client.get)

const app = next({ dev: false })
const handle = app.getRequestHandler()

app.prepare().then(() => {

  const server = express()

  server.get('*', (req, res) => handle(req, res))

  server.listen(3000, (err) => {
    if ( err ) throw err
    console.log(`> Ready on http://localhost:3000`)
  })
})
  1. Подключаем express для создания сервера

  2. Подключаем next для создания базового обработчика запросов NextJS

  3. Подключаем пакет для сжатия

  4. Устанавливаем соединение с redis, и используем promisify, чтобы работать с промисами вместо колбэков.

  5. Запускаем сервер и устанавливаем базовый обработчик для всех запросов ( * ) через handle

Шаг 4. Добавляем команду запуска сервера в package.json

  "scripts": {
   ...
    "start": "node server.js",
   ...
  },

Шаг 5. Формирование ключей

Для того, чтобы в полной мере использовать мощь Redis в NextJS, нам необходимо кэшировать 2 вида файлов:

  1. HTML, который отдается в браузер при серверной генерации страницы

  2. JSON-файл для каждой страницы, который генерирует NextJS для клиентской навигации ( когда мы переходим на страницу NextJS запрашивает этот JSON файл и использует для передачи пропсов страницы

Шаг 6. Создаем ключ для SSR

const getSsrKey = (req) => {
  return req.url
}

Для данной задачи самым простым решением для формирования ключа будет взять url страницы

Шаг 7. Создаем ключ для JSON

const getJsonKey = (req) =>
  req.path
    .match(/\/([^\/]+)+/g)
    .slice(3)
    .join('')

Путь к JSON файлам в рамках фреймворка NextJS имеет вид /_next/data/<BUILD_ID>/your-page-name.json

Поэтому самой полезной частью этого пути является то, что идет после идентификатора сборки - your-page-name.json, поэтому его мы и будем брать для формирования ключа

Шаг 8. Создаем кэш HTML

async function ssrCache(req, res) {

  const key = getSsrKey(req)  
  const cache = await client.get(key)

  if ( cache ) {
    return res.send(cache)
  }

  const data = await app.renderToHTML(req, res, req.path, { ...req.query, ...req.params })

  if ( res.statusCode === 200 && data ) client.set(key, data)

  return res.send(data)
}

Для рендеринга страниц nextJS предоставляет метод renderToHTML. Поэтому наша задача состоит в том, чтобы:

  1. Попытаться получить значение из кэша Redis по ключу.

  2. Если кэш существует, то мгновенно отдать в браузер

  3. Если же кэша нет, то отрендерить страницу с помощью метода renderToHTML

  4. Если страница была отрендерена успешно, закэшировать полученный HTML, после чего отдать в браузер

Шаг 9. Создаем кэш JSON

async function jsonCache(req, res) {

  const key = getJsonKey(req)

  const cache = await client.get(key)

  if ( cache ) {

    const headersToWrite = {
      'content-type': 'application/json',
      'content-encoding': 'gzip'
    }

    const buffer = JSON.parse(cache, (k, v) => {

      if ( v !== null && typeof v === 'object' && 'type' in v && 
      v.type === 'Buffer' &&  'data' in v && Array.isArray(v.data) ) {
        return Buffer.from(v.data)
      }

      return v
    })

    Object.entries(headersToWrite).forEach(([ key, value ]) => res.setHeader(key, value))

    return res.send(buffer)

  }

  const rawResEnd = res.end
  const rawResWrite = res.write
  const chunks = []

  const proxyWrite = new Proxy(res.write, {

    apply(target, thisArg, args) {
      const chunk = Buffer.from(args[ 0 ])
      chunks.push(chunk)
    }

  })

  res.write = proxyWrite

  const data = await new Promise(async (resolve) => {

    res.end = async (res) => {
      resolve(res || chunks)
    }

    await app.render(req, res, req.path, {
      ...req.query,
      ...req.params
    })

  })

  res.write = rawResWrite
  res.end = rawResEnd

  const isChunked = Array.isArray(data)
  const response = isChunked ?  Buffer.concat(data) : (Buffer.from(data))
  const serializedResponse = isChunked ? JSON.stringify(response) : (JSON.stringify(await gzip(response)))

  if ( res.statusCode === 200 && data ) client.set(key, serializedResponse)

  return res.end(response)
}

С JSON кэшем ситуация немного сложнее. Для рендера JSON мы можем воспользоваться методом render. Проблема заключается в том, что этот метод не возвращает никакого значения, а сразу отдает его в браузер. Таким образом, для решения данной задачи нам необходимо подменить функциональность следующих методов, которые используются для выдачи в браузер:

  1. res.write - вместо того, чтобы выводить данные мы сделаем так, чтобы эти данные попадали в массив chunks.

  2. res.end - вместо того, чтобы завершать процесс ответа будет сигнализировать нам о том, что в chunks уже лежат все необходимые для вывода данные, и мы можем начать с ним работать

  3. Теперь мы делаем ту же самую проверку на наличие кэша, и если он существует, то парсим JSON в Buffer, добавляем заголовки о типе и кодировке данных и отдаем в браузер

  4. Если кэша нет, то запоминаем базовые функции res.write и res.end в переменную, чтобы потом иметь возможность их восстановить

  5. Переопределяем write и end прокси функциями, которые обсудили в пунктах 1 и 2

  6. Рендерим контент с помощью render, но благодаря нашим прокси функциям он не будет отдан браузер, а удобно окажется в специально созданной нами переменной

  7. Восстанавливаем базовые версии функций write и end

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

  9. Если мы получили успешный ответ, то сериализуем буфер, сжимаем (в случае необходимости) и сохраняем в кэше редиса

  10. Завершаем ответ методом end

Шаг 11. Собираем все вместе

Для SSR страниц мы применяем ssrCache, для JSON файлов - jsonCache

server.get('/', (req, res) => {
  return ssrCache(req, res)
})

server.get('/_next/data/*', async (req, res) => {
  return jsonCache(req, res)
})

server.get('*', (req, res) => handle(req, res))

Ссылка на репозиторий: https://github.com/IAlexanderI1994/next-redis-article

Благодарю за прочтение. Буду рад вашим вопросам и комментариям






Источник: https://habr.com/ru/post/576874/


Интересные статьи

Интересные статьи

Статья отражает личный опыт разработки расширения ядра для Linux.В ней рассмотрен способ выполнения привилегированных системных вызовов процессом пространства ядра по запросам управляющей...
Здравствуй Хабр, давно не виделись. Сегодня я хотел бы рассказать про свой домашний сервер: какие ошибки допустил, на какой конфигурации сейчас остановился, да и вообще — зачем я это д...
Браузер Chromium, активно развивающийся open-source-родитель Google Chrome и нового Microsoft Edge, обратил на себя серьёзное негативное внимание из-за функции, которая задумывала...
На сегодняшний день у сервиса «Битрикс24» нет сотен гигабит трафика, нет огромного парка серверов (хотя и существующих, конечно, немало). Но для многих клиентов он является основным инструментом ...
Согласно многочисленным исследованиям поведения пользователей на сайте, порядка 25% посетителей покидают ресурс, если страница грузится более 4 секунд.