Експресивний ReactJS або тырим фічі Angular в наш фреймворк

В останні час, AngularJS не критикував тільки ледачий і я, за останні пару місяців, перечитав чимало статей з критикою. Одна така стаття завершувалася фрази «Я, чорт візьми, не розумію, як Angular може бути настільки популярним, адже він такий поганий!»
«А справді» задумався я «якщо він такий поганий, то чому ж такий популярний?» І я, здається, знайшов відповідь. Справа в тому, що у Angular, як і у jQuery, низький поріг входження, він простий і наочний. Так, безсумнівно, і в Angular і jQuery можна робити складні речі, але більшість людей не використовує ці бібліотеки таким чином. Що я маю на увазі?

Припустимо, у розробника стоїть таке завдання: «Зробити список відгуків, якщо відгуків немає, запропонувати користувачеві додати відгук»
Для початку, давайте реалізуємо це на Angular:

<h1>Reviews</h1>
<article ng-repeat="review in reviews">
{{review.user}}: {{review.text}}
</article>
<div ng-hide="reviews">
There are no reviews, wanna <a href="#">add</a> some?
</div>


А тепер на React:
var Reviews = React.createClass({
getReviews (){
if(!this.props.reviews.length){
return (
<div>
There are no reviews, wanna <a href="#">add</a> some?
</div>
)
}
},

getReview (review){
return (
<article ng-repeat="review in reviews">
{review.user}: {review.text}
</article>
)
},

render (){
return (
<h1>Reviews</h1>
{this.maybeGetAddNew()}
{this.props.reviews.map(this.getReview}
)
}
});


Як ми бачимо, код на React не тільки довше, але і заплутаніше. Після того, як я нарешті вбив в голову дизайнерам і верстальникам «Маркап завжди внизу файлу, пиши звичайний HTML, тільки використовуй className замість class», дизайнер заходить в мій файлик, бачить і каже мені «Так ви що, знущаєтесь? Що це за фігурні дужки? Мені потрібно клас додати до одного div-у, а це що таке? Я взагалі-то HTML вчив, а не JS»
Інша справа з Angular, тут дизайнеру все зрозуміло, що і куди треба додавати, можливо, він навіть розуміє що коли зникає і що повторюється, потрібно просто налякати його, що якщо він зачепить атрибути ng-, то його чекають страшний суд, пекло і погибель, і все буде в порядку.
Ну ладно, це ж дизайнери, що з них взяти, нубие, одні словом, ми-то суворі бородаті програмісти, нам такий код по душі. Та ну? А якщо хтось з команди говорить нам «Гей, {username}, а що це в тебе за голий дів? Він мені весь лэйаут ламає!» і ми заходимо в компоненту, і бачимо це:

render (){
return (
{this.getPostTitleIfPostHasTitle()}
{this.getPostAuthorAvatarIfAuthorHasAvatar()}
{this.getPostAuthorNameIfNotAnonymous()}
{this.getPostTeaserImageIfPostHasTeaserimage()}
{/*ну і так далі*/}
)
}


і тепер нам треба бігати по методам і перевіряти умови кожного, щоб знайти таки той нещасливий div. Ну, а якщо деякі методи ще й успадковані з міксина… то тут вже нестримного веселощів вистачить на годину, а то і більше.

Рішення?
Очевидно, нам треба перенести пару найбільш використовуваних директив Angular в React, наприклад, ng-show, ng-hide, ng-class і ng-repeat. Але як? Можна, звичайно ж, зробити міксин, який буде бігти по захардкоденому списком «директив» і звіряти їх з пропами господаря, а далі виконувати логіку, пов'язану з цією директивою»

var DirectiveMixin = {
renderWithDirectives (target){
if(!!this.props.isShownWhen){
return target;
}
return null;
}
}

var Component = {
mixins: [DirectiveMixin],
render (){
return this.renderWithDirectives(
<div>Hi there!</div>
)
}
}

var userIsLoggedIn = false;
var React.renderComponent(<Component isShownWhen={userIsLoggedIn}/>, document.body);


Вже непогано, але є пара моментів. По-перше, ми повинні писати компоненту для кожної дрібної деталі, яка умовно з'являється/ховається на нашому сайті, навіть якщо це малееенький span. Ну, а по-друге, якщо ми захочемо це використовувати для якої-небудь старої компоненти, нам доведеться переписати метод render з використанням renderWithDirectives, а це не завжди можливо. А ще це складно дебажити, якщо у вас є компонента

<AdminBar isShownWhen={userIsLoggedIn}/>


яка повинна з'явитися, а не з'являється, то це від того, що userIsLoggedIn помилково, або AdminBar просто не використовує renderWithDirectives? Ці проблеми можна вирішити, створивши одну маленьку компоненту, яку ми, для читаність, назвемо It. Виглядати вона може приблизно так:

var It = React.createClass({
mixins: [DirectiveComponents],
render (){
return this.renderWithDirectives(this.props.children);
}
});


І використовувати її можна так:

<It isShownWhen={reviews.length}>
<div>There are no reviews, wanna <a href="#">add</a> some?</div>
</It>


А для ще більшої доступності можна заправити це синтактическим цукром, наприклад, ось так:

<Show when={userIsLoggedIn}>
<a href="#">Lougout</a>
</Show>
<Hide when={userIsLoggedIn}>
<a href="#">Login</a>
</Hide>

<Show unless={userIsLoggedOut}>
<a href="#">Lougout</a>
</Show>
<Hide unless={userIsLoggedOut}>
<a href="#">Login</a>
</Hide>


Ну ось і все? У нас є миксим, компонента It і синтактический цукор у вигляді Show/Hide when/unless, залишилося допилити ng-class і ng-repeat і можна на гитхаб і npm? Якось все занадто просто, давайте ускладнимо собі життя, і не просто портуємо директиви Ангуляра, але і зробимо їх краще. Наприклад, ось так:

<Show when="user is logged in">
<a href="#">Logout</a>
</Show>
<Show unless="user is logged in">
<a href="#">Login</a>
</Show>
<Hide when="user is logged in">
No account? <a href="#">Sign up!</a>
</Hide>
<DisplayAll the="reviews for this product">
<div className="review-container">
<div isShownWhenThereAreNo="reviews for this product">
There are no reviews, wanna <a href="#">add</a> some?
</div>
<Review isTheTemplateForThe="review"/>
</div>
</DisplayAll>


Тут вже зрозуміліше нікуди, так як це майже англійська. Тепер-то дизайнер точно зрозуміє, що умовно з'являється/зникає, так і розробник, який бачить цей код вперше, відразу зрозуміє що до чого, йому не доведеться контрол/комманд-клікати на методи виду {this.maybeGetThisStuff()}, щоб розібратися. Та й кому не знайома ситуація, коли злобний верстальник пише злісний тікет здивованому разрабу «Чому в тебе делимитер з'являється молодий місяць?! Я ж сто разів тобі пояснював, він повинен з'являтися коли біля посту немає тамбнэйла, в Пекіні температура повітря вище 20 градусів або у поста є тамбнэйл, у Лондоні ясне небо повний місяць, але його полюбому не повинно бути в молодий місяць!» Тепер йому можна пояснити, щоб він описував англійською по білому поведінку компонентів уже на стадії верстки, наприклад ось так:

<Show when="post has no thumbnail, it's 20 C in Beijing or the sky is clear in London and the moon is full">
<div className="delimiter"/>
</Show>


Уважний читач вже напевно запитує мене «Ах ти ж демон! Ти natural language processor придумав, чи що? Так навіщо ти мені голову пудришь своїм Реактом!»
На жаль, немає, ТА я не винайшов, і Нобелівку, мабуть, вже не отримаю. Принцип роботи цих «строкових» умов схожий на роботу функцій-перекладачів, тобто, наприклад, в даному випадку:

__('Please like and subscribe!')


Рядок Please like and subscribe! є ключем асоціативного масиву, і переклад здійснюється читанням значення по цьому ключу(якщо воно є). Сверическая «__» функція у вакуумі напевно виглядає якось так:

function __(str){
return 'undefined' == typeof translations[str] ? str : translations[str];
}


Таким чином, фраза post has no thumbnail, it's 20 C in Beijing or the sky is clear in London and the moon is full послужить ключем до об'єкту, значення якого і є логічне. Після того як верстальник описав поведінку компоненти, програмісту залишилося допити каву і написати щось на кшталт:

getDesigner2CoderTranslations (){
return {
"post has no thumbnail, it's 20 C in Beijing or the sky is clear in London and the moon is full":
(!this.postHasThumbnail() && this.getTemperature('Beijing') == 20) || 
(this.getSkyState('London') == this.SKY_CLEAR && this.getMoonPhase() == this.MOON_FULL)
}
}


Непогано, от тільки де цей об'єкт з «перекладами» буде зберігається, і як ми його передавати? Ну, я тут подумав і вирішив, що найкраще буде використовувати недокументовану функцію React, під назвою context. Це щось на зразок пропов, які не передаються від безпосереднього батьків безпосереднього дитині, а від вищестоящої компоненти всім нижчестоящим, тобто дітям дітей, дітям, дітей і т. д. Ось неофіційне введення.
І, озброївшись новим знанням, давайте закінчимо:

childContextTypes: {
monstroLanguage: React.PropTypes.object
},

getChildContext: function() {
return {
monstroLanguage: {
"post has no thumbnail, it's 20 C in Beijing or the sky is clear in London and the moon is full":
(!this.postHasThumbnail() && this.getTemperature('Beijing') == 20) || 
(this.getSkyState('London') == this.SKY_CLEAR && this.getMoonPhase() == this.MOON_FULL)
}
};
}


Міксин і директиви декларують, що вони очікують такий контекст, і все «рядкові» умови будуть шукати в ньому.

contextTypes: {
monstroLanguage: React.PropTypes.object
},


Красиво стелишь, фраерок. А де ж на твою магію глянути можна?
Код я виклав на гитхаб. Зараз допиливаю документацію(точніше, перекладаю з bitbucket markdown у github markdown), приклади і заодно тестую. До вечора, думаю, закінчу, і викладу на npm. Хотілося б почути вашу думку, поради, які директиви не завадило б ще додати. Пулл реквесты, природно, всіляко вітаються.

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

0 коментарів

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