[recovery mode] Почему нельзя использовать Backbone.js в 2016 году
Сейчас я надену костюм моего нелюбимого супергероя, Капитана Очевидности, и через лень напишу эту статью. Она призвана с помощью небольшого примера показать один серьезный недостаток фреймворков прошлого поколения и, в частности, 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!')
});
}
});
Все просто, но у посетителя есть возможность сабмитить форму сколько угодно раз после уже отправки запроса. Блокируем кнопку до того, как станет понятно, что произошло с запросом:
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);
});
}
});
Заметили, как удлиннился код? Пришлось создавать ссылки до каждого элемента, который нам нужно заблокировать, а также появился целый блок кода, состоящий из одних только манипуляций 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);
});
}
});
Следите за руками — примитивная форма, ни строчки бизнес-логики, а уже куча кода. Кода, который надо писать, тестировать и поддерживать. Кода, который надо читать новоприбывшим коллегам. Кода, за написание которого надо заплатить программистам. Ну, а дальше… Ну вы поняли:
Думаю, дальше можно не объяснять — при дальнейшем усложнении формы нам нужно будет создавать ссылки на каждый элемент формы, состояние которого будет меняться, а затем вручную изменять его состояние. Насколько это плохо в реальности, а не на демо-примере? Очень плохо. Я проработал с 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 (
);
}
});
Послесловие.
Если у вас возник вопрос, зачем такие очевидные вещи писать в javascript-хабе, ведь React-одепты итак достали уже абсолютно всех, то у вас очень резонное замечание, потому что они конкретно достали и меня тоже. Но я не смог забить на это после того как встретил непробиваемое невежество в мирном с виду топике, приведя там похожий код. После чего не поленился и написал эту статью с примерами. Самый жир:
Пишу на BB давно и много. От прямой манипуляции с DOM отказался сразу, при этом давления со стороны библиотеки не испытал.
Не ради холивара, но поймите ничем особым ангуляр или реакт или еще что либо не лучше и не хуже бекбона. Главное понимать что, как и почему так работает.
PS Да реакт клевый, было время я думал на него перевести приложения. Но, стильно/модно/молодежно — оставим для новичков. После глубокого анализа — бенефит был нулевой.
Если вы не умеете готовить бекбон — это ваши личные проблемы.
Мне даже шаблоны не нужны. А вот вы нагородили кучу всего. Повторюсь, вникните в суть фреймворков.
P.S.
- Если вы считаете, что я привел слишком редкий для разработки кейс, то приведите примеры нередких кейсов.
- Если вы считаете код на Marionette предвзятым, напишите свой код, лучше и проще, я сделаю апдейт поста.
- Если вы по своей воле выбираете Backbone-like фреймворк для старта вашего проекта в 2016 году, то мне очень интересно узнать, почему вы так поступаете.