Чому не можна використовувати 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!')
});
}
});


<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="київ">
<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="київ">
<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="київ">
<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="київ">
<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="київ">
<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="київ">
<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 році, то мені дуже цікаво дізнатися, чому ви так робите.


Джерело: Хабрахабр

0 коментарів

Тільки зареєстровані та авторизовані користувачі можуть залишати коментарі.