noscript-react

Расширение noscript, позволяющее использовать React компоненты в качестве отображения

Usage no npm install needed!

<script type="module">
  import noscriptReact from 'https://cdn.skypack.dev/noscript-react';
</script>

README

React + Noscript

Build Status npm version Dependency Status

Оглавление

CommonJS подключеие

Подключение npm пакета noscript-react в CommonJS стиле производится следующим образом:

var NSReact = require('noscript-react');
var NS = require('noscript');
var ns = NS();

// Наложение расширения на noscript
NSReact(ns);

В этом случае React и ReactDOM будут подключены через require в пакете noscript-react.

Как это работает

Есть специальные классы ns.ViewReact, ns.ViewReactCollection и внутренний класс ns.BoxReact. Кроме того, что они имеют все те же поля, что и обычные ns.View, ns.ViewCollection и ns.Box, есть еще поле component — декларация реакт-компонента.

Например,

ns.ViewReact.define('aside', {
    component: {
        render: function() {
            return React.createElement(
                'div',
                { className: 'aside' },
                // YATE: apply /.views.menu ns-view
                this.createChildren('menu')
            );
        }
    }
});

По умолчанию, если это поле не указано или не указан метод render в нём, то отрисовывается ReactElement, реализующий тег div и внутренние вью размещаются в нём. Таким образом, сохраняется аналогичное с YATE поведение по формированию отображения ns.View. Стоит отметить, что указанному div добавляются className и data-key из props, которые может получить вьюшка в результате вызова createChildren с пропсами.

Реакт-компоненты в props получают свою вьюшку view и объект с её моделями - models.

С помощью ссылки на view в пропсах есть знания о кусочке дерева, который лежит ниже этой вьюшки, соответственно есть возможность расставить детей в шаблоне.

Обновляются компоненты по привычной ns-схеме: если реактивная вьюшка стала не валидной (поменялись данные, например), то при следующем ns.Update она будет перерисована. Перерисовка происходит средствами React.

Чтобы это реализовать пока пришлось переопределить приватный _updateHTML и _addView у ns.ViewReact и ns.ViewReactCollection. Рассчитываем на то, что в ns эти методы станут публичным, чтобы можно было законно переопределять.

Есть набор ограничений, которым стоит следовать, когда используются реактивные вьюшки и боксы:

  • корневая вьюшка app должна быть обязательно ноускриптовой;
  • реактивный бокс создаётся только когда он был описан как дочерний элемент реактивной вьюшки. В этом случае обычный бокс создан не будет. Поэтому стоит озаботится о подключении ns.BoxReact к приложению.

Сама реализация ns.ViewReact, ns.ViewReactCollection, ns.BoxReact может находиться в отдельном репо и подключаться к ns в виде плагина, по аналогии с босфорусом.

Серверный рендеринг

Для использования "реактивных" вью на сервере необходимо подключить плагин noscript-bosphorus к приложению и установить глобальных флаг ns.SERVER = true. Это позволит, используя ns.Update и метод ns.Update.prototype.generateHTML, сгенерировать на сервере HTML страницы, включая в него "реактивные" вью.

Например,

ns.SERVER = true;

ns.layout.define('index', {
    app: {
        reactView: true
    }
});

ns.View.define('app');
ns.ViewReact.define('reactView');

var appView = ns.View.create('app');
var appLayout = ns.layout.page('index');
var update = new ns.Update(appView, appLayout, {});
update.generateHTML()
    .then(function(appHTML) {
        // Тут доступен HTML приложения в appHTML
    });

События

Встроенные

Для реактивной вьюшки работают встроенные события.

ns.ViewReact.define('foo', {
    events: {
        'ns-view-init': function() {
            // доопределяем инициализацию
        },
        'ns-view-htmlinit': function() {
            // компонент инициализирован (componentWillMount)
        },
        'ns-view-show': function() {
            // компонент в DOM и виден (componentDidMount)
        },
        'ns-view-hide': function() {
            // компонент cпрячется (меняется лейаут)
        },
        'ns-view-htmldestroy': function() {
            // компонент обновится
        }
    }
})

