Как стать автором
Обновить

Экспрессивный ReactJS или тырим фичи Angular в наш фреймворк

Время на прочтение 6 мин
Количество просмотров 9.7K
В последние время, 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. Хотелось бы услышать ваше мнение, советы, какие директивы не мешало бы еще добавить. Пулл реквесты, естественно, всячески приветствуются.
Теги:
Хабы:
+2
Комментарии 13
Комментарии Комментарии 13

Публикации

Истории

Работа

Ближайшие события

Московский туристический хакатон
Дата 23 марта – 7 апреля
Место
Москва Онлайн