Pull to refresh
117.88
X5 Tech
Всё о технологиях в ритейле

GET запросы на практике: правила, принципы и примеры

Reading time14 min
Views18K

Я думаю, что вы не раз уже гуглили, заглядывали в статьи, манифесты IT-гигантов о лучших практиках проектирования API. Я тоже.

Но в большинстве из них всё ограничивается описанием URL ресурса, мотивацией использовать пагинацию, сложными словами про кэширование и SSL. Это, безусловно, необходимо для общего понимания технологий, но практически не помогает, когда ты сидишь перед пустой страницей и надо начать “проектировать контракт”.

Я работаю тимлидом направления системного анализа в X5Tech и за все время развития карьеры сталкивалась с большим количеством кейсов проектирования Web систем. IT продукты в большинстве очень динамичны: постоянно изменяются требования, появляются новые, итеративно улучшается пользовательский опыт (по принципу 20% усилий на 80% результата, а остальное доделаем потом).

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

В этой статье предлагаю спроектировать контракт по шагам, и на каждом из них я расскажу про общие рекомендации из копилочки “Полезное”, а также про личные правила и практики, полученные долгим опытом работы над постоянно меняющимися ИТ-продуктами, которые помогут для “дальновидного” проектирования GET REST API.

Общая теория про GET

Я не буду подробно вникать в механизмы HTTP, в философию REST API до тех пор, пока это не понадобится в конкретном примере. Но вспомним основы:

REST API, или API передачи репрезентативного состояния, представляет собой набор правил и соглашений для создания веб-служб и доступа к ним. Он позволяет приложениям взаимодействовать друг с другом по протоколу HTTP, используя стандартные методы, такие как GET, POST, PUT и DELETE.

Подробно вспомнить про концепцию можно здесь.

Лирическая вставка

Таким определением хочу обратить внимание на то, что REST – это не жёсткий протокол, а лишь набор правил и соглашений, некоторых лучших практик. Поэтому проектирование REST – это практически творческая задача, т. к. у каждой компании, команды, разработчика немного своё понимание прекрасного, полученное опытом.

GET методы используются для получения информации. Проектирование контракта обычно включает в себя выяснение следующих элементов: 

  • Тип метода – тут понятно: GET. Он используется для получения данных с сервера без их изменения.

  • Endpoint – это указание на конкретный ресурс, к которому мы хотим получить доступ. Например, если мы хотим получить информацию о пользователях, то endpoint может быть /users.

  • Query параметры – это параметры, которые мы можем передавать в URL строке после знака “?”. Например, если мы хотим получить список пользователей, отсортированных по имени и возрасту, то URL может выглядеть так: /users?sort=name&age=30. Query параметры позволяют нам фильтровать, сортировать, форматировать данные, которые мы получаем (в основном).

  • Логика обработки – это описание того, как метод будет работать и какие данные он будет получать. Это не является частью самого интерфейса, но важно при проектировании API. Например, мы можем определить, что метод GET на ресурсе /users будет возвращать список пользователей из таблицы users в формате JSON с какими-либо трансформациями.

  • Ответ – это сам контент в определённом формате, который отдаёт сервер. В данном случае мы будем оперировать форматом JSON, который является одним из самых популярных форматов для обмена данными в веб-приложениях.

Описание базовой задачи

Разбирать рекомендации и правила будем на примере двух макетов:

Макет 1. Карточка товара

Экран содержит информацию о товаре и список заявок, которые на этот товар созданы.

  • Список заявок можно сортировать по возрастанию и убыванию по одному из полей: номер заявки, статус, средняя цена, минимальная цена, максимальная цена, автор.

  • Список заявок можно фильтровать, выбрав одно значение в каждом из фильтров: номер заявки, статус, автор.

  • На каждой заявке есть пометка "!", означающая наличие ошибки в заявке (по некоторой логике, не имеет значения).

  • При наведении на автора можно увидеть дополнительную информацию о нём.

  • На каждой заявке есть кнопки: Отменить, Согласовать, Отклонить.

    • Отмена доступна только автору.

    • Согласование и отклонение доступно текущему согласованту (по некоторой логике, не имеет значения).

