Кешируем API ответы для frontend приложения с помощью Yakbak

от автора

Представьте себе типичную ситуацию: вы frontend разработчик в обычный компании. После сытного обеда вы лениво скроллите Хабр и смотрите Ютуб. Вдруг в чат прилетает сообщение от девопсов: «Ребята, мы планируем сегодня вечером разгрузить мастер и перенести admission контроллер на ноду и чтобы два раза не вставать всем подам выделим 50 CPU. Завтра всё будет работать как обычно, но быстрее!«
Вы понимаете, что скорее всего всё пойдёт не так и штатной работы микросервисов можно ожидать не раньше чем через пару дней. Но есть более важная для вас задача: на сегодняшнем митинге вы обещали тимлиду показать МВП фичи уже послезавтра.
Остаётся два варианта: поднять зоопарк микросервисов на своей машине прямо сейчас (долгий кровавый путь) или закешировать все необходимые для frontend приложения API запросы.
Предлагаю простой вариант использования второго варианта.

Приложение для демонстрации

Создадим небольшое приложение. В нем мы возьмём имя персонажа из Властелина колец и проверим на Гитхабе:

  1. Существует ли Гитхаб-юзер с таким логином;
  2. Какие существуют коммиты с упоминанием этого имени.

Для этого сгенерируем небольшое Ангуляр приложение и воспользовался некоторыми открытыми API:

  • Список персонажей из Властелина колец в удобном формате предоставляет the-one-api.dev. (Я скопировал массив в проект, потому что было лень хардкодить ключи доступа);
  • Информацию о юзерах и коммитах мы поищем на Гитхабе.

Механика сайта очень простая: выбираем персонажа и дергаем GitHub REST API. Результат выводим в красиво RPG UI стиле.

Если вас больше интересует результат работы приложения, а на техническая реализация, то можете поиграть с приложением тут. В продакшен версии приложения, запросы шлются непосредственно на Github.

Develop режим

Итак. Программист во время разработки запускает приложение через yarn start, оно стартует по адресу на localhost:4200. API запросы идут на локальный прокси, расположенный по этому же адресу, а оттуда уже на гитхаб. Конечно, можно сразу слать запросы на Гитхаб, а прокси нужен исключительно для демонстрации.

Proxy конфиг:

