Эмуляция бэкенда: как разрабатывать изолированный фронтенд с помощью Mock Service Worker

gdtdtqzlqomoomz8eyxmwe_2l5g.png

Всем привет! Сегодня я хочу рассказать о Mock Service Worker — технологии, которая позволяет эмулировать поведение бэкенда в ситуациях, когда по каким-то причинам невозможно использовать реальный бэкенд для полноценной разработки фронтенда.

Эта технология подойдёт в следующих случаях:

  • если в вашей команде фронтенд должен разрабатываться параллельно или даже раньше, чем бэкенд на основе контрактов;

  • если технически невозможно использовать реальный API бэкенда в условиях локальной разработки — такая ситуация возникла в моём рабочем проекте в банке, где бэкенд функционирует только в тестовом контуре и доступен через виртуальную машину;

  • если вы пишите end-to-end тесты, в которых проверяете работу критических пользовательских сценариев.

Для всех выше перечисленных задач отлично подойдёт технология Mock Service Worker, которую я давно и успешно применяю в реально работающих приложениях. Тем более, что совсем недавно вышла новая мажорная версия соответствующей библиотеки msw, и в ней достаточно много важных обновлений.

Что такое API Mocking и при чём тут Service Workers

Mock — это модель, основанная на реальных данных, а API Mocking — это технология обработки таких моделируемых данных. Функцию API Mocking сервиса может выполнять отдельный сервер на Node.js, который запускается локально и обрабатывает запросы к API. Однако существует и другой способ обработки запросов, который не требует использования отдельного сервера, при этом отлично эмулирует сетевое поведение и предоставляет мощные инструменты для тестирования, изолированной разработки и дебага различных сетевых сценариев. Это технология Mock Service Worker (далее MSW).

MSW предоставляет файл сервис-воркера, который подключается и регистрируется на этапе инициализации приложения, слушает события отправки XHR-запросов и перехватывает и обрабатывает их по заданным правилам для конкретных эндпоинтов. На продакшене Service Worker не будет активирован, и приложение автоматически переключится на работу с реальным API.

Подробнее про Service Worker API можно почитать тут: https://developer.mozilla.org/ru/docs/Web/API/Service_Worker_API.

MSW позволяет использовать привычные сервисы для отправки запросов к API (такие как встроенный fetch, библиотеку axios и др.) и использовать в запросах те же эндпоинты, которые работают с реальным бэкендом. Таким образом, не появляется необходимость поддерживать две конфигурации и два набора эндпоинтов для разработки и продакшена.

Как начать работать с Mock Service Worker

Для того чтобы настроить Mock Service Worker понадобится соответствующая библиотека msw, информацию о которой можно найти на официальном сайте: https://mswjs.io.

Установка и инициализация MSW

Для того, чтобы установить библиотеку msw, необходимо ввести следующую команду в терминале, находясь в папке проекта:

npm install msw@latest --save-dev

Для работы с последней версией библиотеки понадобится версия Node.js не меньше 18.0.0. Если на проекте используется TypeScript, то его версию придётся поднять как минимум до версии 4.7.

Далее необходимо инициализировать Mock Service Worker и создать необходимые файлы для его работы. Это можно сделать с помощью следующей команды:

npx msw init ./public --save

Данная команда создаст файл с воркером mockServiceWorker.js в папке ./public, в которой хранятся публичные статические файлы проекта. Параметр --save позволит сохранить путь до папки в package.json проекта для будущих обновлений скрипта воркера.

Создание обработчиков запросов

Следующий шаг — это создание обработчиков запросов, которые будут перехватывать запросы к реальному бэкенду и эмулировать его работу.

В ранних версиях msw синтаксис работы с запросами повторял синтаксис обработчиков маршрутов на сервере Node.js, например, в Express. Однако с выпуском версии 2.0 вся логика работы была адаптирована к нативному стандарту Fetch API. Это важное изменение, позволяющее полноценно эмулировать работу с запросами с использованием самого современного браузерного стандарта.

Обработка запросов в MSW реализована через использование двух концепций:

  1. request handler — это обработчик запросов, привязанный к конкретному URL, который перехватывает запрос и запускает функцию-резолвер;

  2. response resolver — это функция-резолвер, которая имеет доступ к параметрам запроса и возвращает ответ, эмулирующий ответ от реального бэкенда.

Давайте рассмотрим эти концепции на практике, а заодно создадим обработчик GET-запроса на эндпоинт /api/posts.

Для начала необходимо создать директорию src/mocks, в которой будет храниться всё, что относится к моковым данным. Далее создадим в ней файл handlers.js, в котором будут описаны все обработчики моковых запросов.