Макет 1. Карточка товара
Макет 1. Карточка товара
Макет без наведения
Макет 1.1. Карточка товара
Макет 1.1. Карточка товара

Макет 2. Раздел “Заявки”

Экран содержит все заявки по всем товарам. Правила те же, но дополнительно:

  • Есть дополнительный столбец Товар.

  • По столбцу Товар дополнительно доступна фильтрация и сортировка.

  • При клике на товар можно перейти в карточку товара.

Макет 2. Заявки
Макет 2. Заявки

Шаг 1. Понимаем, какой метод нужно реализовать

Первое, что нужно сделать – выявить, за какие данные будет отвечать метод. Для этого нужно проанализировать зависимость информации. На экране 1 есть два основных блока данных: данные по товару и данные по заявкам.

Должны ли они приходить одной транзакцией? 

Чтобы определить это, зададимся вопросами: 

  • Могут ли эти данные влиять друг на друга?

  • Фильтрация списка заявок повлияет на информацию о товаре? 

  • Информация о товаре зависит от заявок?

В данном случае – скорее нет. Поэтому на экране понадобятся как минимум 2 метода:

  • получение данных товара;

  • получение данных заявок.

Возьмём метод получения данных о заявках и далее будем работать с ним.

💡 Рекомендация: при понимании, какой метод нужно проектировать, оперируйте основной сущностью, которую вы возвращаете. Эту концепцию сложно объяснить без дополнительных страниц с примерами. Приведу слегка странное правило, чтобы объяснить концепцию: в таблице заявок при наведении нужно отобразить автора, при наведении на его имя отобразить имя его руководителя, а при наведении на него увидеть подчиненных. Назовём данные автора первым уровнем зависимости (зависит от заявки напрямую), данные руководителя – вторым (они зависят не от заявки, а от автора), а данные подчинённых – третьим.

В таком случае возвращать сразу всю эту информацию в методе на получение списка заявок неправильно, т. к. связано слишком много логических уровней. Чаще всего проектируют так, чтобы в ответе была основная сущность + 1 уровень зависимости (например, данные автора, расшифровка статуса, данные товара), а с остальными уровнями разбираются отдельно.

Шаг 2. Формируем URL (endpoint)

Существуют основные правила, которых придерживаются при формировании endpoint URL. В теорию вдаваться не будем, но быстро вспомним пример из книги Web API Design – Crafting Interfaces that Developers Love:

/owners/5678/dogs?q=fluffy+fur

Эндпоинт говорит о том, что есть ресурс owners (владельцы), указан ID владельца (5678), и нужно получить всех dogs этого владельца, причём, пушистых (fluffy+fur).

Наша задача: получить заявки, которые относятся к товару. Логически и интуитивно подходят следующие варианты:

  1. /products/0001/requests

  2. /requests?product=0001

Из той же книги Web API Design: Crafting Interfaces that Developers Love:

It's not uncommon to see people string these together making a URL 5 or 6 levels deep. Remember that once you have the primary key for one level, you usually don't need to include the levels above because you've already got your specific object. In other words, you shouldn't need too many cases where a URL is deeper than what we have above /resource/identifier/resource

In summary, keep your API intuitive by simplifying the associations between resources, and sweeping parameters and other complexities under the rug of the HTTP question mark.

Если сильно сократить, автор не рекомендует выходить за рамки первого уровня иерархии в URL /resource/identifier/resource

Но это правило нам пока не поможет, потому что оба эндпоинта ему удовлетворяют. Сравним их по применимости:

/products/{productId}/requests

Этот эндпоинт применим для экрана 1 (заявки по товару). Но для экрана 2 с заявками по всем товарам не применим вовсе, т. к. в URL закреплено указание одного из товаров. В таком случае для экрана 2 потребуется дополнительный эндпоинт.

Такое решение имеет место быть, особенно если экраны сильно различаются по набору полей, по логике, либо по доступам к ним.

 /requests?product={productId}

