https://habrahabr.ru/post/283148/
Сейчас я надену костюм моего нелюбимого супергероя, Капитана Очевидности, и через лень напишу эту статью. Она призвана с помощью небольшого примера показать один серьезный недостаток фреймворков прошлого поколения и, в частности, 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
Я еще не успел нормально разобраться в коде их примеров и понять их плюсы и минусы, поэтому добавлю свой комментарий чуть позже. Но то, что отличные от моих примеров решения есть — это факт.