Начнём с импорта модуля http, который позволяет перехватывать и обрабатывать REST-запросы:

import { http } from 'msw';

Следом напишем заготовку под обработчик:

// request handler
const postsHandler = http.get("/api/posts", postsResolver);

Модуль http позволяет обработать как вызов конкретного стандартного REST-метода (GET, POST, PATCH и др.), так и вызовы всех методов сразу на один и тот же URL — для этого предназначен метод модуля http.all(predicate, resolver).

Подробнее про методы модуля http можно почитать в документации msw: https://mswjs.io/docs/api/http.

Кроме REST-запросов, msw может также эмулировать запросы к GraphQL с помощью модуля graphql. Дополнительную информацию об этом можно найти в документации: https://mswjs.io/docs/network-behavior/graphql.

Первый параметр в request handler называется predicate (далее предикат) и отражает правила, по которым проверяется URL запроса. Предикат может быть как обычной строкой, так и регулярным выражением. Запрос будет обработан функцией-резолвером, переданной в параметре resolver, только если URL запроса совпадёт с правилом, заданным в предикате. В нашем случае предикатом является строка с относительным URL api/posts. Теперь при вызове любого GET-запроса на данный URL из клиентского приложения с работающим Mock Service Worker запустится обработчик вызова postsHandler.

Подробнее о правилах формирования предиката можно почитать в документации: https://mswjs.io/docs/basics/intercepting-requests#http-request-matching, а пока что пойдём дальше и напишем код функции-резолвера:

// response resolver
const postsResolver = ({ request, params, cookies }) => {
  return HttpResponse.json([
    {
      title:
        "Что такое генераторы статических сайтов и почему Astro — лучший фреймворк для разработки лендингов",
      url: "https://habr.com/ru/articles/779428/",
      author: "@AlexGriss",
    },
    {
      title: "Как использовать html-элемент ?",
      url: "https://habr.com/ru/articles/778542/",
      author: "@AlexGriss",
    },
  ]);
};

Response resolver предоставляет доступ к единственному аргументу в виде объекта, в котором содержится информация о перехваченном запросе.

В поле request будет доступна реализация нативного интерфейса Request из Fetch API, так что вам будут доступны все стандартные методы и свойства соответствующего класса. В поле params будут доступны параметры пути, такие как, к примеру, postId для URL вида api/posts/:postId. Наконец, в поле cookies будут доступны все установленные при запросе куки в виде строковых пар ключ-значение.

Функция-резолвер должна возвращать инструкцию о том, что нужно сделать при перехвате запроса. Чаще всего это будет ответ в виде mock-данных, и для этого в msw используется инстанс нативного класса Response из всё того же Fetch API. Вы можете использовать класс Response напрямую, но разработчики msw рекомендуют работать с библиотечным классом HttpResponse, который является более продвинутой надстройкой над Response. Например, он позволяет мокать установку cookies и дополняет стандартные методы класса Response удобными методами для отправки ответа с различными Content-Type.

О классе HttpResponse можно почитать подробнее в документации библиотеки msw: https://mswjs.io/docs/api/http-response.

Кроме обычных текстовых данных в виде plain text, json, xml или formData, библиотека msw позволяет возвращать стримы ReadableStream. Подробнее об этом можно почитать в соответствующем разделе документации: https://mswjs.io/docs/recipes/streaming

В приведённом примере вызывается метод HttpResponse.json(), в который передаётся моковая структура данных — в данном случае это коллекция постов на Хабре. Далее она будет отправлена на клиент при успешной обработке вызова.

Давайте посмотрим на то, как должен выглядеть итоговый файл handlers.js:

// src/mocks/handlers.js

import { HttpResponse, http } from "msw";

// response resolver
const postsResolver = () => {
  return HttpResponse.json([
    {
      title:
        "Что такое генераторы статических сайтов и почему Astro — лучший фреймворк для разработки лендингов",
      url: "https://habr.com/ru/articles/779428/",
      author: "@AlexGriss",
    },
    {
      title: "Как использовать html-элемент ?",
      url: "https://habr.com/ru/articles/778542/",
      author: "@AlexGriss",
    },
  ]);
};

// request resolver
const postsHandler = http.get("/api/posts", postsResolver);

export const handlers = [postsHandler];

В конце необходимо эскпортировать переменную handlers, содержащую массив со всеми обработчиками вызовов.

Настройка Mock Service Worker для работы в браузере

Далее в папке src/mocks создадим файл browser.js, в котором нужно будет настроить работу воркера с ранее добавленными обработчиками вызовов:

