Экспрессивный 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. Хотелось бы услышать ваше мнение, советы, какие директивы не мешало бы еще добавить. Пулл реквесты, естественно, всячески приветствуются.

    Комментарии 13

      0
      Смотрю я на это, и радуюсь в душе что использую веб компоненты и Polymer как удобную обертку над ними.
      Вот ваш первый пример, полный, вместе с загрузкой данных:

      <polymer-element name="my-reviews">
      	<template>
      		<h1>Reviews</h1>
      		<template repeat="{{reviews}}">
      			<article>{{user}}: {{text}}</article>
      		</template>
      		<div hidden?="{{reviews}}">
      			There are no reviews, wanna <a href="#">add</a> some?
      		</div>
      	</template>
      	<script>
      		Polymer({
      			created	: function () {
      				var _this = this;
      				$.getJSON('api/reviews', function (reviews) {
      					_this.reviews	= reviews;
      				});
      			}
      		});
      	</script>
      </polymer-element>
      

      Не сложнее, скорее даже проще. hidden — родной атрибут во всех современных браузерах, если что)

      При этом всё изолировано, можно наследовать и вставлять где угодно на странице самым простым способом:

      <my-reviews></my-reviews>
      

      Как и в ReactJS обновляется только измененная часть DOM.

      P.S. В coffeescript получается существенно проще JS часть, вот где сахарок рулит):
      Polymer(
        created : ->
          $.getJSON 'api/reviews', (reviews) =>
            @reviews = reviews
      )
      
        0
        А как Polymer чувствует себя в мобильных браузерах, в частности в стоковом Android 4.0? По скорости нормально работает, или лучше что-то попроще использовать?
          0
          Нужно пробовать, на Android 4.4 с Firefox и Chrome нормально работает, в iOS новых тоже, а вот в таком старом Android не пробовал, нет у меня его.

          Как пример я писал приложение для Firefox OS, работает бодро на 256 MB RAM + 1.2 GHz CPU, отличие в скорости от стоковых приложений не заметно, основные тормоза с анимациями (перемещение/смещение с фоновым рисунком, есть нюансы), но с Poymer это не связано ровно никак, к тому же в Firefox OS большинство нужных фич работает через полифилы.

          В последнем Android это вообще всё будет нативно работать, так что очень рекомендую.
            0
            Ясно, спасибо за ответ.
            У меня просто задача стоит выбрать фреймворк для мобильной версии одного сайта, а я тут совсем не специалист :(
            Пока склоняюсь к Angular, как к довольно простому и не слишком тяжелому.
            Основная целевая аудитория — Android 4.0 и выше.
            Думал ещё про ReactJS, но он хоть и быстрый, но сложнее будет, а бюджет маленький, времени не особо много — вобщем как всегда.
              +1
              Polymer по-моему ещё проще, и большая часть функциональности в перспективе будет работать нативно, так что в в скорости только выиграете. К тому же есть разные способы оптимизации, например, одноразовые биндинги. И пишете вы обычный HTML/CSS, в отличии от ReactJS, оно выглядит просто и понятно, легко контролировать.
                +1
                Ключевое слово «в перспективе» :( Классическая боль веб-разработчика. Да, шэдоу-дом и инкапуслированные CSS это круто и правильно, кто бы спорил! Но пока нет нативной поддержки этих фич в 99% пользовательских браузеров — это всё фикция и тормоза. Сейчас использовать полимеры в реальном продакшене (забываем про хром, вспоминаем про ie) — это добровольно вешать на себя довольно серьёзные оверхеды полифилов. Продукт реакта, в общем, будет работать заметно быстрее, чем продукт полимера. Я на полимер облизывался ещё года полтора назад… но по моим наблюдениям, с точки зрения широкой аудитории: воз и ныне там. И более того, мне кажется, что из политических соображений, многие фичи, необходимые для реализации полимера «по полной программе», будут искусственно тормозится не-гуглами.
                  0
                  HTML5 приложение моё не тормозит, зависит от того, что за приложение. По поводу внедрения — Mozilla сказали, что уже работают, но не будут htnl импорты добавлять. Как только сделают — остальные, уверен, подтянутся.
                  Почему вы считаете, что ReactJS получится быстрее Polymer, например, в Firefox где из веб компонентов только template есть?
                    0
                    А пусть Polymer покажут бенчмарки, что мы тут все гадаем, да гадаем…
                    0
                    А почему shadow DOM это правильно?
                      0
                      По моему мнению — shadow DOM это ключевая компонента понятия «тру визуальная компонента», прошу прощения за тавтологию. Основное качество — максимальная независимость компоненты от контекста приложения, с сохранением стройной и гибкой декларативной составляющей описания + возможностью суперпозиции с другими компонентами.

                      От VCL(Delphi, c++Builder) до WPF\Adobe Flex\Qt QML — все удобные UI фрэймворки проводят эту идею. Если появится такая, нативная среде исполнения, сущность — отсохнет целая гроздь «костылей» (тот же яндекс-БЭМ — если не отсохнет совсем, то уж точно серьёзно «усохнет»).

                      Переход от <input/> к <myinput/>, где myform и myinput — мои, полностью кастомные компоненты (как по визуалу, так и по бизнес-логике) которые я могу независимо, в сторонке, описать и потом использовать так же свободно как «стандартные» тэги — это сродни перехода от WinForms к WPF, это то что уже вчера надо было.

                      Т.е. гугл тут даже не изобретает тут велосипед, он просто пытается протолкнуть производство недостающих деталей к уже готовым и объезженным велосипедам. И если у него получится — я буду только рад.
            0
            Вы знаете, когда я лоханулся с Angular(пресловутый dirty checking, 2000 биндингов хватит всем!) я выбирал, переписать все на ReactJS или на веб компонентах. И я не выбрал веб-компоненты по нескольким причинам:
            1) Минификация. Я использую WebPack(кстати, рекомендую), все опциональные модули грузятся асинхронно через AMD, а вот критические модули грузятся CommonJS-ом, то есть они попадают в один большой минифицированный .js, который грузится сразу, одним реквестом. Как мне реализовать это на веб компонентах? У меня всегда будет 100500 реквестов, пока все не подгрузится, не так ли?

            2) Как я могу из веб компоненты передать что-то выше по иерархии? Например, у меня есть компонента-виджет, которая показывает последние посты, при клике на пост, мне нужно, чтобы мэйн вьюшка показала этот пост, то есть компонента должна сообщить родителю, что пользователь хочет посмотреть пост. В React у меня для этого есть action(ну, который из Flux), который я вызываю, а в веб компонентах, мне надо было бы погрузится во все прелести callback hell.

            3) Flickering(как же это будет по-русски?), то есть «дерганье» интерфейса, пока что-то не подгрузится. То есть, если у меня есть компонента с главным меню, то на ее месте будет пусто, пока реквест не вернется, а когда вернется. она ВНЕЗАПНО появится и начнет толкать весь лэйаут в стороны. Angular, кстати, тоже этим страдает, и ng-cloak нихрена не спасает, враки это все. В реакте же я могу показать прелоудер, либо еще что, пока все реквесты не вернутся, а когда вернуться, я могу сделать всему этому красивую анимацию входа.

            В общем, мое мнение, что веб компоненты пока слишком сыры, и тот факт, что Вам приходится пользоваться полифилами, этому доказательство. Когда-нибудь весь веб перейдет на веб компоненты, это неизбежно, но этот день не сегодня. Кстати, комманда React об этом осведомлена и к этому готовится. Рождение на свет веб компонентов не будет означать смерть для фронтенд JS библиотек, ведь фреймворки будут нужны в любом случае.
              0
              1) Если честно — у меня свой движок, он разруливает зависимости модулей (плагины, расширения в семантике других движков) и подгружает минифицированные (минифицирует и объединяет тоже движок с учетом зависимостей, так же учитывая CSP) версии всех html/js/css файлов нужных на конкретной странице, так что запросов гораздо меньше чем при использовании «в лоб», к тому же у меня бывают сложные страницы, но это не одностраничник, это тоже накладывает отпечаток на особенность использования.
              2) На самом деле Polymer не собирается заменять React или Angular как таковой, он не предлагает ничего для маршрутизации и прочего. В принципе, вы можете генерировать события на элементе и ловить их выше, можно написать/взять готовое решение для pub/sub событий и использовать их вместе. Одно другому не мешает, Polymer именно для представления, не больше того.
              3) Скажем так, проблема существует. Но поставить анимацию загрузки можно элементарно, можно поставить атрибут unresolved на body, он снимется когда все веб-компоненты будут готовы. То есть вы можете добавить прелоадер в body, и показывать только его если body[unresolved], и скрывать только его в остальных случаях.

              Полифилы это да, но с другой стороны в том же React нет нативной поддержки их трюков ни в одном браузере, и не будет, так что полифилы можно сравнять с React, но при этом есть неплохая вероятность, что у пользователя одна из последних версий Chrome, и он получить приятное ускорение интерфейса.
            0
            1) Не обижайтесь, но вы изобрели велосипед, вместо того, чтобы использовать готовое, поддерживаемое решение с коммуньюти и документацией, каковым является WebPack(ну или Browserify, или RequireJS)

            2) Это я и называю колбэк хеллом. Веселье начинается, когда у нас сложная иерархия с многими уровнями компонентов, все предки будут тащит какой-нибудь onAuthorProfileLinkClicked много уровней, пока, наконец, не передадут его адресату. Pub/Sub я не люблю, т.к. это сложно дебажить, не всегда понятно, кто емитнул такой-то евент, а в определенных реализациях компоненты надо еще и ансабать, а то memory leak-ов не насчитаешься. В Flux же ансаб не нужен, а инициатора события я всегда могу найти, просто заглянув в стэк, т.к. вызов action-а синхронен. В принципе, теперь, когда я об этом думаю, Flux можно наверное использовать вместе с веб компонентами. Надо попробовать, на досуге, после праздников. Если чего дельного выйдет, отпишусь ;)

            3) То есть сделать вот так

            body[unresolved]{
            opacity: 0;
            }
            


            например? Что ж, хитро, ЕМНИП я именно так решал проблему с «конвульсиями» лэйаута в Angular. Но ведь тогда мы ограничены только анимациями CSS для анимации входа? JS анимации использовать не получится?

            В общем, пока мне все равно кажется, что для моих целей web components еще слишком «сыры». Для меня, переломный момент наступит когда кто-то сварганет большой проект на web components, я на него зайду, и скажу «Ептить, это же охренительно!», а потом нагуглю видео запись конфы с разрабами, где они будут говорить «Мы писали наш проект на web components, и это было охренительно!» Ну, а пока…

            Ах да, если Вас не затруднит, у меня появилось еще пара вопросов:
            4) А как это скалируется? Ну, то есть, во-первых, нормально ли пишутся большие проекты на web components, а во-вторых, легко ли переписать маленькое приложение в большое, как у вас с code reuse?

            5) А как это тестируется?

            Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

            Самое читаемое