Как сконфигурировать 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/articles/576874/


Комментарии

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

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