[recovery mode] Почему нельзя использовать Backbone.js в 2016 году

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

35d9be90110f429eb74ccddfce0fe3ef.png

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!')
                        });
        }
});



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

e01ada557d9847a685df9bc1979307ef.png

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);
                        });
        }
});

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

655306526a30432c9c350e9f73a92189.png

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

c5afefd9532240e1ba59cc4559cbe2eb.png

Правим код:

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'. При этом здесь нет ни строчки бизнес-логики. Тем временем, форма снова расширилась:

2bc9734b0710455b95a1ff2d261fd3ec.png

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

cd9b5567728d4a92ba8874404e31a194.png

55a7e1c1d0ab45e7882790315f58e652.png

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

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);
                        });
        }
});



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

ec98143373b74e54820851c91d572901.png

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

© Habrahabr.ru