Этот эндпоинт применим для обоих экранов: 

  • для экрана 1 (заявки по товару) в параметр будет подставлен запрошенный товар;

  • для экрана 2 (все заявки) параметр не будет подставлен, и в ответе будут получены все заявки без фильтрации;

  • также применим для фильтра по товару на экране 2.

Таким образом, этот эндпоинт более универсален и может использоваться сразу на двух экранах. Иногда я пользуюсь таким ходом, и воспользуюсь им в этот раз. Но такие решения требуют хорошего погружения в бизнес-требования.

💡 Рекомендация: Не создавайте зависимость в URL, если связь между родительским и дочерним элементом больше похожа на фильтр. Вынесите эту зависимость в параметры за “?”

Шаг 3. Формирование входящих параметров

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

Технически передать входные данные для GET метода можно с помощью query параметров (в URL за знаком вопроса) и с помощью JSON Body. Также в вашем распоряжении находятся headers и cookies, но они передают более техническую информацию, поэтому пока рассматривать их не будем.

💡 Рекомендация: передача GET запроса с JSON Body считается плохой практикой с точки зрения паттерна REST, хотя сам протокол HTTP это не запрещает.

Но многие сетевые фильтры, браузеры просто теряют Body где-то по пути. Имейте это ввиду и используйте только Query параметры.

Более точно про GET и BODY

Evgeny Talyzin в комментариях объяснил эту проблему более предметно:

"Сам протокол" не "не запрещает" GET с Body. До 2014-го года RFC2616 (HTTP 1.1 Spec) декларировал что GET запрос может иметь Body, но этот Body должен быть проигнорирован сервером. Если сервер как-то обрабатывает Body, и его ответ меняется в зависимости от передаваемых в нём данных - он нарушает RFC.

После 2014-го это убрали, и заменили на "body GET запроса не имеет семантики и может вызвать реджект запроса".

Это собственно довольно заметно становится в последнее время. Всё больше библиотек просто зафейлит и исходящий GET реквест с Body (javascript fetch, Unity из тех с чем я работал), и входящий GET c Body (вот на вскидку не помню...).

Для Query параметров есть ограничения:

  • Браузеры имеют ограничения по длине URL. Не рекомендуется использовать URL длиной более 2048 символов.

  • Сложные (например иерархические) структуры сложно преобразовать в плоский вид строки.

  • Такие параметры могут сохраняться в истории.

Сортировка

С сортировкой по одному полю всё довольно просто. Классически используются 2 параметра: 

  • поле сортировки sort, sortBy, sortProperty;

  • порядок сортировки order, sortDirection.

Поля для сортировки, как и все технические значения, я предпочитаю оформлять верхним регистром, поэтому:

  • sortProperty может принимать значения: NUMBER, STATUS, AVG_PRICE и т. д.;

  • sortDirection может принимать значения ASC (по возрастанию), DESC (по убыванию).

Недолго думая, добавляем параметры с примером значений в запрос:

 /requests?sortProperty=NUMBER&sortDirection=ACS&product={productId}

Фильтры

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

💡 Напоминание: для фильтрации по списочным значениям нужно использовать ID этих значений, а не их текстовые названия (чаще всего). Например, при фильтрации по статусу “Активен” строка может выглядеть так:

/requests?status=1

/requests?status=ACTIVE

Но точно не так:

/requests?status=Активен

На предыдущем шаге мы закончили со следующим:

/requests?product=0001

На этом этапе познакомимся с практикой: готовься к худшему.

Практика: готовься к худшему

Представь, что требования могут измениться, а контракт должен остаться.

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

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

Может ли пользователь спустя время захотеть видеть заявки не по одному, а по пяти/двум/десяти своим товарам? Конечно может, даже если сейчас это не прописано в требованиях.

Упражнение состоит в том, чтобы увидеть такие нюансы, и, если есть возможность, подготовить для этого “рельсы”. 

Как подготовить эти “рельсы”?

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

Было:

/requests?product=0001

Стало:

/requests?products=0001,0002,0003