Порядок всплытия событий сохраняется. ns.Update.prototype.perf учитывает отрисовки и обычных и реактивных видов.

«Космические»

Работают «космические» события по аналогии с обычными вью.

ns.ViewReact.define('foo', {
    events: {
        'my-global-event@show': function() {},
        'my-global-event@init': function() {}
    }
})

Наследование

Как и для обычного вида, для реактивного, можно указывать базовый вид.

ns.ViewReact.define('bar', {
    methods: {
        helloFromViewBar: function() {}
    },
    component: function() {
        helloFromComponentBar: function() {}
    }
});
ns.ViewReact.define('foo', {
    events: {
        'ns-view-htmlinit': function() {
            // унаследовали метод родительской вьюшки
            this.helloFromViewBar();
        }
    },
    component: {
        hello: function() {
            // унаследовали метод родительского компонента
            this.helloFromComponentBar();
        }
    }
}, 'bar');

Наследуются методы родительского вида, а для компонента — методы компонента родительского вида и миксины, которые были определены у родительского компонента.

API ns.ViewReact

ns.ViewReact - это наследник ns.View, который вместо YATE использует ReactComponent. Выделяется 3 типа связанных компонентов с ns.ViewReact:

  • none - компонент ещё не создавался (отсутствует).
  • root - корневой компонент. С него начинается создание вложенных в ns.ViewReact компонентов (других ns.ViewReact).
  • child - дочерний компонент. Это компонент, который размещён в какому-то root на любом уровне вложенности.
  • destroyed - компонент уничтожен в момент уничтожения ns.ViewReact.

Такое деление было введено для того, чтобы понимать, когда необходимо вызвать ReactDOM.render, а когда forceUpdate для ReactComponent.

Каждый раз, когда _updateHTML вызывается у ns.ViewReact, происходит актуализация состояния вложенных в неё вью. Это позволяет выяснить, какая часть дерева стала невалидной и перерисовать её. При первом вызове - невалидно всё дерево.

Перерисовка чаще всего вызывается на root компоненте. Но возможен вызов и на child компоненте. Например, если ns.ViewReact, содержащая child компонент, является асинхронной или обновление было вызвано через метод ns.ViewReact~update.

#mixComponent

Статичный метод ns.ViewReact, позволяющий расширить описанный при декларации view компонент базовым миксином, обеспечивающим отрисовку компонента по описанным выше правилам.

#createClass

Статичный метод ns.ViewReact. Создаёт React компонент по его декларации, который потом будет использоваться для рендринга.

#getChildView

Позволяет получить дочернее ns.ViewReact по указанному id (в случае ns.ViewCollection по указанной модели). Используется в методе createChildren связанного с view компонента, что позволяет при наследовании при необходимости переопределить поведение.

#forEachItem

Проходит по всем доступным для работы дочерним view для ns.ViewReact. В случае бокса - это активные вью, в случае коллекции - это активные элементы коллекции. Данный метод служит точкой переопределения перебора дочерних элементов в createChildren методе компонента.

#createElement

Создаёт React элемент c указанием view и models в props. В качестве ключа использует ns.ViewReact~__uniequeId. Также позволяет передать дополнительный props для создаваемого компонента.

#reactComponentType

Тип React компонента.

  • none (по умолчанию) - компонент ещё не создан
  • root - корневой (родительский) компонент
  • child - дочерний компонент
  • destroyed - компонент уничтожен

#softDestroy

"Тихо" удлаяет React компонент, связанный с ns.ViewReact. Для этого, ns.ViewReact помечается типом, что компонент уничтожен, и уничтожается. Сам же компонент будет удалён при первом же ns.Update. Используется в ns.ViewReactCollection.

API ns.ViewReactCollection

Коллекция наследуется от ns.ViewReact, поэтому имеет схожее с ним API. Определение коллекции производится аналогично ns.ViewCollection. Отличием является то, что элементы ns.ViewReactCollection - это реактивные вью ns.ViewReact. Поэтому они должны быть определены через ns.ViewReact.define.

Пример создания коллекции:

