Что делать, если мы достигли состояния разработки пользовательского интерфейса, когда нам нужно взаимодействовать с API, но их нет?

Прежде всего, нам нужен контракт API от команды разработчиков бэкенда. Это может быть что-то вроде документа схемы API или документа OpenAPI.

Способы продвижения в пользовательском интерфейсе с контрактами API

  1. Самый простой способ — закомментировать функцию вызова API некоторыми статическими данными Mock. Очевидная проблема здесь в том, что мы застряли с одним набором статических данных.
  2. Используйте предварительно настроенный фиктивный сервер, например сервер JSON.
    Это лучше, чем первый подход, позволяет быстро приступить к работе и поддерживает динамические данные. Но структура API строгая и регулируется сервером JSON. Возможно, не удастся воспроизвести наши фактические маршруты API с сервером JSON.
  3. Используйте клиентские библиотеки сетевых перехватчиков, такие как axios-mock-adapter или Pretender.

Какой была бы идеальная установка для насмешек?

  1. Мы должны иметь возможность динамически перечислять/создавать/обновлять/удалять данные.
  2. Мы должны быть в состоянии воспроизвести наши фактические маршруты API.
  3. У нас должно быть минимальное количество отклонений в нашем производственном коде от использования настройки Mock.
  4. Одним словом, он должен быть максимально приближен к реальности.

Мираж

Он написан поверх библиотеки претендентов и работает изолированно от вашего производственного кода. Он работает в вашем браузере на стороне клиента, перехватывая запросы XMLHttpRequest и Fetch.

Самым большим преимуществом Mirage перед любой другой клиентской библиотекой макетов является база данных в памяти. Мы можем определить модели, которые наследуют полезные функции, такие как создание, обновление, удаление записей и методы запросов, такие как find, findBy, where и т. д.

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

Пример базовой настройки Mirage

Предположим, у нас есть пользовательский интерфейс, который показывает списки автомобилей и позволяет пользователю добавлять в список другие автомобили.

Итак, у нас есть приведенная ниже информация об API и схеме.

GET /api/cars : to fetch list of cars
POST /api/car : to add new car to database
Car Schema 
{
   name: <string>
   type: <string>
   transmission": <string>,
   capacity: <number>,
   manufacturer: <companyId>
}
Company Schema 
{
   name: <string> 
   carIds: <array>
}

Давайте настроим Mirage

Установите «miragejs» из npm и импортируйте метод «createServer».
Этот метод возвращает экземпляр сервера. Когда экземпляр Mirage Server будет создан, он начнет принимать сетевые запросы.

Теперь мы будем мокнуть первый API для получения списка автомобилей.
Нам нужна одна коллекция для автомобилей в БД Mirage, поэтому мы должны сообщить об этом Mirage, добавив модель.
А затем мы добавляем маршрут для получения списка автомобилей

import { createServer } from ‘miragejs’;
export default createServer({
 models: {
  car: Model
 },
 routes() {
  this.namespace = “/api”;
  this.get(‘cars’, (schema, request) => {
    return schema.cars.all();
  });
 }
});

Теперь, когда наше приложение вызывает /api/cars, оно перехватывается Mirage и вызывает обработчик маршрута, который мы предоставили вместе с API.

this.get(‘cars’, (schema, request) => {
    return schema.cars.all();
  });

обработчик маршрута предоставляет схему двух параметров и запрос. Используя схему, мы можем найти коллекции и модели в БД. Итак, когда мы пишем «schema.cars.all», мы получаем доступ к коллекции cars с помощью объекта схемы, а затем вызываем метод коллекции «all», чтобы получить список всех автомобилей.
Объект запроса содержит URL, тело, параметры запроса, заголовки и т. д.

Теперь, если мы хотим поддерживать API, такой как /api/cars?name=somecar, мы изменим наш обработчик, как показано ниже.

this.get(‘cars’, (schema, request) => {
    if(request.queryParams){
      return schema.cars.findBy(request.queryParams)
    }
    return schema.cars.all();
  });

Теперь ответ, который мы получаем, пуст, потому что у нас еще ничего нет в коллекции автомобилей.

Добавим несколько начальных записей

Mirage config имеет свойство seed, которое мы можем использовать для добавления некоторых записей при запуске.

import { createServer } from ‘miragejs’;
export default createServer({
 models: {
  car: Model, 
  company: Model 
 },
seeds(server) {
  server.create('car', {
   name: 'CAR1',
   transmission: 'Manual',
   type: 'sedan',
   capacity: 5,
   manufacturerId: 1
  });
server.create('car', {
   name: 'CAR2',
   transmission: 'Automatic',
   type: 'hatchback',
   capacity: 4,
   manufacturerId: 1
  })
  
  server.create('company', {
    name: 'Tata'
  })
 },
 routes() {
  this.namespace = “/api”;
  this.get(‘cars’, (schema, request) => {
    return schema.cars.all();
  });
 }
});

Теперь мы получаем список автомобилей при вызове /api/cars.

{
  "cars": [
    {
      "name": "CAR1",
      "transmission": "Manual",
      "type": "sedan",
      "capacity": 5,
      "id": "1"
    },
    {
      "name": "CAR2",
      "transmission": "Automatic",
      "type": "hatchback",
      "capacity": 4,
      "id": "2"
    }
  ]
}

