Причины говнокода во фронтенде. Мнение мимокрокодила

в 18:15, , рубрики: javascript, React, react.js, ReactJS, redux, vue, vue.js, vuejs, vuex, Программирование

Дисклеймер

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

Откуда есть пошли SPA

Как известно, изначальной задачей JavaScript было обеспечение интерактивности на HTML-странице, и предназначался язык, в первую очередь, для верстальщиков и дизайнеров, а для программистов предлагалось использовать интегрируемые в страницу Java-апплеты. И, несмотря на сходство в названии с Java, общего у JS с нею было только оно да C-подобный синтаксис. Под капотом же JavaScript был значительно вдохновлен языком Scheme, но именно этому странному языку, написанному в кратчайшие сроки, суждено было стать одним из самых популярных на планете, и решать задачи, для которых он никогда не проектировался.

Страх и отвращение в мире JS

Первое свое SPA-приложение (сейчас такое неспешно пилится за вечерок) я написал в 2009 году, вдохновившись GMail. Вся эта магия AJAX тогда просто будоражила воображение, рисовала картины будущего, где веб-версия офисного пакета станет чем-то обыденным, даже Photoshop обзаведется ею, в DOOM 3 можно будет поиграть прямо в окне браузера с приличной частотой кадров, а десктопные приложения вообще будут летать, ведь там не нужно будет использовать медленный интерпретируемый JS... Мне тогда очень повезло, что поблизости был человек, умеющий в дизайн, и понимающий особенности верстки тогдашних сайтов. То был сайт-галерея, где снимки подгружались динамически во время пролистывания, и их можно было комментировать, вводя капчу, генерируемую PHP-скриптом. И все это без единого перехода между страницами! (Восклицание было уместным на тот момент, но сейчас вызывает, скорее, легкую ухмылку.) Тогда из фреймворков у меня был JavaScript/JScript и еще классы... ну как классы, что-то в этом роде:

function MyClass(arg1, arg2, arg3, arg4) {
	var privateProp1;
	var privateProp2;

	this.method = function(arg) {
		return privateProp1 == privateProp2;
	};

	// constructor
	privateProp1 = arg1;
	privateProp2 = arg2;
	this.publicProp = arg3;
	this._protectedProp = arg4;

	return this;
}

Кто-то будет ругаться, что методы нужно указывать через MyClass.proptype.method = ..., но, признаюсь честно, уже давно в таком стиле не писал, и как оно точно было в ту пору — уже не помню.

Конечно, уже существовала jQuery, но я всегда ее недолюбливал и считал избыточной, ведь эта маленькая библиотечка тогда нехило так добавляла к весу приложения, а использовать надо было от силы процентов десять от всего функционала, что вполне можно было реализовать прямыми руками на чистом JavaScript. Кто-то начнет тыкать в меня палкой, припоминая Ext JS, Prototype, GWT (Google Gears), и что-нибудь еще, про них могу сказать лишь то, что они, как неандертальцы, оставив в современных JS-фреймворках часть своего генома, все же оказались тупиковой ветвью эволюции.

Время шло, JS развивался, а я отошел от веб-разработки, правда, как оказалось — не навсегда.

Попытки систематизировать хаос

Интернет не стоял на месте — перед разработчиками сайтов и веб-приложений ставились все более и более масштабные задачи: сайты все больше становились похожими на своих десктопных собратьев и требовали применения иных подходов к разработке. Уже в 2010 свет увидели SproutCore от Apple (на базе которого уже через год появится Ember.js) и Angular.js от Google. Несмотря на все различия в реализации, в том числе в подходах к шаблонизации, синтаксисе и системах сборки, оба фреймворка решали одну и ту же задачу: предоставить разработчикам масштабируемый каркас для разработки веб-приложений, да и, что уж тут скрывать — оба реализовывали паттерн MVC. И правда, если взглянуть на синтаксис того же Angular.js, становится понятно, что писать на нем что-то небольшое не имело смысла — слишком велик был оверхед, хотя, признаюсь, сама идея реактивности тогда завораживала.

Как уже сказано выше, в 2011 году вышла первая версия Ember.js, который развивал идеи своего прародителя и предлагал all-in-one фреймворк, включая в себя полноценные представления в виде шаблонов на основе Handlebars и утилиты для управления проектом для создания новых элементов, запуска, сборки и тестирования проекта. Даже jQuery был на долгие годы внедрен внутрь Ember.js.)

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

