Почему разрабатывать веб-интерфейсы так сложно?

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

Кроме того, мы исследуем так называемые “нерезультативные пути” (“unhappy paths”), часто упускаемые из виду. К ним относятся состояния загрузки, обработка ошибок, а также более широкие архитектурные аспекты, включающие безопасность, производительность и доступность.

Устранение языковых несоответствий

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

Сайт в виде “обычного” документа

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

В идеальном мире создание пользовательского интерфейса напоминало бы работу с визуальным UI-проектировщиком. Такие инструменты, как C++ Builder и Delphi, или более современные альтернативы, например Figma, позволяют перетаскивать компоненты на холст, который затем гладко отображается на любом экране.

IDE прошлого века для десктопных приложений

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

Представление проблемы в Jira

Выше представлен тикет Jira как иллюстрация рассматриваемой проблемы. Как видите, это достаточно сложный пользовательский интерфейс. В таком UI можно ожидать наличие компонента Navigator, списка Dropdown, Accordion и так далее. Но их нет, вернее сказать, мы имеем дело с их непосредственным присутствием.

Разработчики приложили немало усилий, чтобы смоделировать их с помощью HTML, CSS и JavaScript. Если временно отключить CSS для сайта, то получим нечто подобное:

Представление проблемы в Jira без использования CSS

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

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

К сожалению, языковое несоответствие  —  это лишь малая часть проблем, и, честно говоря, ситуация несколько изменилась с появлением таких декларативных библиотек пользовательского интерфейса, как React и Vue. Однако в области фронтенд-разработки существуют и другие загвоздки. Управление данными (состоянием), безусловно, входит в этот список.

Проблема управления состоянием

Управление состоянием в современной фронтенд-разработке является сложной задачей. Практически каждое приложение должно получать данные с удаленного сервера по сети. При этом часто возникают различные проблемы, такие как аутентификация для доступа к API и особенности кода для получения данных, например используемый протокол, формат данных, стандарт API (RESTful/GraphQL/RPC).

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

Компонент аккордеон

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

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

Исследование нерезультативных путей

При разработке пользовательского интерфейса мы часто ориентируемся на “результативный путь” (“happy path”)  —  оптимальный путь пользователя, где все идет по плану. Однако пренебрежение “нерезультативными путями” чревато сильным усложнением интерфейса. Вот несколько сценариев, которые могут привести к возникновению “нерезультативных путей” и, как следствие, усложнить UI-разработку.

Ошибки в другом компоненте

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

Например, в следующем коде при попытке получить доступ к чему-то несуществующему в переданном свойстве, возникает типичная ошибка TypeError: Cannot read properties of undefined (reading ‘doesnt’), и выбрасывается исключение.

const MenuItem = ({
item,
onItemClick,
}: {
item: MenuItemType;
onItemClick: (item: MenuItemType) => void;
}) => {
// @ts-ignore
const information = item.name + item.something.doesnt.exist;

return (
<li key={item.name}>
<h3>{item.name}</h3>
<p>{item.description}</p>
<button onClick={() => onItemClick(item)}>Add to Cart</button>
</li>
);
};

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

Оборачивание проблемного компонента с помощью ErrorBoundary

Сбои в работе систем из технологической цепочки

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

Неожиданное поведение пользователя

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

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

Доступ к удаленному состоянию по сети

В React сетевое программирование ставит уникальные задачи, которые выходят за рамки кажущейся простоты выполнения вызовов API с помощью useEffect. Хотя результативный путь может показаться простым, реальность быстро усложняется при учете различных сценариев, таких как обработка ошибок, состояния загрузки и повторные попытки. Добавьте к этому сложности, связанные с кэшированием, и код быстро станет громоздким и сложным для поддержки.

Поэтому старайтесь не делать так с помощью хука useEffect (хотя это хорошая отправная точка).

const [users, setUsers] = useState<User[]>([])

useEffect(() => {
const fetchUsers = () => {
fetch('https://jsonplaceholder.typicode.com/users')
.then((response) => response.json())
.then((data) => setUsers(data));
}

fetchUsers();
}, []);

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

const [loading, setLoading] = useState<boolean>(false);
const [error, setError] = useState<string>('');

useEffect(() => {
const fetchUsers = () => {
setLoading(true);
fetch('https://jsonplaceholder.typicode.com/users2')
.then((response) => {
if (response.status >= 200 && response.status <= 299) {
return response.json();
} else {
setLoading(false);
throw Error(response.statusText);
}
})
.then((data) => {
setLoading(false);
setUsers(data)
})
.catch((e) => {
setLoading(false);
setError(e);
});
}

fetchUsers();
}, []);

if(error) {
return <ErrorMessage />
}

В документации по библиотеке react-query приведен длинный список возможных ограничений.

Некоторые проблемы, перечисленные на странице react-query

Другие факторы, которые следует учитывать

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

  • Кроссбраузерная совместимость. Различные браузеры имеют разные механизмы рендеринга, и даже разные версии одного и того же браузера могут вести себя по-разному. Поэтому кроссбраузерная совместимость является одной из основных проблем.
  • Оптимизация производительности. Для обеспечения высокой скорости и эффективности работы сайта необходимо глубокое понимание того, как браузеры отображают элементы, “ленивой” загрузки, оптимизации активов и многого другого.
  • Доступность. Создание доступного сайта, удобного для всех пользователей, включая людей с ограниченными возможностями, требует дополнительных знаний и тестирования.
  • Вопросы безопасности. С ростом числа фронтенд-приложений безопасность на стороне клиента приобрела еще большую важность. Помните о таких проблемах, как межсайтовый скриптинг (XSS), подделка межсайтовых запросов (CSRF) и утечка данных через логи на стороне клиента.

Заключение

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

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

Читайте также:

Читайте нас в Telegram, VK и Дзен


Перевод статьи Juntao Qiu: Why Web UI Development Is So Hard?

Предыдущая статьяСоздаем ИИ с помощью OpenAI
Следующая статьяЧистый код работает медленно, но он все равно нужен