Добавим информацию о производителе в API автомобилей

Чтобы добавить производителя, мы расширим объект модели автомобиля и добавим отношение производителя к автомобилю.
Mirage предоставляет два метода ownTo и hasMany для определения отношений.
Поскольку автомобиль может принадлежать только одной компании, мы определим это как принадлежит()

models: {
  car: Model.extend({
   manufacturer: belongsTo('company')
  }), 
  company: Model
 },

Но если мы снова вызовем /api/cars, мы получим тот же ответ без информации о производителе.
Теперь, чтобы получить информацию о производителе, включенную в ответ, мы добавим в нашу конфигурацию нечто, называемое сериализатором. Сериализатор предоставляет крючки для изменения ответа наших API. Mirage предоставляет несколько предопределенных сериализаторов. Мы будем использовать RestSerializer.

import { createServer, Model, belongsTo, hasMany, RestSerializer } from ‘miragejs’;
export default createServer({
  serializers: {
      car: RestSerializer.extend({
        embed: true,
        include: ['manufacturer']
      })
    },
  ....
})

Теперь получаем ответ производителя

{
  "cars": [
    {
      "name": "CAR1",
      "transmission": "Manual",
      "type": "sedan",
      "capacity": 5,
      "id": "1",
      "manufacturer": {
        "name": "Honda",
        "id": "1"
      }
    },
    {
      "name": "CAR2",
      "transmission": "Automatic",
      "type": "hatchback",
      "capacity": 4,
      "id": "2",
      "manufacturer": {
        "name": "Honda",
        "id": "1"
      }
    }
  ]
}

Давайте изменим ответ, чтобы он соответствовал нашим рабочим API.

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

{
  data:[{ 
    name:'CAR1',
    "transmission": "Automatic",
    "type": "hatchback",
    "capacity": 4,
    ...
  }],
  metadata: {}
}

Итак, теперь мы реализуем метод сериализации

import { createServer, Model, belongsTo, hasMany, RestSerializer } from ‘miragejs’;
export default createServer({
  serializers: {
      car: RestSerializer.extend({
        embed: true,
        include: ['manufacturer'],
        serialize(object, request) {
          // call base call method
          let json = RestSerializer.prototype.serialize.apply(this, arguments);
        return {
            metadata: {},
            data: json[object.modelName] ||         json[this._container.inflector.pluralize(object.modelName)]
          }
        }      
      })
    },
  ....
})

Теперь мы получаем приведенный ниже ответ по желанию

{
  "metadata": {},
  "data": [
    {
      "name": "CAR1",
      "transmission": "Manual",
      "type": "sedan",
      "capacity": 5,
      "id": "1",
      "manufacturer": {
        "name": "Honda",
        "id": "1"
      }
    },
    {
      "name": "CAR2",
      "transmission": "Automatic",
      "type": "hatchback",
      "capacity": 4,
      "id": "2",
      "manufacturer": {
        "name": "Honda",
        "id": "1"
      }
    }
  ]
}

Окончательный код (вы можете посетить https://miragejs.com/repl/ и запустить его)

import { createServer, Model, belongsTo, hasMany, RestSerializer } from 'miragejs';
const AppSerializer = RestSerializer
.extend({
        attrs:['name', 'type'],
        serialize(object, request) {
          console.log(object.modelName)
          let json = RestSerializer.prototype.serialize.apply(this, arguments);
        return {
            metadata: {},
            data: json[object.modelName] || json[this._container.inflector.pluralize(object.modelName)]
          }
        }
      })
export default
  createServer({
    serializers: {
      car: AppSerializer.extend({
        embed: true,
        include: ['manufacturer']
      })
    },
    models: {
      car: Model.extend({
        manufacturer: belongsTo('company')
      }),
      company: Model.extend({
        car: hasMany()
      })
    },
    seeds(server) {
      server.create('company', {
        name: 'Honda',
        id:1
      })
      
      server.schema.cars.create({
        name: 'CAR1',
        transmission: 'Manual',
        type: 'sedan',
        capacity: 5,
        manufacturerId: 1
      });
      server.create('car', {
        name: 'CAR2',
        transmission: 'Automatic',
        type: 'hatchback',
        capacity: 4,
        manufacturerId: 1
      })
    },
    routes() {
      this.namespace = "/api";
      this.get('cars', (schema, request) => {
        const qParams = request.queryParams;
        if(Object.keys(qParams).length) {
          return schema.cars.findBy(qParams);
        }
        return schema.cars.all();
      });
      this.get('/car/:id', (schema, request) => {
        const id = request.params.id;
        return schema.cars.find(id);
      });
      
      this.post('/car', (schema, request) => {
        const body = JSON.parse(request.requestBody)
        return schema.cars.create(body);
      })
      
      this.get('/companies', (schema, request) => {
        return schema.companies.all();
      })
      
      this.post('/company', (schema, request) => {
        const body = JSON.parse(request.requestBody)
        return schema.companies.create(body);
      })
      this.passthrough();
    }
  });

Посетите https://miragejs.com/, чтобы узнать больше о функциях Mirage. Это только охватывает необходимые основы.

PS: когда мы разрабатываем функции с использованием этой методологии, мы получаем набор функций пользовательского интерфейса, которые работают без внутренней поддержки, и это действительно полезно для предоставления предсказуемой презентации/демонстрации.