Поиск золотой середины

Свято место, как известно, пусто не бывает, и уже спустя два года (в 2013 году) одна небезызвестная компания, знаменитая своей борьбой с ограничениями MySQL и написанием компилируемого аналога PHP по причине наличия фатального недостатка у оного, выкладывает в общий доступ свой внутренний продукт, как они называют его, библиотеку для создания пользовательских интерфейсов — React. Впоследствии это событие вызовет лавинообразный рост интереса к разработке SPA в частности и фронтенд разработке в целом. Все дело в том, что React не навязывал какую-либо архитектуру для приложения, не требовал знания большого количества паттернов, и вообще мог быть довольно легко последовательно освоен даже начинающими разработчиками. Теперь буквально каждый мог написать за пару часов свое реактивное приложение. Впрочем, без ложки дегтя, как всегда, не обошлось: React оказался не только внутренним продуктом, который развивается без существенной оглядки на сообщество, но и экспериментом, что доказывает и нумерация версий вплоть до 0.14.8, выпущенной в марте 2016, за которой уже последовала версия 15.0.0 (А что так можно было что ли?). Сама библиотека обросла массой расширений, сторонних и не очень, концептуальными решениями вроде Flux и скриптами для инициализации, сборки и отладки приложений. В итоге, React — это такой конструктор, из которого можно собрать что угодно, от простого сайта-визитки, до сложного приложения с кучей взаимосвязанных элементов.

Впрочем, не одним React'ом едины, и в декабре того же 2013 года выходит версия 0.6 темной лошадки Vue.js. Фреймворк, вдохновленный, по всей видимости, теми же идеями, что и React, но предоставляющий чуть больше возможностей из коробки, быстро занял свое место в пантеоне JS-фреймворков. Предоставляя практически такую же гибкость, что и React, Vue.js, тем не менее, обзавелся набором собственных официальных расширений, которые отлично интегрировались друг с другом, и лучшей, на мой взгляд, из существующих (по крайней мере, среди JS-фреймворков) документацией. Теперь начинающий разработчик мог не думать над тем — какую реализацию роутера, реактивности или глобального состояния выбрать, а это существенно экономит время. Также Vue.js никогда не стеснялся заимствовать идеи у React, перерабатывать их и встраивать в собственные решения, так появились такие вещи как Vuex и Composition API.

Безусловно, JS-фреймворков куда как больше, но остальные не столь популярны, так что оставим их за скобками.

Одурманивание через упрощение

Казалось бы, наступил рай — почти все браузеры слились в экстазе с Chromium, фреймворки есть на любой вкус и цвет, бери да пиши, что пожелаешь. Но реальность оказалась суровой: огромное количество сайтов оказалось непомерно раздутыми и неоптимизированными, да еще и, благодаря технологиям и огромному количеству фронтендеров стало модно писать на JS не только веб-приложения и сайты, но также десктопные приложения, фактически, упаковывая SPA вместе с браузером в единый бинарник. Рай не наступил, но как же так получилось? Давайте попытаемся разобраться.

Экономические причины, заставляющие разработчиков использовать JS везде, даже там, где не стоило бы — оставим за скобками. В конце-концов, все помнят почивший Atom и здравствующий Visual Studio Code. Оба на Electron'е, но Atom был славен тем, что тормозил везде и всегда, а вот VS Code умудряется вполне сносно работать даже не на самых мощных машинах, так что важен не только инструмент, но и руки, которые его держат.

Снизившийся порог вхождения в веб-разработку породил огромное количество разработчиков, которые знают, как им кажется, фреймворки, или вообще отдельный фреймворк, но при этом не знают толком JS. Отсюда мы наблюдаем в проектах операции вида .slice(x, y).filter(...).map(...), совершаемые без зазрения совести не над десятками, а над тысячами объектов. Банальные ошибки из-за непонимания того, как работают асинхронные вызовы. Уверен, что многие встречали в коде какого-нибудь молодого сотрудника на работе подобную запись:

// заполняем чем-нибудь массив
const arr = [...];

// что-то где-то происходит в коде

arr.forEach(async (item) => ...);

return arr; // возвращаем массив

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