const PROXY_CONFIG = {     context: [       "/users",       "/search"     ],     "target": "https://api.github.com/",       // "target": "http://localhost:3111",     "changeOrigin": true,     "secure": true }  module.exports = PROXY_CONFIG;

Схема без кеша

Прослойка кеша

Главная идея: добавляем еще один кеширующий слой между локальным прокси и гитхабом.

Схема с кешем

Для создания кеширующей прослойки я воспользовался библиотекой yakbak, слегка обогатив её новой хеширующей функцией.
Как же работает эта библиотека? Упрощённо по шагам:

  1. Библиотека получает запрос и вычисляет его хеш;
  2. Проверяет, если ли файл с таким хешем в специальной директории;
  3. Если файл не существует, то Yakbak отправляет запрос на целевой сервер, а полученный ответ записывает в файл и отвечает за изначальный запрос;
  4. Если файл существует, то Yakbak читает файл и отвечает на изначальный запрос.

Чтобы воспользовать этим решением, нужно переключить target в прокси с https://api.github.com/ на http://localhost:3111 и запустить node yakbak-conf.js record. После этого все новые запросы и ответы на гитхаб, будут записываться в папку tapes/. А если запустить команду node yakbak-conf.js (без слова record), то Yakbak будет только отвечать уже записанными файлами и новые запросы на Гитхаб слать не будет.

Если немного попользоваться таким решением, то приходит понимание, что лучше использовать читабельные названия файлов и не все параметры нужно запихивать в кеш. К примеру, если у вас есть в query параметрах какие-то параметры с датами, то проще их исключить их хеширования. Поэтому для себя я выбрал следующий формат названия файла с записанным ответом: ${req.url}__${req.method}__${queryHash}. К тому же, всегда удобно залезть ручками в нужный файл и что-то там подправить под себя.

Как выглядит список закешированных файлов:

... __search__commits__GET__eyJxIjoic2F1cm9uIn0=.js __search__commits__GET__eyJxIjoidGFudGEifQ==.js __search__commits__GET__eyJxIjoiZnJvciJ9.js __users__adaldrida__GET__e30=.js __users__adalgar__GET__e30=.js __users__adalgrim__GET__e30=.js __users__adamanta__GET__e30=.js ...

А что внутри каждого файла?

var path = require("path");  /**  * GET /users/adalgar  *  * cookie: Webstorm-7f35e0f0=5cbe6ea7-fbdc-4756-42-b67baf0b74c4; AIOHTTP_SESSION="gAAAAABh4C__bz3aTNe_Podsmatrivay_kq3iCaA=="  * accept-language: ru-RU,ru;q=0.9,en-US;q=0.8,en;q=0.7  * accept-encoding: gzip, deflate, br  * referer: http://localhost:4200/  * sec-fetch-dest: empty  * sec-fetch-mode: cors  * sec-fetch-site: same-origin  * sec-ch-ua-platform: "Linux"  * user-agent: Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/106.0.0.0 Safari/537.36  * sec-ch-ua-mobile: ?0  * accept: application/vnd.github+json  * sec-ch-ua: "Chromium";v="106", "Google Chrome";v="106", "Not;A=Brand";v="99"  * cache-control: no-cache  * pragma: no-cache  * connection: close  * host: api.github.com  */  module.exports = function (req, res) {   res.statusCode = 200;    res.setHeader("server", "GitHub.com");   res.setHeader("date", "Wed, 16 Nov 2022 21:26:23 GMT");   res.setHeader("content-type", "application/json; charset=utf-8");   res.setHeader("cache-control", "public, max-age=60, s-maxage=60");   res.setHeader("vary", "Accept, Accept-Encoding, Accept, X-Requested-With");   res.setHeader("etag", "W/\"07c85ce555cf40c22d222e0bfaccc7cccf5528d40312724324341b02e19ba0\"");   res.setHeader("last-modified", "Sat, 06 Apr 2019 16:00:30 GMT");   res.setHeader("x-github-media-type", "github.v3; format=json");   res.setHeader("access-control-expose-headers", "ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset");   res.setHeader("access-control-allow-origin", "*");   res.setHeader("strict-transport-security", "max-age=31536000; includeSubdomains; preload");   res.setHeader("x-frame-options", "deny");   res.setHeader("x-content-type-options", "nosniff");   res.setHeader("x-xss-protection", "0");   res.setHeader("referrer-policy", "origin-when-cross-origin, strict-origin-when-cross-origin");   res.setHeader("content-security-policy", "default-src 'none'");   res.setHeader("content-encoding", "gzip");   res.setHeader("x-ratelimit-limit", "60");   res.setHeader("x-ratelimit-remaining", "44");   res.setHeader("x-ratelimit-reset", "1668635372");   res.setHeader("x-ratelimit-resource", "core");   res.setHeader("x-ratelimit-used", "16");   res.setHeader("accept-ranges", "bytes");   res.setHeader("content-length", "452");   res.setHeader("x-github-request-id", "7200:EE5F:2342344:207742:111157F");   res.setHeader("connection", "close");    res.setHeader("x-yakbak-tape", path.basename(__filename, ".js"));    res.write(new Buffer("H4sIAAAAAAAAA52TQYvbMBCF7/kVwefuyukmWWIopdDrLgTaUnoJsqy1VWRJSGOHxOx/70jjTRMfWrwn48e8T29GmmGxXGba1spkxTL7UnFdc599", "base64"));   res.write(new Buffer("iKqqUFrvHtabx8ddUoyt5CHJ2dPX/fbHz2ctfu83T+f96vksNmTjPQfuD53Xsa4BcKFgjNRwXytourIL0gtrQBq4F7ZlHXs753P/aU2c2o8kOpDEKdWpkUgYxAZ200QDrZ5moQzJcVP7YrW2R0RMDfwfx7CLixLSrzL1OyDoGpiFRuL4sJXXcRAqwLxIdXQMLH7wukZMwFvxspoVa/RgqKPBPAPz0tk3XlcG4ZUDZc28eOHaST1aX3Ojznw+DJ2BGDHbvCDJQWbZ41uc5ybLwJxXPRencSxeCql6nPM7iBMvJYOTk3GTvuOLICUokAdetWlnX7gOkraTt7HQdFqnf3zfjpvTtVTipkcWcbQVaeDXFbLlKi7uhdIoL3mpb8ilstclcFQA45s1kxCuK7USB5p0scxTslFMzxRBJP7do1sFl+JSIzAL4Gw5xC4+5qvdXb6+y7ffVtsiz4uH/Bd11rnqf3WL18UfF7Fg8fwEAAA=", "base64"));   res.end();    return __filename; };

Ниже я привожу полный конфиг кеширующего сервера, он достаточно краткий и прозрачный.

const express = require('express'); const yakbak = require('yakbak'); const {pick, omit} = require('ramda');  const record = process.argv.includes('record') || false; const base64 = (s) => Buffer.from(s).toString('base64'); const getUrlName = (req) => req.url.replace(/\//gi, '__').split('?')[0]; const getQueryHash = (req, queryParams) => base64(JSON.stringify(omit(queryParams, req.query)));  const omittedParameters = [     'start',     'end' ];  const yak = yakbak('https://github/', {     dirname: __dirname + '/tapes',     noRecord: !record,     hash: (req, body) => {         const name = getUrlName(req);         const queryHash = getQueryHash(req, omittedParameters);          return `${name}__${req.method}__${queryHash}`;     } });  express().use(function (req, res, next) {     yak(req, res); }).listen(3111);

Вот собственно и всё. Все исходники проекта можно найти у меня на Гитхабе тут и попрактиковаться на кошках.

P.S. Могу сказать что однажды я применял Yakbak для ускорения прохождения E2E тестов для frontend части, но, конечно, эти тесты после этого уже были не совсем E2E 🙂


ссылка на оригинал статьи https://habr.com/ru/articles/699834/


Комментарии

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

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