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

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

Время на прочтение5 мин
Количество просмотров7.4K
  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

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

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

Публикации