Привет, Хабр! Меня зовут Виктор, я фронтенд-работчик в 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 < 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
})
Запустив приложение, в браузере увидим сгенерированный список студентов:
По каждому запросу в консоли можно увидеть подробную информацию от MSW:
Для того, чтобы сервис-воркер перестал перехватывать тот или иной запрос, можно просто отключить/удалить соответствущий обработчик.
Вывод
Описанный подход существенно упрощает и ускоряет разработку, поскольку делает фронтенд-команду менее зависимой от бэкенда. Основными преимуществами в таком походе для меня стали:
возможность запуска mock-сервера непосредственно из основного проекта
легкое переключение между реальными и фейковыми эндпойнтами
быстрая и гибкая настройка обработчиков запросов
Делитесь в комментариях мнениями о таком варианте mock-сервера.
nin-jin
Ну, если у вас компоненты напрямую в бэкенд ходят, то действительно, как?!? Как такое можно допустить? Когда потребуется изменить эндпоинты вы будете бегать по всем компонентам и менять их?
А если как у взрослых людей через модель, то она и без бэка прекрасно может работать.
JustDont
Транспортный слой всё равно придётся написать, и вот для него как раз таки что-то на бэке придется иметь (или MSW, но это такое — в вебсокеты оно не умеет, например).
Но выше транспортного слоя — угу, причитания по поводу мокирования выглядят надуманными. В чем проблема просто напрямую положить фальшивые данные в модель?
Ungla
Ну например. Компоненты используют хуки useStudentsList(). Хуки используют вызовы api Api.getStudentsList(). Вызовы api используют конфиги.
Можно пример с моделью?
nin-jin
Ну вот, хуки - это и есть модель для бедных. Вот пример реактивной модели. А тут пример её использования.