Почему нельзя использовать Backbone.js в 2016 году +13


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

Смотрите, предположим нам поставили задачу реализовать простую форму:



var MyFormView = Marionette.ItemView.extend({
	template: 'myForm',
	className: 'my-form',
	ui: {
		$submit: '.js-submit'
	},
	events: {
		'click @ui.$submit': 'submit'
	},
	send: function () {
		myAsyncSubmit
			.done(function () {
				alert('Form submitted!');
			})
			.fail(function () {
				alert('Error occured!')
			});
	}
});


<template id="myForm">
<form>
	<div class="my-form-wrapper">
		<div class="block">
			<label for="input1">Your Name:</label><br>
			<input type="text" id="input1">
		</div>

		<div class="block">
			<label for="input2">Your Region:</label><br>
			<input type="text" id="input2">
		</div>

		<div class="block">
			<label for="input3">Your City:</label><br>
			<input type="text" id="input1">
		</div>

		<button type="submit" class="js-send">Send</button>
	</div>	
</form>
</template>


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



var MyFormView = Marionette.ItemView.extend({
	template: 'myForm',
	className: 'my-form',
	ui: {
		$submit: '.js-submit'
	},
	events: {
		'click @ui.$submit': 'submit'
	},
	submit: function () {
		this.ui.$submit.prop('disabled', true);

		myAsyncSubmit
			.done(function () {
				alert('Form submitted!');
			})
			.fail(function () {
				alert('Error occurred!')
			})
			.always(function () {
				this.ui.$submit.prop('disabled', true);
			});
	}
});


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



Поле «Your ZIP» забирает на сервере информацию по заданному ZIP и подставляет ее в поля ниже. Это позволяет обойтись без ввода «Your Region» и «Your City», но поэтому при нажатии кнопки «Set», блокируются поля «Your Region», «Your City» и кнопка «Submit» до завершения запроса, вот так:



Правим код:

var MyFormView = Marionette.ItemView.extend({
	template: 'myForm',
	className: 'my-form',
	ui: {
		$inputZip: '#inputZip',
		$setZip: '.js-set-zip',
		$inputRegion: '#inputRegion',
		$inputCity: '#inputCity',
		$submit: '.js-submit'
	},
	events: {
		'click @ui.$setZip': 'setZip',
		'click @ui.$submit': 'submit'
	},
	setZip: function () {
		toggleZipInputs(true);

		myAsyncSetZip
			.done(function () {
				alert('Form submitted!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				toggleZipInputs(false);
			});

		function toggleZipInputs (value) {
			this.ui.$inputZip.prop('disabled', value);
			this.ui.$setZip.prop('disabled', value);
			this.ui.$inputRegion.prop('disabled', value);
			this.ui.$submit.prop('disabled', value);
		}.bind(this);
	},
	submit: function () {
		this.ui.$submit.prop('disabled', true);

		myAsyncSubmit
			.done(function () {
				alert('Form submitted!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				this.ui.$submit.prop('disabled', true);
			});
	}
});


<template id="myForm">
<form>
	<div class="my-form-wrapper">
		<div class="block">
			<label for="inputName">Your Name:</label><br>
			<input type="text" id="inputName">
		</div>

		<div class="block">
			<label for="inputZip">Your ZIP:</label><br>
			<input type="text" id="inputZip">
			<button type="button" class="js-set-zip">Set</button>
		</div>

		<div class="block">
			<label for="inputRegion">Your Region:</label><br>
			<input type="text" id="inputRegion">
		</div>

		<div class="block">
			<label for="inputCity">Your City:</label><br>
			<input type="text" id="inputCity">
		</div>

		<button type="submit" class="js-send">Send</button>
	</div>	
</form>
</template>


Заметили, как удлиннился код? Пришлось создавать ссылки до каждого элемента, который нам нужно заблокировать, а также появился целый блок кода, состоящий из одних только манипуляций DOM-деревом, вынесенный в функцию 'toggleZipInputs'. При этом здесь нет ни строчки бизнес-логики. Тем временем, форма снова расширилась:



Увеличение на этот раз лишь количественное, смысл работы полей остался тот же самый — кнопка «Set Your Car Number» на время выполнения запроса блокирует поля «Your Car Model» и «Your Car Age», кнопка «Set Your Social ID» делает тоже самое с полями «Your Gender», «Your Age» и «Your Sexual Orientation». Обе они также блокируют кнопку «Submit»:





Ооокей, пишем код:

var MyFormView = Marionette.ItemView.extend({
	template: 'myForm',
	className: 'my-form',
	ui: {
		$inputZip: '#inputZip',
		$setZip: '.js-set-zip',
		$inputRegion: '#inputRegion',
		$inputCity: '#inputCity',
		$inputCarNumber: '#inputCarNumber',
		$setCarNumber: '.js-set-car-number',
		$inputCarModel: '#inputCarModel',
		$inputCarAge: '#inputCarAge',
		$inputSocialId: '#inputSocialId',
		$setSocialId: '.js-set-social-id',
		$inputGender: '#inputGender',
		$inputAge: '#inputAge',
		$inputSexualOrientation: '#inputSexualOrientation',
		$submit: '.js-submit'
	},
	events: {
		'click @ui.$setZip': 'setZip',
		'click @ui.setCarNumber': 'setCarNumber',
		'click @ui.$submit': 'submit'
	},
	setZip: function () {
		toggleZipInputs(true);

		myAsyncSetZip
			.done(function () {
				alert('Form submitted!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				toggleZipInputs(false);
			});

		function toggleZipInputs (value) {
			this.ui.$inputZip.prop('disabled', value);
			this.ui.$setZip.prop('disabled', value);
			this.ui.$inputRegion.prop('disabled', value);
			this.ui.$submit.prop('disabled', value);
		}.bind(this);
	},
	setCarNumber: function () {
		toggleCarInputs(true);

		myAsyncSetCarNumber
			.done(function () {
				alert('Car Number set!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				toggleCarInputs(false);
			});

		function toggleCarInputs (value) {
			this.ui.$inputCarNumber.prop('disabled', value);
			this.ui.$setCarNumber.prop('disabled', value);
			this.ui.$inputCarModel.prop('disabled', value);
			this.ui.$inputCarAge.prop('disabled', value);
			this.ui.$submit.prop('disabled', value);
		}.bind(this);
	},
	setSocialId: function () {
		toggleSocialInputs(true);

		myAsyncSetSocial
			.done(function () {
				alert('Social ID set!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				toggleSocialInputs(false);
			});

		function toggleSocialInputs (value) {
			this.ui.$inputSocialId.prop('disabled', value);
			this.ui.$setSocialId.prop('disabled', value);
			this.ui.$inputGender.prop('disabled', value);
			this.ui.$inputAge.prop('disabled', value);
			this.ui.$inputSexualOrientation.prop('disabled', value);
			this.ui.$submit.prop('disabled', value);
		}.bind(this);
	},
	submit: function () {
		this.ui.$submit.prop('disabled', true);

		myAsyncSubmit
			.done(function () {
				alert('Form submitted!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				this.ui.$submit.prop('disabled', true);
			});
	}
});


<template id="myForm">
<form>
	<div class="my-form-wrapper">
		<div class="col">
			<div class="block">
				<label for="inputName">Your Name:</label><br>
				<input type="text" id="inputName">
			</div>

			<div class="block">
				<label for="inputZip">Your ZIP:</label><br>
				<input type="text" id="inputZip">
				<button type="button" class="js-set-zip">Set</button>
			</div>

			<div class="block">
				<label for="inputRegion">Your Region:</label><br>
				<input type="text" id="inputRegion">
			</div>

			<div class="block">
				<label for="inputCity">Your City:</label><br>
				<input type="text" id="inputCity">
			</div>
		</div>

		<div class="col">
			<div class="block">
				<label for="inputCarNumber">Your Car Number:</label><br>
				<input type="text" id="inputCarNumber">
				<button type="button" class="js-set-car-number">Set</button>
			</div>

			<div class="block">
				<label for="inputCarModel">Your Car Model:</label><br>
				<input type="text" id="inputCarModel">
			</div>

			<div class="block">
				<label for="inputCarAge">Your Car Age:</label><br>
				<input type="text" id="inputCarAge">
			</div>
		</div>

		<div class="col">
			<div class="block">
				<label for="inputSocialId">Your Social ID:</label><br>
				<input type="text" id="inputSocialId">
				<button type="button" class="js-set-social-id">Set</button>
			</div>

			<div class="block">
				<label for="inputGender">Your Gender:</label><br>
				<input type="text" id="inputGender">
			</div>

			<div class="block">
				<label for="inputAge">Your Age:</label><br>
				<input type="text" id="inputAge">
			</div>

			<div class="block">
				<label for="inputSexualOrientation">Your Sexual Orientation:</label><br>
				<input type="text" id="inputSexualOrientation">
			</div>
		</div>

		<button type="submit" class="js-send">Send</button>
	</div>	
</form>
</template>


Следите за руками — примитивная форма, ни строчки бизнес-логики, а уже куча кода. Кода, который надо писать, тестировать и поддерживать. Кода, который надо читать новоприбывшим коллегам. Кода, за написание которого надо заплатить программистам. Ну а дальше… Ну вы поняли:



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

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

var MyForm = React.createClass({
		var self = this;
		this.setState({ isZipSetting: true });

		myAsyncSetZip
			.done(function () {
				alert('Zip set!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				self.setState({ isZipSetting: false });
			});
	},
	setCarNumber: function () {
		var self = this;
		this.setState({ isCarNumberSetting: true });

		myAsyncSetCarNumber
			.done(function () {
				alert('Car number set!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				self.setState({ isCarNumberSetting: false });
			});
	},
	setSocialId: function () {
		var self = this;
		this.setState({ isSocialIdSetting: true });

		myAsyncSetSocialId
			.done(function () {
				alert('Social ID set!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				self.setState({ isSocialIdSetting: false });
			});
	},
	submit: function () {
		var self = this;
		this.setState({ isSubmitting: true });

		myAsyncSubmit
			.done(function () {
				alert('Form submitted!');
			})
			.fail(function () {
				alert('Error occured!')
			})
			.always(function () {
				self.setState({ isSubmitting: false });
			});
	},
	render: function () {
		return (
<form>
	<div className="my-form-wrapper">
		<div className="col">
			<div className="block">
				<label for="inputName">Your Name:</label><br>
				<input type="text" id="inputName">
			</div>

			<div className="block">
				<label for="inputZip">Your ZIP:</label><br>
				<input type="text" id="inputZip" { isZipSetting ? 'disabled' : '' }>
				<button type="button" onClick={this.setZip} { isZipSetting ? 'disabled' : '' }>Set</button>
			</div>

			<div className="block">
				<label for="inputRegion">Your Region:</label><br>
				<input type="text" id="inputRegion" { isZipSetting ? 'disabled' : '' }>
			</div>

			<div className="block">
				<label for="inputCity">Your City:</label><br>
				<input type="text" id="inputCity" { isZipSetting ? 'disabled' : '' }>
			</div>
		</div>

		<div className="col">
			<div className="block">
				<label for="inputCarNumber">Your Car Number:</label><br>
				<input type="text" id="inputCarNumber" { isCarNumberSetting ? 'disabled' : '' }>
				<button type="button" onClick={this.setCarNumber} { isCarNumberSetting ? 'disabled' : '' }>Set</button>
			</div>

			<div className="block">
				<label for="inputCarModel">Your Car Model:</label><br>
				<input type="text" id="inputCarModel" { isCarNumberSetting ? 'disabled' : '' }>
			</div>

			<div className="block">
				<label for="inputCarAge">Your Car Age:</label><br>
				<input type="text" id="inputCarAge" { isCarNumberSetting ? 'disabled' : '' }>
			</div>
		</div>

		<div className="col">
			<div className="block">
				<label for="inputSocialId">Your Social ID:</label><br>
				<input type="text" id="inputSocialId" { isSocialIdSetting ? 'disabled' : '' }>
				<button type="button" onClick={this.setSocialId} { isSocialIdSetting ? 'disabled' : '' }>Set</button>
			</div>

			<div className="block">
				<label for="inputGender">Your Gender:</label><br>
				<input type="text" id="inputGender" { isSocialIdSetting ? 'disabled' : '' }>
			</div>

			<div className="block">
				<label for="inputAge">Your Age:</label><br>
				<input type="text" id="inputAge" { isSocialIdSetting ? 'disabled' : '' }>
			</div>

			<div className="block">
				<label for="inputSexualOrientation">Your Sexual Orientation:</label><br>
				<input type="text" id="inputSexualOrientation" { isSocialIdSetting ? 'disabled' : '' }>
			</div>
		</div>

		<button type="submit" onClick={this.submit} { (isSubmitting || isZipSetting || isCarNumberSetting || isSocialIdSetting) ? 'disabled' : '' }>Submit</button>
	</div>	
</form>
    );
  }
});


Послесловие.

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

Пишу на BB давно и много. От прямой манипуляции с DOM отказался сразу, при этом давления со стороны библиотеки не испытал.

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

PS Да реакт клевый, было время я думал на него перевести приложения. Но, стильно/модно/молодежно — оставим для новичков. После глубокого анализа — бенефит был нулевой.

Если вы не умеете готовить бекбон — это ваши личные проблемы.

Мне даже шаблоны не нужны. А вот вы нагородили кучу всего. Повторюсь, вникните в суть фреймворков.


P.S.

  • Если вы считаете, что я привел слишком редкий для разработки кейс, то приведите примеры нередких кейсов.
  • Если вы считаете код на Marionette предвзятым, напишите свой код, лучше и проще, я сделаю апдейт поста.
  • Если вы по своей воле выбираете Backbone-like фреймворк для старта вашего проекта в 2016 году, то мне очень интересно узнать, почему вы так поступаете.


Update.

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

За время обширной дискуссии только два человека не поленились и подтвердили свои слова делом, за что им спасибо:

Решение с epoxy от alexgrom
Решение с View-оберткой от Delphinum

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


К сожалению, не доступен сервер mySQL