Чаще всего, такие “рельсы” стОят не дорого:

  • название параметра вообще не влияет на работу разработчиков;

  • вместо списка передать одно значение – не проблема (единственное число – это частный случай множественного); 

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

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

Таким образом, доработали пример: 

/requests?sortProperty=NUMBER&sortDirection=ACS&products=0001,0002,0003

Для упрощения опустим пока параметры сортировки:

  • Для экрана 1 используем: /requests?products=0001

  • Для экрана 2 без фильтров используем: /requests?products=

  • Для экрана 2 с фильтрами используем: /requests?products=003

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

/requests?products=0001&statuses=1,2&numbers=002,003&authors=7438,74364

Шаг 4. Формирование ответного JSON

Формирование ответного JSON у многих занимает бОльшую часть времени проектирования.

Я всегда для начала накидываю драфт по тому, что вижу на экране, не заморачиваясь со структурой, типами данных и т. д. Это помогает начать, так как проблему blank page никто не отменял.

[{
  "number": "123", // №
  "status": "Активно", // Статус
  "avgPrice": "1700", // Ср.цена
  "priceMin": "1500", // Мин. цена
  "priceMax": "1900", // Макс. цена
  "author": "XXXXXXX@mail.ru", // Автор
  "product": "Тестовый товар", // Товар
  "hasError": true // Наличие ошибки для иконки
}]

Практика: добавь id,name в объекты

Вторым шагом нужно выделить логические объекты в JSON объекты и обогатить иерархию. Здесь под объектами я имею в виду те “поля”, которые могут иметь больше, чем одно свойство. Чаще всего это:

  • справочные значения, например, статусы (имеют не только текстовое название, но и ID);

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

  • то, что логически группируется и на этапе драфта часто имеет дублирующийся “корень” в названии, например, цены, даты, флаги доступности.

В 99% для справочников и значений из списка вам потребуется не только человеко-читаемое название, но и код/ID этого значения, даже если этого не видно на макете. Например, для реализации следующего:

  • при клике на товар перейти в карточку товара;

  • при клике на сотрудника перейти на другой сайт/портал с данными о сотруднике;

  • при клике на статус заявки отфильтровать/отсортировать список по этому статусу;

  • назначить определённые цвета для статусов на клиенте (например, “Активен” – зелёный, “Отклонён” – красный).

Поэтому практика заключается в том, чтобы смело добавить эти идентификаторы наряду с визуальными названиями в объекты:

[
  {
    "id": "12345",
    "number": "123",
    "status": {
      "code": "ACTIVE",
      "name": "Активно"
    },
    "price": {
      "avg": "1700",
      "min": "1500",
      "max": "1900"
    },
    "author": {
      "id": "93784637",
      "login": "XXXXXXX@mail.ru",
      "phone": "7903XXXXXXX",
      "fullName": "Иванова Светлана",
      "job": "Ведущий менеджер"
    },
    "product": {
      "id": "001",
      "name": "Тестовый товар"
    },
    "hasError": true
  }
]

Не обязательно называть свойства именно id и name. Многие используют code, description и другие формы. Как по мне, id и name самые понятные и простые. Выбирайте то, что подходит в конкретном случае, но старайтесь соблюдать консистентность. Вот тут собрала чек-лист: Приглаживаем названия в API

Зачем это нужно?

Представим, что мы начали контракт с драфта, а остальные требования появлялись по ходу дела. Рефакторинг структуры JSON часто доставляет сложности (надо не только новое поле добавить, но и предыдущие изменить, переименовать, не упустить изменение структуры и т. д.), поэтому, скорее всего, мы бы просто добавляли в портянку текста новые поля и получили следующее:

[
  {
    "number": "123",
    "status": "Активно",
    "statusCode": "ACTIVE",
    "avgPrice": "1700",
    "priceMin": "1500",
    "priceMax": "1900",
    "author": "XXXXXXX@mail.ru",
    "authorPhone": "7903XXXXXXX",
    "authorFullName": "Иванова Светлана",
    "authorJob": "Ведущий менеджер",
    "product": "Тестовый товар",
    "productId": "001",
    "hasError": true
  }
]

