Привет, Хабр! Меня зовут Виктор, я фронтенд-работчик в Admitad. Моя команда делает личный кабинет клиентов. Недавно я в очередной раз столкнулся с типичной проблемой: для создания нового функционала фронтенд и бэкенд нужно было реализовывать параллельно. Но как делать фронт, не имея 100% рабочих эндпойнтов на бэкенде? Сегодня я расскажу о том, какие подходы применял, и разберу их плюсы и минусы.

О проблеме

Как я уже сказал выше, проблема заключалась в том, что мне было необходимо отобразить полученные в результате запроса на сервер данные в одном из разделов приложения. Но на бэкенде этот эндпойнт был еще не готов. Стандартный выход из подобной ситуации это использование mock-данных.

Mock-объект - тип объектов, реализующих заданные аспекты моделируемого программного окружения (c)Википедия

Как организовать получение mock-данных?

Для решения проблемы есть несколько вариантов. Начнем с самого популярного.

Пишем сервер на node.js

Мы всегда можем написать свой mock-server на node.js используя такие фреймворки, как express или koa.

Особых сложностей в этом нет, особенно, если вы уже знакомы с node.js. Например, на express код простейшего сервера будет выглядеть таким образом:

import express from 'express';

const app = express();

const port = 3000;

app.get('/', (req, res) => {
  res.send('This is Main!');
});

app.listen(port, () => {
  console.log(`App listening at http://localhost:${port}`);
});

О более продвинутом можно почитать тут.

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

Используем внешние API сервисы

Уверен, многие знакомы с таким сервисом готовых API, как jsonplaceholder.typicode.com, в котором есть как готовые энднпойнты, например, /posts, /comments..., так и возможность добавлять свои, причем, либо создав в репозитории своего проекта файл db.json с фейковыми данными, либо с помощью приложения Mockend. Главный, на мой взгляд, минус заключается в том, что такой подход не позволяет использовать например POST-запросы.

Используем Mock Service Worker + Faker

Этот способ мне показался наиболее удобным, поскольку позволяет вести разработку в пределах основного проекта, легко подключая необходимые эндпойнты и отключая уже ненужные. В отличие от предыдущего способа, он предоставляет такие возможности как обработка всех REST-запросов и генерация набора данных с помощью моделей.

Итак, наши инструменты:

  • Mock Service Worker(далее MSW) - перехватывает запросы на сетевом уровне в браузере, обрабатывает их и возвращает ответы;

  • Faker - генерирует данные в соответствии с нашими потребностями;

Работает всё следующим образом: настраиваем service worker, который будет обрабатывать наши сетевые запросы и возвращать сгенерированные нами фэйковые данные, пока бэкенд в разработке. Таким образом работа двух команд может вестись параллельно. А когда бэкенд будет готов, достаточно будет сделать сборку без флага, запускающего mock-сервис.

Две основные концепции с которыми работает MSW, это:

  • request handler - обработчик запроса, который определяет, должен ли быть замокан запрос

  • response resolver - функция, принимающая перехваченный запрос и возвращающая замоканный ответ

Обработчики запросов

MSW содержит в себе два набора методов для создания обработчиков для работы с API: rest и graphql.

Методы rest:

  • rest.get()

  • rest.post()

  • rest.put()

  • rest.patch()

  • rest.delete()

  • rest.options()

Первым аргументом методы принимают URL запроса, а вторым response resolver.

Пример из документации:

import { setupWorker, rest } from 'msw'

const worker = setupWorker(
  rest.get('/users/:userId', (req, res, ctx) => {
    const { userId } = req.params

    return res(
      ctx.json({
        id: userId,
        firstName: 'John',
        lastName: 'Maverick',
      }),
    )
  }),
)

worker.start()

Пример

Я создал простой проект с использованием mswjs + faker в качестве mock-сервера. Код проекта находится в репозитории. Фронт написан на React+TypeScript, redux в качестве стейт менеджера в связке с redux-saga, для стилей Sass. Файловая структура такая:

Пройдемся по папкам:

  • в api хранится реализация отправки сетевых запросов

  • в components понятно, компоненты

  • в mocks все, что связано с mock-сервером

  • в modules вынесен функционал связанный с каждой отдельной сущностью, в моем случае это Students

  • в store находится все, что отвечает за состояние приложения

Для создания проекта я использовал create-react-app:

npx create-react-app studentList

Далее установил faker и Mock Service Worker как devDependencies:

Настройки MSW

Для начала инициализируем Mock Service Worker используя следующую команду:

npx msw init public/ --save

В результате в папке /public будет создан файл mockServiceWorker.js со всеми необходимыми настройками для подключения сервис-воркера для mock-сервера.

После чего создадим файл src/mocks/browser.js, в котором подключим обработчики запросов:

// src/mocks/browser.js

import { setupWorker } from 'msw';
import { handlers } from './handlers';

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

Внимание! Использовать Mock Service Worker в production сборке крайне не рекомендуется!

Используем переменную окружения NODE_ENV, для проверки окружения, для которого собирается приложение в src/index.js:

// src/index.js

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';

if (process.env.NODE_ENV === 'development') {
  const { worker } = require('./mocks/browser');

  worker.start();
}

ReactDOM.render(<App />, document.getElementById('root'));

После выполнения команды npm start и запуска приложения откроем браузер и увидим в консоли Developer Tools сообщение о том, что mock-server запущен.

Значит все правильно настроили.

Подводные камни