// src/mocks/browser.js
import { setupWorker } from "msw/browser";

import { handlers } from "./handlers";

export const worker = setupWorker(...handlers);

Функция setupWorker принимает список обработчиков и подготавливает канал связи между клиентом и воркером mockServiceWorker.js, который мы сгенерировали на первом этапе.

Далее мы готовы активировать работу MSW через вызов метода воркера worker.start(). Ниже показан пример активации воркера во входной точке React-приложения:

// src/index.jsx
import React from 'react';
import ReactDOM from 'react-dom';

import { App } from './App';

async function enableMocking() {
  if (process.env.NODE_ENV === "development") {
    const { worker } = await import("./mocks/browser");

    return worker.start();
  }
}

const rootElement = ReactDOM.createRoot(document.getElementById("root"));

enableMocking().then(() => {
  rootElement.render();
});

Активация MSW должна происходить только в режиме разработки. Результатом выполнения метода воркера worker.start() будет являться промис, при резолве которого необходимо рендерить клиентское приложение. Такая последовательность задач необходима для того, чтобы избежать гонки состояний между регистрацией воркера и запросами, которое делает приложение на старте работы.

Для приложения на ванильном JS достаточно просто запустить метод worker.start() на этапе инициализации приложения.

Если всё сделано правильно, то при запуске приложения в консоли браузера можно будет увидеть сообщение:

[MSW] Mocking enabled.

MSW умеет работать не только в браузерной среде, но и в связке с Node.js и React Native. Настроить моки для данных окружений помогут соответствующие разделы в документации библиотеки: https://mswjs.io/docs/integrations/node и https://mswjs.io/docs/integrations/react-native.

Проверка обработки запросов

Теперь если в клиентском приложении сделать запрос на URL /api/posts, Mock Service Worker должен его перехватить и вернуть mock-данные.

Давайте рассмотрим это на примере компонента Posts в рамках React-приложения:

// src/components/Posts.jsx
import { useEffect, useState } from "react";

import { Post } from "./Post";

export const Posts = () => {
  const [posts, setPosts] = useState(null);
  const [isLoading, setIsLoading] = useState(true);

  useEffect(() => {
    fetch(`/api/posts`)
      .then((response) => response.json())
      .then((posts) => {
        setPosts(posts);
      })
      .finally(() => {
        setIsLoading(false);
      });
  }, []);

  return isLoading
    ? "Loading..."
    : posts.map((post) => (
        
      ));
};

Теперь при загрузке React-компонента на вкладке Network в Developer Tools браузера отобразится XHR-запрос на URL api/posts с ответом в виде mock-данных:

Запрос на /api/posts

Запрос на /api/posts

MSW будет перехватывать любой запрос, если его URL соответствует предикату пути в обработчике запросов, неважно откуда из приложения он вызван. Кроме отображения во вкладке Network каждый перехваченный запрос вывыдет дополнительную информацию в консоль браузера.

В заключение

Технология Mock Service Worker является не просто средой для работы с mock-данными, она позволяет буквально эмулировать сетевое поведения без развёртывания отдельного сервера. MSW предоставляет широкие возможности для повторения логики бэкенда на клиенте, тестирования сложных пользовательских сценариев и полноценной разработки фронтенда в условиях, когда бэкенд не готов. Точная эмуляция сетевого поведения позволяет автоматически переключаться на работу с реальными данными и реальными API, как только приложение запускается в продакшене.

MSW умеет эмулировать работу как с обычными REST-запросами, так и с GraphQL и даже возвращать стримы в качестве ответа на запрос. Библиотека msw позволяет настроить интеграцию сервис-воркера с различными средами, такими как браузер, Node.js и React Native. Эмуляция реального сетевого поведения стала ещё более доступной в новой версии MSW с введением интерфейсов обработчиков запросов, которые повторяют реализацию нативных интерфейсов Fetch API, таких как Request и Response.

Мы изучили основы работы, подключения и настройки технологии Mock Service Worker, но на этом её возможности не заканчиваются. Предлагаю попробовать поэкспериментировать с MSW самостоятельно, повторив руководство, представленное в этой статье. Продолжить изучение технологии можно на сайте с документацией к библиотеке msw: https://mswjs.io/docs/getting-started.

Приглашаю вас подписаться на мой телеграм-канал: https://t.me/alexgriss, в котором я пишу о фронтенд-разработке, публикую полезные материалы, делюсь своим профессиональным мнением и рассматриваю темы, важные для карьеры разработчика.

© Habrahabr.ru