Такой контракт и читать неудобно, и описывать, и заполнять.

Итог практики:

[
  {
    "id": "12345",
    "number": "123",
    "status": {
      "code": "ACTIVE",
      "name": "Активно"
    },
    "price": {
      "avg": "1700",
      "min": "1500",
      "max": "1900"
    },
    "author": {
      "id": "93784637",
      "login": "XXXXXXX@mail.ru",
      "phone": "7903XXXXXXX",
      "fullName": "Иванова Светлана",
      "job": "Ведущий менеджер"
    },
    "product": {
      "id": "001",
      "name": "Тестовый товар"
    },
    "hasError": true
  }
]

Практика: оберни ответ в объект

Мы уже поработали с объектами внутри JSON, но не учли, что сам ответ (в данном случае массив заявок) тоже является объектом и у него тоже могут появиться дополнительные свойства, например:

  • постраничное отображение (пагинация);

  • рядом стоящая цифра с общим количеством заявок;

  • средняя цена из средних цен в выборке с учётом фильтрации (фантазия, на макете не представлено).

Макет 1.2. Карточка товара + пагинация
Макет 1.2. Карточка товара + пагинация

На данном этапе ответ представляет собой массив, в который просто некуда добавить любые свойства, относящиеся ко всему ответу, а не к каждому элементу.

Практика заключается в том, чтобы обернуть весь ответ в объект JSON, например так:

{
  "content": [
    {
      "id": "12345",
      "number": "123",
      "status": {
        "code": "ACTIVE",
        "name": "Активно"
      },
      "price": {
        "avg": "1700",
        "min": "1500",
        "max": "1900"
      },
      "author": {
        "id": "93784637",
        "login": "XXXXXXX@mail.ru",
        "phone": "7903XXXXXXX",
        "fullName": "Иванова Светлана",
        "job": "Ведущий менеджер"
      },
      "product": {
        "id": "001",
        "name": "Тестовый товар"
      },
      "hasError": true
    }
  ]
}

В такую структуру отлично впишутся данные о пагинации и общем количестве заявок:

Ответ JSON с пагинацией
{
  "pageNumber": 0,
  "pageSize": 2,
  "totalCount": 221,
  "content": [
    {
      "id": "12345",
      "number": "123",
      "status": {
        "code": "ACTIVE",
        "name": "Активно"
      },
      "price": {
        "avg": "1700",
        "min": "1500",
        "max": "1900"
      },
      "author": {
        "id": "93784637",
        "login": "XXXXXXX@mail.ru",
        "phone": "7903XXXXXXX",
        "fullName": "Иванова Светлана",
        "job": "Ведущий менеджер"
      },
      "product": {
        "id": "001",
        "name": "Тестовый товар"
      },
      "hasError": true
    }
  ]
}

Можно пойти дальше и обернуть ещё разок, если требуется возвращать какую-либо ещё информацию об ответе:

Ответ JSON с уровнем result
{
  "description": "Найдены заявки по фильтрам ...",
  "result": {
    "pageNumber": 0,
    "pageSize": 2,
    "totalCount": 221,
    "content": [
      {
        "id": "12345",
        "number": "123",
        "status": {
          "code": "ACTIVE",
          "name": "Активно"
        },
        "price": {
          "avg": "1700",
          "min": "1500",
          "max": "1900"
        },
        "author": {
          "id": "93784637",
          "login": "XXXXXXX@mail.ru",
          "phone": "7903XXXXXXX",
          "fullName": "Иванова Светлана",
          "job": "Ведущий менеджер"
        },
        "product": {
          "id": "001",
          "name": "Тестовый товар"
        },
        "hasError": true
      }
    ]
  }
}

Шаг 5. Практика: откуда я это возьму?

Этот шаг нужен для финализации и перепроверки того, что получилось. 

Теперь нужно ещё раз взглянуть на каждый элемент макета и логики клиента (в нашем случае frontend), и задаться вопросом: “Откуда это можно взять? Есть ли это в ответе?”.

