company_banner

Опыт Rambler Group: как мы начали полностью контролировать формирование и поведение фронтовых React компонентов


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

    На наш взгляд нам удалось реализовать неплохой пример сбалансированного по сложности и профиту решения, который мы успешно используем в production на основе Symfony и React.

    Какой формат обмена данными мы можем выбрать, планируя разработку API бэкенда в активно разрабатываемом веб-продукте, содержащем динамические формы со связанными полями и сложную бизнес-логику?

    • SWAGGER — вариант неплохой, есть документация и удобные инструменты для отладки. Тем более для Symfony есть библиотеки которые позволяют автоматизировать процесс, но к сожалению JSON Schema оказалась предпочтительнее;
    • JSON Schema — данный вариант предложили фронтенд разработчики. У них уже были библиотеки, позволяющие на его основе выводить формы. Это и определило наш выбор. Формат позволяет описывать примитивные проверки, которые можно сделать в браузере. Так же есть документация, которая описывает все возможные варианты схемы;
    • GraphQL — довольно молод. Не такое большое количество server side и фронтенд библиотек. На момент создания системы не рассматривался, в перспективе — оптимальный способ создания API, об этом будет отдельная статья;
    • SOAP — имеет строгую типизацию данных, возможность построить документацию, но его не так-то просто подружить с React фронтом. Также SOAP имеет больший overhead на один и тот же полезный объем передаваемых данных;

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

    • высокая вероятность багов;
    • зачастую не 100% документация и покрытие тестами;
    • низкая «модульность» в силу закрытости программного API. Обычно такие решения пишутся под монолит и не подразумевают шаринг между проектами в виде компонентов, так как это требует особого архитектурного построения (читай удорожания разработки);
    • высокий уровень вхождения новых разработчиков. Чтобы понять всю крутость велосипеда может потребоваться много времени;

    Поэтому хорошей практикой является использование распространенных и стабильных библиотек (вроде left-pad из npm) по правилу — лучший код это тот, который ты так и не написал, а бизнес-задачу решил. Разработка бэкенда веб-решений в рекламных технологиях Rambler Group ведется на Symfony. На всех используемых компонентах фреймворка останавливаться не будем, ниже поговорим о главной части, на базе которой реализована работа — Symfony form. На фронтенде используется React и соответствующая библиотека, расширяющая JSON Schema под WEB специфику — React JSON Schema Form.

    Общая схема работы:



    Подобный подход дает много плюсов:

    • документация генерируется из коробки, как и возможность построить автоматические тесты — опять же по схеме;
    • все передаваемые данные типизированы;
    • есть возможность передать информацию о базовых валидационных правилах;
      быстрая интеграция транспортного уровня в React — за счет библиотеки React JSON Schema от Mozilla;
    • возможность на фронтенде из коробки генерировать web компоненты за счет интеграции bootstrap;
    • логическая группировка, набор валидаций и возможных значений HTML элементов, а также вся бизнес логика контролируется в единой точке — на бэкенде, нет дублирования кода;
    • максимально просто портировать приложение на другие платформы — view часть отделена от управляющей (см. предыдущий пункт), вместо React и браузера рендерингом и обработкой запросов пользователя может выступать Android или iOS приложение;

    Давайте рассмотрим компоненты и схему их взаимодействия подробнее.

    Изначально JSON Schema позволяет описывать примитивные проверки, которые можно сделать на клиенте, вроде обязательности или типизации различных частей схемы:

    const schema = {
      "title": "A registration form",
      "description": "A simple form example.",
      "type": "object",
      "required": [
        "firstName",
        "lastName"
      ],
      "properties": {
        "firstName": {
          "type": "string",
          "title": "First name"
        },
        "lastName": {
          "type": "string",
          "title": "Last name"
        },
        "password": {
          "type": "string",
          "title": "Password",
          "minLength": 3
        },
        "telephone": {
          "type": "string",
          "title": "Telephone",
          "minLength": 10
        }
      }
    }
    

    Для работы со схемой на фронтенде есть популярная библиотека React JSON Schema Form, дающая необходимые для веб-разработки надстройки над JSON Schema:

    uiSchema — сама JSON Schema определяет тип передаваемых параметров, но для построения веб-приложения этого недостаточно. Например поле типа String может быть представлено в виде <input… /> или в виде <textarea… />, это важные нюансы, с учетом которых нужно правильно отрисовать схему для клиента. Для передачи этих нюансов и служит uiSchema, например для представленной выше JSON Schema можно уточнить визуальную веб-составляющую следующей uiSchema:

    const uiSchema = {
      "firstName": {
        "ui:autofocus": true,
        "ui:emptyValue": ""
      },
      "age": {
        "ui:widget": "updown",
        "ui:title": "Age of person",
        "ui:description": "(earthian year)"
      },
      "bio": {
        "ui:widget": "textarea"
      },
      "password": {
        "ui:widget": "password",
        "ui:help": "Hint: Make it strong!"
      },
      "date": {
        "ui:widget": "alt-datetime"
      },
      "telephone": {
        "ui:options": {
          "inputType": "tel"
        }
      }
    }
    

    Live Playground пример можно посмотреть здесь.

    При таком использовании схемы рендеринг на фронтенде будет реализован стандартными компонентами bootstrap в несколько строк:

    render((
      <Form schema={schema}
            uiSchema={uiSchema} />
    ), document.getElementById("app"));
    

    Если стандартные виджеты, поставляемые с bootstrap вас не устраивают и нужна кастомизация — для некоторых типов данных можно указать в uiSchema свои шаблоны, на момент написания статьи поддержаны string, number, integer, boolean.

    FormData — содержит данные формы, например:

    {
      "firstName": "Chuck",
      "lastName": "Norris",
      "age": 78,
      "bio": "Roundhouse kicking asses since 1940",
      "password": "noneed"
    }
    

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

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

    Из коробки библиотека позволяет работать только с этими тремя секциями, но для полноценного веб-приложения необходимо добавить еще ряд фич:

    Errors — необходимо также уметь передавать ошибки различных проверок бэкенда для отрисовке пользователю, причем ошибки могут быть как простые валидационные — например на уникальность логина при регистрации пользователя, так и более сложные исходя из бизнес логики — т.е. мы должны уметь кастомизировать их (ошибок) количество и тексты отображаемых уведомлений. Для этого в набор передаваемых данных, помимо описанных выше, была добавлена секция Errors — для каждого поля здесь определен список ошибок для отрисовки

    Action, Method — для отправки на бэкенд подготовленных пользователем данных были добавлены два атрибута, содержащих URL бэкенд контроллера осуществляющего обработку и HTTP метод доставки

    В итоге для коммуникации между фронтом и бэком получился json со следующими секциями:

    {
      "action": "https://...",
      "method": "POST",
      "errors":{},
      "schema":{},
      "formData":{},
      "uiSchema":{}
    }
    

    Но как генерировать эти данные на бэкенде? На момент создания системы не было готовых библиотек, позволяющих конвертировать Symfony Form в JSON Schema. Сейчас они уже появились, но имеют свои недостатки — например LiformBundle довольно свободно трактует JSON Schema и меняет стандарт по своему усмотрению, поэтому, к сожалению, пришлось писать свою реализацию.

    В качестве основы для генерации используются стандартные Symfony form. Достаточно использовать builder и добавить необходимые поля:
    Пример формы
    $builder
       ->add('title', TextType::class, [
           'label' => 'label.title',
           'attr' => [
               'title' => 'title.title',
           ],
       ])
       ->add('description', TextareaType::class, [
           'label' => 'label.description',
           'attr' => [
               'title' => 'title.description',
           ],
       ])
       ->add('year', ChoiceType::class, [
           'choices' => range(1981, 1990),
           'choice_label' => function ($val) {
               return $val;
           },
           'label' => 'label.year',
           'attr' => [
               'title' => 'title.year',
           ],
       ])
       ->add('genre', ChoiceType::class, [
           'choices' => [
               'fantasy',
               'thriller',
               'comedy',
           ],
           'choice_label' => function ($val) {
               return 'genre.choice.'.$val;
           },
           'label' => 'label.genre',
           'attr' => [
               'title' => 'title.genre',
           ],
       ])
       ->add('available', CheckboxType::class, [
           'label' => 'label.available',
           'attr' => [
               'title' => 'title.available',
           ],
       ]);
    


    На выходе эта форма преобразуется в схему вида:
    Пример JsonSchema
    {
     "action": "//localhost/create.json",
     "method": "POST",
     "schema": {
       "properties": {
         "title": {
           "maxLength": 255,
           "minLength": 1,
           "type": "string",
           "title": "label.title"
         },
         "description": {
           "type": "string",
           "title": "label.description"
         },
         "year": {
           "enum": [
             "1981",
             "1982",
             "1983",
             "1984",
             "1985",
             "1986",
             "1987",
             "1988",
             "1989",
             "1990"
           ],
           "enumNames": [
             "1981",
             "1982",
             "1983",
             "1984",
             "1985",
             "1986",
             "1987",
             "1988",
             "1989",
             "1990"
           ],
           "type": "string",
           "title": "label.year"
         },
         "genre": {
           "enum": [
             "fantasy",
             "thriller",
             "comedy"
           ],
           "enumNames": [
             "genre.choice.fantasy",
             "genre.choice.thriller",
             "genre.choice.comedy"
           ],
           "type": "string",
           "title": "label.genre"
         },
         "available": {
           "type": "object",
           "title": "label.available"
         }
       },
       "required": [
         "title",
         "description",
         "year",
         "genre",
         "available"
       ],
       "type": "object"
     },
     "formData": {
       "title": "",
       "description": "",
       "year": "",
       "genre": ""
     },
     "uiSchema": {
       "title": {
         "ui:help": "title.title",
         "ui:widget": "text"
       },
       "description": {
         "ui:help": "title.description",
         "ui:widget": "textarea"
       },
       "year": {
         "ui:widget": "select",
         "ui:help": "title.year"
       },
       "genre": {
         "ui:widget": "select",
         "ui:help": "title.genre"
       },
       "available": {
         "ui:help": "title.available",
         "ui:widget": "checkbox"
       },
       "ui:widget": "mainForm"
     }
    }
    


    Весь код, преобразовывающий формы в JSON закрытый и используется только в Rambler Group, если у сообщества будет интерес к этой теме — мы отрефакторим выложим ее в формате бандла в наш github репозиторий.

    Давайте рассмотрим еще несколько аспектов без реализации которых сложно построить современное веб-приложение:

    Валидация полей


    Она задается с помощью symfony validator, описывающих правила валидации объекта, пример валидатора:

    <property name="title">
        <constraint name="Length">
            <option name="min">1</option>
            <option name="max">255</option>
            <option name="minMessage">title.min</option>
            <option name="maxMessage">title.max</option>
        </constraint>
        <constraint name="NotBlank">
            <option name="message">title.not_blank</option>
        </constraint>
    </property>
    


    В данном примере constrain типа NotBlank модифицирует схему, добавляя поле в массив required полей схемы, а constrain типа Length добавляет атрибуты schema->properties->title->maxLength и schema->properties->title->minLength, которые уже должна учитывать валидация на фронтенде.

    Группировка элементов


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

    Очевидным является решение разделить форму на логические группы управляющих элементов чтобы пользователю было проще ориентироваться и делать меньше ошибок:

    Как вы знаете, возможности Symfony Form из коробки довольно большие — например формы могут наследоваться от других форм, это удобно, но в нашем случае есть минусы. В текущей реализации порядок в JSON Schema определяет порядок отрисовки элемента формы в браузере, наследование может этот порядок нарушать. Одним из вариантов было группировать элементы, например:

    Пример вложенной формы
    $info = $builder
       ->create('info',FormType::class,['inherit_data'=>true])
       ->add('title', TextType::class, [
           'label' => 'label.title',
           'attr' => [
               'title' => 'title.title',
           ],
       ])
       ->add('description', TextareaType::class, [
           'label' => 'label.description',
           'attr' => [
               'title' => 'title.description',
           ],
       ]);
    
    $builder
       ->add($info)
       ->add('year', ChoiceType::class, [
           'choices' => range(1981, 1990),
           'choice_label' => function ($val) {
               return $val;
           },
           'label' => 'label.year',
           'attr' => [
               'title' => 'title.year',
           ],
       ])
       ->add('genre', ChoiceType::class, [
           'choices' => [
               'fantasy',
               'thriller',
               'comedy',
           ],
           'choice_label' => function ($val) {
               return 'genre.choice.'.$val;
           },
           'label' => 'label.genre',
           'attr' => [
               'title' => 'title.genre',
           ],
       ])
       ->add('available', CheckboxType::class, [
           'label' => 'label.available',
           'attr' => [
               'title' => 'title.available',
           ],
       ]);
    


    Такая форма будет преобразована в схему вида:

    Пример вложенной JsonSchema
    "schema": {
       "properties": {
         "info": {
           "properties": {
             "title": {
               "type": "string",
               "title": "label.title"
             },
             "description": {
               "type": "string",
               "title": "label.description"
             }
           },
           "required": [
             "title",
             "description"
           ],
           "type": "object"
         },
         "year": {
           "enum": [
             "1981",
             "1982",
             "1983",
             "1984",
             "1985",
             "1986",
             "1987",
             "1988",
             "1989",
             "1990"
           ],
           "enumNames": [
             "1981",
             "1982",
             "1983",
             "1984",
             "1985",
             "1986",
             "1987",
             "1988",
             "1989",
             "1990"
           ],
           "type": "string",
           "title": "label.year"
         },
         "genre": {
           "enum": [
             "fantasy",
             "thriller",
             "comedy"
           ],
           "enumNames": [
             "genre.choice.fantasy",
             "genre.choice.thriller",
             "genre.choice.comedy"
           ],
           "type": "string",
           "title": "label.genre"
         },
         "available": {
           "type": "object",
           "title": "label.available"
         }
       },
       "required": [
         "info",
         "year",
         "genre",
         "available"
       ],
       "type": "object"
     }
    


    и соответствующую uiSchema
    "uiSchema": {
       "info": {
         "title": {
           "ui:help": "title.title",
           "ui:widget": "text"
         },
         "description": {
           "ui:help": "title.description",
           "ui:widget": "textarea"
         },
         "ui:widget": "form"
       },
       "year": {
         "ui:widget": "select",
         "ui:help": "title.year"
       },
       "genre": {
         "ui:widget": "select",
         "ui:help": "title.genre"
       },
       "available": {
         "ui:help": "title.available",
         "ui:widget": "checkbox"
       },
       "ui:widget": "group"
     }
    


    Данный способ группировки нам не подошел так как форма для данных начинает зависеть от представления и ее нельзя использовать, к примеру, в API или других формах. Было решено использовать дополнительные параметры в uiSchema не поломав текущий стандарт JSON Schema. В итоге в симфоневую форму добавили дополнительные опции примерно такого вида:

    'fieldset' => [
       'groups' => [
           [
               'type' => 'base',
               'name' => 'info',
               'fields' => ['title', 'description'],
               'order' => ['title', 'description']
           ]
       ],
       'type' => 'base'
    ]
    

    Это будет преобразовано в следующую схему:

    "ui:group": {
        "type": "base",
        "groups": [
        {
            "type": "group",
            "name": "info",
            "title": "legend.info",
            "fields": [
            "title",
            "description"
            ],
            "order": [
            "title",
            "description"
            ]
        }
        ],
        "order": [
        "info"
        ]
    },
    


    Полная версия schema и uiSchema
    "schema": {
       "properties": {
         "title": {
           "maxLength": 255,
           "minLength": 1,
           "type": "string",
           "title": "label.title"
         },
         "description": {
           "type": "string",
           "title": "label.description"
         },
         "year": {
           "enum": [
             "1989",
             "1990"
           ],
           "enumNames": [
             "1989",
             "1990"
           ],
           "type": "string",
           "title": "label.year"
         },
         "genre": {
           "enum": [
             "fantasy",
             "thriller",
             "comedy"
           ],
           "enumNames": [
             "genre.choice.fantasy",
             "genre.choice.thriller",
             "genre.choice.comedy"
           ],
           "type": "string",
           "title": "label.genre"
         },
         "available": {
           "type": "boolean",
           "title": "label.available"
         }
       },
       "required": [
         "title",
         "description",
         "year",
         "genre",
         "available"
       ],
       "type": "object"
     }
    

    "uiSchema": {
       "title": {
         "ui:help": "title.title",
         "ui:widget": "text"
       },
       "description": {
         "ui:help": "title.description",
         "ui:widget": "textarea"
       },
       "year": {
         "ui:widget": "select",
         "ui:help": "title.year"
       },
       "genre": {
         "ui:widget": "select",
         "ui:help": "title.genre"
       },
       "available": {
         "ui:help": "title.available",
         "ui:widget": "checkbox"
       },
       "ui:group": {
         "type": "base",
         "groups": [
           {
             "type": "group",
             "name": "info",
             "title": "legend.info",
             "fields": [
               "title",
               "description"
             ],
             "order": [
               "title",
               "description"
             ]
           }
         ],
         "order": [
           "info"
         ]
       },
       "ui:widget": "fieldset"
     }
    


    Так как на стороне фронтенда используемая нами React библиотека этого не поддерживает из коробки, пришлось добавить эту функциональность самим. С добавлением нового элемента «ui:group» мы получаем возможность полностью контролировать процесс группировки элементов и форм используя текущий API.

    Динамические формы


    Что, если, одно поле зависит от другого, например, выпадающий список подкатегорий зависит от выбранной категории?



    Symfony FORM дает нам возможность с помощью Event’ов делать динамические формы, но, к сожалению, на момент реализации эту возможность не поддерживала JSON Schema, хотя в последних версиях эта возможность появилась. Первоначально была идея отдавать весь список в Enum и EnumNames объекте, на основе которого и фильтровать значения:

    {
     "properties": {
       "genre": {
         "enum": [
           "fantasy",
           "thriller",
           "comedy"
         ],
         "enumNames": [
           "genre.choice.fantasy",
           "genre.choice.thriller",
           "genre.choice.comedy"
         ],
         "type": "string",
         "title": "label.genre"
       },
       "sgenre": {
         "enum": [
           "eccentric",
           "romantic",
           "grotesque"
         ],
         "enumNames": [
           {
             "title": "sgenre.choice.eccentric",
             "genre": "comedy"
           },
           {
             "title": "sgenre.choice.romantic",
             "genre": "comedy"
           },
           {
             "title": "sgenre.choice.grotesque",
             "genre": "comedy"
           }
         ],
         "type": "string",
         "title": "label.genre"
       }
     },
     "type": "object"
    }
    

    Но при таком подходе для каждого такого элемента придется писать свою обработку на фронтенде, не говоря уже о том что все сильно усложняется, когда этих объектов становится несколько или один элемент зависит от нескольких списков. Помимо этого сильно вырастает объем данных отправляемых на фронтенд для корректной обработки и отрисовки всех зависимостей. Например, представьте себе отрисовку формы состоящей из трех полей связанных между собой — страны, города, улицы. Объем начальных данных которые необходимо отправить бэкенду на фронтенд может расстроить тонких клиентов, а, как вы помните, мы должны заботиться о наших пользователях. Поэтому было решено реализовать динамику добавив кастомные атрибуты:

    • SchemaID — атрибут схемы, содержит адрес контроллера для обработки текущей введенной FormData и обновления схемы текущей формы, если этого требует бизнес логика;
    • Reload — атрибут, говорящий фронтенду что изменение этого поля инициирует обновление схемы, отправляя данные формы на бэкенд;

    Наличие SchemaID может казаться дублированием — ведь есть атрибут action, но здесь мы говорим о разделении ответственности — контроллер SchemaID отвечает за промежуточное обновление schema и UISchema, а контроллер action выполняет необходимое бизнес действие — например создает или обновляет объект и не допускает отправки части формы так как производит валидационные проверки.С этими дополнениями схема начинает выглядеть следующим образом:

    {
     "schemaId": "//localhost/schema.json",
     "properties": {
       "genre": {
         "enum": [
           "fantasy",
           "thriller",
           "comedy"
         ],
         "enumNames": [
           "genre.choice.fantasy",
           "genre.choice.thriller",
           "genre.choice.comedy"
         ],
         "type": "string",
         "title": "label.genre"
       },
       "sgenre": {
         "enum": [],
         "enumNames": [],
         "type": "string",
         "title": "label.sgenre"
       }
     },
     "uiSchema": {
       "genre": {
         "ui:options": {
           "reload": true
         },
         "ui:widget": "select",
         "ui:help": "title.genre"
       },
       "sgenre": {
         "ui:widget": "select",
         "ui:help": "title.sgenre"
       },
       "ui:widget": "mainForm"
     },
     "type": "object"
    }
    

    В случае изменения поля «genre» фронтенд отправляет всю форму с текущими введенными данными на бэкенд, получает в ответ набор секций необходимых для отрисовки формы:

    {
      action: “https://...”,
      method: "POST",
      schema:{}
      formData:{}
      uiSchema:{}
    }
    

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

    Заключение


    За счет небольшого расширения стандартного подхода мы получили ряд дополнительных возможностей, позволяющих полностью контролировать формирование и поведение фронтовых React компонентов, строить динамические схемы исходя из бизнес логики, иметь единую точку формирования валидационных правил и возможность быстро и гибко создавать новые VIEW части — например мобильные или desktop приложения. Пускаясь в подобные смелые эксперименты нужно помнить о стандарте, на базе которого вы работаете и сохранить обратную совместимость с ним. Вместо React на фронтенде может использоваться любая другая библиотека, главное написать транспортный адаптер к JSON Schema и подключить какую-либо библиотеку рендеринга форм. У нас хорошо сработал Bootstrap с React так как был опыт работы с этим технологическим стеком, но подход, о котором мы рассказали никак не ограничивает вас в выборе технологий. На месте Symfony также мог быть любой другой фреймворк позволяющий конвертировать формы в формат JSON Schema.

    Upd: вы можете посмотреть наш доклад на Symfony Moscow Meetup#14 об этом с 1:15:00.
    Rambler Group 179,78
    Компания
    Поделиться публикацией
    Похожие публикации
    Комментарии 11
    • +1
      «… возможность быстро и гибко создавать новые VIEW части — например мобильные или desktop приложения»
      Как велосипед для создания форм на бутстрапе, реакте и пхп поможет создавать мобильные и десктоп приложения?
      Он в лучшем случае поможет создавать те же формы при использовании реакт нейтив для смартфонов и електрона для десктопа, но никак не все приложение
      • 0
        Есть подозрения, что под «формами» можно подразумевать вообще любой компонент. Который в свою очередь ссылается на React компонент. Так что в результате получается результат: Общая переносимая АПИшка, общий набор компонентов, различия только в данных «UISchema», т.е. в метаинформации о том как отображать данные.
        • +2
          Только вот подобные конфиги тяготеют к тьюринг-полноте.
          • 0
            А это плохо? Если документировано, читаемо и лаконично, конечно же.
            • +3
              Если. А на деле как правило из таких конфигов получается недоязык программирования целиком и полностью состоящий из протекающих абстракций и костылей.
              • 0

                Любое новый пропс все равно нужно или поддерживать на всём лайфцикле или отказаться от него. В таком случае лучше уже документить конфиги путем генерации в результате обхода пропсов чем наоборот. Точно ничего не потеряете.

                • 0
                  Front-end разработчики попросили добавлять props в ui:options, там с этим проще. Остались только глобальные типа ui:group.
                  • 0

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

              • 0
                мы к этому точно не стремимся. по максимуму использовать jsonSchema. добавлять только если действительно проще будет использовать в работе.
          • 0

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

            • 0
              Ещё есть дизайнеры, которые в рамках дизайна одной страницы в одинаковых компонентах могут размер шрифта уменьшить(потому-что демо текст не влез).
              С дизайнерами можно построить диалог, если он конечно не «я художник — я так вижу». Возможно в рамках вашего продукта это оправданно, хотя обычно лучше все делать в едином стиле.

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

            Самое читаемое