Описанный пример взят из документации, но с ним все не так просто. Если приложение сразу после загрузки отправляет сетевые запросы (а именно так и работает наше приложение), вначале посыпятся 404-е ответы. Так происходит, потому что регистрация сервис-воркера - процесс асинхронный. Поэтому старт приложения и необходимые для этого запросы необходимо выполнить после окончания этого процесса:

Добавляем обработчики запросов

Список студентов я получаю по запросу GET /students. Создадим для него обработчик:

// /src/mocks/handlers/students.ts

import {rest} from 'msw';

import { STUDENTS_LIST_ROUTE_MASK } from '../mocks.constants';
import {db} from '../db';
import { IStudent } from '../../components/StudentCard/StudentCard.types';
export const getStudentsHandler = rest.get(STUDENTS_LIST_ROUTE_MASK, ( req, res, ctx)=>{
    // Генерируем мо модели массив из шести студентов
    // и возвращаем его в ответе
    let students:IStudent[] = [];
    for (let i = 0; i &lt; 6; i += 1) {
    students.push(db.student.create());
}

return res(
    ctx.json({
        students
    })
)

})

Модель данных

Для генерации списка студентов используем библиотеку для моделирования данных @mswjs/data и faker. 

// /src/mocks/models/StudentModel

import { primaryKey } from '@mswjs/data';
import faker from 'faker';

export const StudentModel = {
    id: primaryKey(() => faker.random.number(10000).toString()),
    firstName: ()=> faker.name.firstName(),
    lastName: () => faker.name.lastName(),
    age: () => faker.datatype.number({min: 18, max: 69}),
    email: () => faker.internet.email(),
    phone: () => faker.phone.phoneNumber(('+7 (###) ###-##-##')),
    city: () => faker.address.cityName(),
    company: () => faker.company.companyName(),
    avatar: () => faker.image.avatar(),
    information: () => faker.lorem.words(10),
};

В каждой модели прописываем данные, которые faker будет генерировать на каждый запрос. Апишка фейкера выглядит немного необычно, но довольно удобно, это очень дружелюбный DSL, реализованный с помощью цепочек выполнения, разделенный по категориям, которых более 20 (!). Используем следующие категории:

 Image

Содержит в себе методы для генерации изображений. Например, для того чтобы сгенерировать URL, содержащий аватар 128x128 пикселей нужно вызывать

faker.image.avatar()

Таким образом мы получим URL, например:

https://cdn.fakercloud.com/avatars/eduardostuart_128.jpg

Company

Возвращает информацию связанную с компанией, это может быть: companySuffix, catchPhrase, или множество других полей, нам понадобится метод companyName.

Internet

Тут нам понадобятся методы email, url. Названия говорят сами за себя.

Datatype

Генерирует случайную последовательность таких типов, как:

  • number - целые числа, предел которых можно ограничить полями min и max

  • float - действительные числа

  • arrayElements - массив

  • и т.д.

В файле db.ts для подключения созданной модели используем функцию factory из @mswjs/data:

// /src/mocks/db.ts

import { factory } from '@mswjs/data';

import {StudentModel} from './models'

export const db = factory({
    student: StudentModel
})

Запустив приложение, в браузере увидим сгенерированный список студентов:

Пример отображения данных, сгенерированных с помощью faker.js и полученных приложением по запросу GET /students
Пример отображения данных, сгенерированных с помощью faker.js и полученных приложением по запросу GET /students

По каждому запросу в консоли можно увидеть подробную информацию от MSW:

Для того, чтобы сервис-воркер перестал перехватывать тот или иной запрос, можно просто отключить/удалить соответствущий обработчик.

Вывод

Описанный подход существенно упрощает и ускоряет разработку, поскольку делает фронтенд-команду менее зависимой от бэкенда. Основными преимуществами в таком походе для меня стали:

  • возможность запуска mock-сервера непосредственно из основного проекта

  • легкое переключение между реальными и фейковыми эндпойнтами

  • быстрая и гибкая настройка обработчиков запросов

Делитесь в комментариях мнениями о таком варианте mock-сервера.

Комментарии (4)


  1. nin-jin
    26.08.2021 14:03

    Но как делать фронт, не имея 100% рабочих эндпойнтов на бэкенде?

    Ну, если у вас компоненты напрямую в бэкенд ходят, то действительно, как?!? Как такое можно допустить? Когда потребуется изменить эндпоинты вы будете бегать по всем компонентам и менять их?

    А если как у взрослых людей через модель, то она и без бэка прекрасно может работать.


    1. JustDont
      26.08.2021 14:29

      Транспортный слой всё равно придётся написать, и вот для него как раз таки что-то на бэке придется иметь (или MSW, но это такое — в вебсокеты оно не умеет, например).
      Но выше транспортного слоя — угу, причитания по поводу мокирования выглядят надуманными. В чем проблема просто напрямую положить фальшивые данные в модель?


    1. Ungla
      26.08.2021 16:50

      Ну, если у вас компоненты напрямую в бэкенд ходят, то действительно, как?!? Как такое можно допустить? Когда потребуется изменить эндпоинты вы будете бегать по всем компонентам и менять их?


      Ну например. Компоненты используют хуки useStudentsList(). Хуки используют вызовы api Api.getStudentsList(). Вызовы api используют конфиги.

      Можно пример с моделью?


      1. nin-jin
        26.08.2021 18:42
        -1

        Ну вот, хуки - это и есть модель для бедных. Вот пример реактивной модели. А тут пример её использования.