ns.Model.define('list', {
    split: {
        items: '/',
        params: {
            'id': '.id'
        },
        model_id: 'item'
    },

    methods: {
        request: function() {
            return Vow.fulfill([
                {id: 1, value: 1},
                {id: 2, value: 2},
                {id: 3, value: 3}
            ]).then(function(data) {
                this.setData(data);
            }, this);
        }
    }
});

ns.Model.define('item', {
    params: {
        id: null
    }
});

ns.ViewReactCollection.define('list', {
    models: ['list'],
    split: {
        byModel: 'list',
        intoViews: 'item'
    },
    component: {
        render: function() {
            return React.createElement(
                'div',
                { className: 'list' },
                this.createChildren()
            )
        }
    }
});

ns.ViewReact.define('item', {
    models: ['item'],
    component: {
        render: function() {
            return React.createElement(
                'div',
                { className: 'item' },
                this.state.item.value
            )
        }
    }
});

API ns.BoxReact

Поведение ns.BoxRact, его методы и описание в layout полностью соответствует ns.Box. Поэтому каких-то особых правил описания его в lyaout нет.

API ReactComponent

Каждый компонент, связанный с реактивной вьюшкой, расширяет поведение реакт-компонента с помощью специального миксина.

getModel

Возвращает модель по id

getModelData

Возвращает данный указанной модели по определенному jpath. Если jpath не указан — вернутся все данные.

ns.ViewReact.define('articleCaption', {
    models: ['article'],
    component: {
        render: function() {
            return React.createElement(
                'h1',
                { className: 'article-caption' },
                // YATE: model('article').caption
                this.getModelData('article', '.caption')
            )
        }
    }
});

createChildren

Аналог apply /.views.view ns-view или apply /.views.* ns-view в yate.

Создаст реакт-элементы для указанных реактивных вьюшек, если они есть среди активных потомков текущей вьюшки. Если указанной вьюшки нет, вернет null. Позволяет передать props для создаваемых реакт-элементов.

Возможные варианты вызова:

this.createChildren() // создаст компоненты для всех дочерних view

this.createChildren({length: 25}); // создаст компоненты для всех дочерних view и передаст им указанные props

this.createChildren('child-view') // создаст дочернее view с id `child-view`.

this.createChildren('child-view', {length: 25}) // создаст дочернее view с id `child-view` и передаст в неё указанные props

this.createChildren(['child-view1', 'child-view2']); // создаст дочерние view с id `child-view1`, `child-view2`

this.createChildren(['child-view1', 'child-view2'], {length: 25}); // создаст дочерние view с id `child-view1`, `child-view2`  и передаст в них указанные props

Различия:

  1. Для ns.ViewReact метод принимает id вьюшек, которые нужно создать, и props для их компонентов.
  2. Для ns.ViewReactCollection метод принимает модели коллекций, с которыми связаны создаваемые вьюшки, и props для компонентов элементов коллекции.

Особенности

Дефолтный displayName

Если не указывать displayName у компонента, то он будет сгенерирован автоматически на основании айдишника вьюшки, приведенный из camelCase к минус-разделителям.

ns.ViewReact.define('myView', {
    component: {
        render() {
            // my-view
            console.log(this.constructor.displayName)
        }
    }
})

Это удобно, если использовать реакт-миксин для генерации БЭМ-классов, который в качестве имени блока берет displayName компонента.

Если displayName определен в декларации явно, то будет использован он.

Работа со стейтом

Реактивные вьюшки могут использовать стейт реакт-компонента. Стейт сохраняется между перерисовками вьюшки.

setState не вызывает перерисовку

Простой вызов setState не вызывает перерисовку компонента, связанного с вью. Для того, чтобы это произошло необходимо явно вызвать this.props.view.invalidate(), перед тем как устанавливать новый стейт.

У реакт-компонента, который связан с вью определен shouldComponentUpdate, который разрешит перерисовку в одном из следующих случаев:

  • вьюшка невалидная (поменялись версии моделек или был вызван invalidate)
  • один из деток невалидный
  • у вьюшки еще нет экземпляра компонента

Умные перерисовки в боксах

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

  • создается меньше экземпляров реакт-компонентов
  • перерисовки между двумя экземлярами вида происходит в виртуальном доме, именно за счет того, что render возвращает тот же самый экземпляр реакт-компонента