Для демонстрации я припасла и не напоминала про следующие бизнес-правила из задачи:

  1. “Автор заявки может её отменить до момента согласования”.

  2. “Текущий согласующий может отклонить или согласовать заявку”.

Какие можно предположить варианты:

  1. В ответе отдать id автора (уже есть), статус (уже есть), id согласующего и прописать эту логику на клиенте.

  2. Сформировать логику на сервере и в ответе отдать список разрешений, например:

    1. canCancel: true,

    2. canApprove: false,

    3. canDecline: false.

Решение зависит от:

  • комплексности (сколько свойств участвуют в условии);

  • сложности (варианты сочетания этих свойств);

  • потенциала к изменению (может, вы планируете сделать админку, где будете управлять возможностями гибко и эти правила будут динамичны);

  • сроков (можем ли мы себе позволить перенести логику на сервер, заложив рельсы на гибкость, но потратив чуть больше времени).

Чем больше каждый из показателей, тем вероятнее вариант 2 – так и сделаем. При этом учтём правило объектов и соберём “права” в объект rights:

Ответ JSON с правами rights
{
  "description": "Фильтрация по автору в данный момент недоступна, получены заявки без учета фильтра",
  "result": {
    "pageNumber": 0,
    "pageSize": 2,
    "totalCount": 221,
    "content": [
      {
        "id": "12345",
        "number": "123",
        "status": {
          "code": "ACTIVE",
          "name": "Активно"
        },
        "price": {
          "avg": "1700",
          "min": "1500",
          "max": "1900"
        },
        "author": {
          "id": "93784637",
          "login": "XXXXXXX@mail.ru",
          "phone": "7903XXXXXXX",
          "fullName": "Иванова Светлана",
          "job": "Ведущий менеджер"
        },
        "product": {
          "id": "001",
          "name": "Тестовый товар"
        },
        "hasError": true,
        "rights": {
          "canCancel": true,
          "canApprove": true,
          "canDecline": true
        }
      }
    ]
  }
}

 💡 Рекомендация: практика “Откуда я это возьму?” справедлива и для продумывания логики на сервере, и при формировании пула входящих параметров. Например, как сервер поймёт, какой пользователь обращается и можно ли ему выдать доступ? Является ли он автором или текущим согласующим?

Спойлер: для этого нужно авторизоваться и в запросе передать авторизационные данные. Но об этом в другой раз.

Итог

В результате мы получили следующий метод:

/requests?sortProperty=NUMBER&sortDirection=ACS&products=0001&statuses=1,2,3&numbers=002,003&authors=7438,74364

С ответом: 

{
  "description": "Фильтрация по автору в данный момент недоступна, получены заявки без учета фильтра",
  "result": {
    "pageNumber": 0,
    "pageSize": 2,
    "totalCount": 221,
    "content": [
      {
        "id": "12345",
        "number": "123",
        "status": {
          "code": "ACTIVE",
          "name": "Активно"
        },
        "price": {
          "avg": "1700",
          "min": "1500",
          "max": "1900"
        },
        "author": {
          "id": "93784637",
          "login": "XXXXXXX@mail.ru",
          "phone": "7903XXXXXXX",
          "fullName": "Иванова Светлана",
          "job": "Ведущий менеджер"
        },
        "product": {
          "id": "001",
          "name": "Тестовый товар"
        },
        "hasError": true,
        "rights": {
          "canCancel": true,
          "canApprove": true,
          "canDecline": true
        }
      }
    ]
  }
}

По моему мнению, мы добились следующего:

  • можно добавлять новые поля в объекты без дублирования названия родительского поля: это упростит и разработку, и проектирование, и тестирование;

  • для справочных значений на клиенте можно оперировать ID, а не только названием: можно добавлять любые клики, переходы на всё, что есть в ответе, не привлекая команду сервера и не изменяя контракт;

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

  • не сделали ничего сложного, но упростили дальнейшую жизнь.


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

Tags:
Hubs:
Total votes 28: ↑26 and ↓2+24
Comments11

Articles

Information

Website
www.x5.ru
Registered
Founded
2006
Employees
over 10,000 employees
Location
Россия