Гораздо большую проблему представляет собой та самая кажущаяся простота. Пока приложение состоит из трех-пяти компонентов и пары формочек, все отлично. Не важно, выберем мы Redux или MobX для хранения состояния. Просто берем и добавляем по мануалу Vuex/Pinia + Axios в проект и получаем глобальное хранилище и запросы к серверу. Да, есть небольшой оверхед, но по современным меркам он ни на что не влияет. Но вот проект начинает разрастаться, форм уже не две, а десять, далеко не все из них состоят лишь из пары полей. Взаимосвязь между компонентами усложняется, появляется множество параллельных запросов. В какой-то момент оказывается, что частое изменение верхних уровней состояния заметно подтормаживает на Redux, поэтому, пожалуй, попробуем что-нибудь другое, может, MobX? Отличная штука этот Vuex, тут вам и глобальное состояние, и мутации, и экшены (кстати, почему нельзя напрямую вызывать синхронные мутации, или, все-таки, можно?), и вызовы api-сервера... Мы тут решили, что бизнес-логику нужно вынести из компонентов, так как становится сложно разобраться в их структуре, теперь вся наша бизнес-логика переехала во Vuex/Redux... Проект уже давно состоит не из одной страницы, а из нескольких, и при быстром переходе с одной на другую, может возникнуть коллизия данных — более ранний запрос к серверу пришел позднее и перезаписал данные в состоянии, теперь у нас выводятся не те данные, какие должны были быть. Что такое принцип единственности истины, я же просто загружаю данные с сервера? О, круто, появился новый Composition API, будем использовать его. Стоп! А зачем нам теперь Pinia? Как и когда загружать контент на страницу при переходе? Использовать глобальный индикатор загрузки или блокировать отдельные элементы? Если вы никогда не задавались подобными вопросами — у меня вновь для вас плохие новости. И проблема тут состоит из двух факторов:

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

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

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

Глобальное состояния, Flux-архитектура и вот это вот все

Особняком стоит Flux-архитектура, которая с удивительной легкостью смузи зашла в массы. Зачем разбираться во всяких сложных паттернах по типу MVC или MVVM, если можно взять готовое решение в виде Redux/Vuex/Pinia и внедрить в проект, как это показано в тысячах статей и примеров? Чтобы понять саму проблематику, нужно взглянуть на все в историческом разрезе. Когда Facebook, а ныне — признанная экстремистской на территории РФ Meta, разрабатывала React, то у них уже был готовый сайт, с работающими и отлаженными JS-скриптами. Целью Джордана Валке, первоначального автора React, было решение внутренней проблемы: обновление ленты Facebook без перезагрузки всей страницы. То есть нужно было асинхронно получать обновленные данные ленты и рендерить их на текущей странице. Вы видите здесь бизнес-логику? Вот и я не вижу. Собственно, тут стоит отдать дань уважения разработчикам — они изначально позиционировали React как библиотеку для построения интерфейсов, а не полноценный фреймворк. По сути, у нас есть только состояние, механизм его обновления и представление, являющееся функцией от этого состояния. Вкупе все это можно назвать тонким клиентом. При таком подходе, хранение индивидуального состояния в каждом из компонентов отдельно (state/setState) и механизм его обновления становятся нетривиальными с ростом количества этих самых компонентов. И здесь нашлось решение — менеджер глобального состояния, отвечающий как за его предоставление, так и за изменение. Теперь у нас есть декларативного вида представление, состоящее из компонентов без внутреннего состояния (stateless-компонентов), и состояние, которое реагирует на события этого представления:

Диаграмма тонкого клиента на React + Redux

Диаграмма тонкого клиента на React + Redux

Именно поэтому во множестве статей предлагают выполнять запросы к серверу внутри обработчиков действий (action creators). А ваше приложение тоже отлично ложится на эту архитектуру? Не зря все та же компания придумала и реализовала GraphQL, позволяющий, по сути, получать изменения всего состояния целиком, за один запрос. Ваш сервер тоже так умеет? Теперь появилось очередное решение — React Query, который должен решить проблемы кэширования запросов (EmberData, кстати, передает привет)...

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

Куда поместить бизнес-логику?

Об этом, с примерами на Vue.js и React, поговорим во второй части.

Автор:
Devoter

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js