Удаленные AJAX компоненты для ReactJS

    Здесь будет идти речь о том, как отдельно от всего реакт-приложения подгрузить удаленный реакт компонент и отрендерить его! Я покажу как решил эту проблему, т.к. год спустя я так и не могу найти аналогичные решения кроме как react-remote-component-demo.



    При разработке проекта на React была поставлена задача, необходимо чтобы одностраничное приложение на React подгружало по AJAX дополнительные компоненты и показывало их, дополнительную сложность составляло то, что эти компоненты должны правиться на сервере независимо от самого приложения.


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


    Т.к. при сборке я использовал Webpack, то первые же попытки нагуглить что-то приводили к использованию require.ensure.


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


    Соответственно оказалось невозможным использовать CommonsChunkPlugin, чтобы сказать вебпаку, вот такие-то входные файлы положить отдельно туда-то, т.к. я не знаю сколько файлов будет.


    Итого само реакт-приложение собирается с помощью вебпака, а удаленные компоненты подготавливаются отдельно (удаленные в данном случае от самого реакт-приложения, поэтому я дал им такое определение).


    Удаленные компоненты я также хотел красиво писать на ES6. Значит необходимо было использовать Babel дополнительно для компиляции моих удаленных компонентов.


    Методом проб и ошибок я смог заставить компилироваться мой удаленный компонент.


    Компонент выглядел так:


    class CMP extends React.Component {
      constructor(props) {
        super(props);
      }
    
      render() {
        return <div>
          <div>Hello from <strong>FIRST</strong> remote component!</div>
          <div>{this.props.now}</div>
        </div>
      }
    }
    
    module.exports = CMP;

    Обратите внимание, это листинг всего исходного удаленного компонента, здесь нет никаких подгрузок модулей import ... from ... или ... = require(...) из node_modules, иначе работать не будет. Дополнительно в конце стоит module.exports.


    Вот такой компонент на ES6 1.jsx я могу компилировать в ES5 1.js с помощью бабеля чтобы он не ругнулся.


    После компиляции у меня есть готовый текстовый файл с удаленным компонентом 1.js:


    1.js
    "use strict";
    
    var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
    
    function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
    
    function _possibleConstructorReturn(self, call) { if (!self) { throw new ReferenceError("this hasn't been initialised - super() hasn't been called"); } return call && (typeof call === "object" || typeof call === "function") ? call : self; }
    
    function _inherits(subClass, superClass) { if (typeof superClass !== "function" && superClass !== null) { throw new TypeError("Super expression must either be null or a function, not " + typeof superClass); } subClass.prototype = Object.create(superClass && superClass.prototype, { constructor: { value: subClass, enumerable: false, writable: true, configurable: true } }); if (superClass) Object.setPrototypeOf ? Object.setPrototypeOf(subClass, superClass) : subClass.__proto__ = superClass; }
    
    var CMP = function (_React$Component) {
      _inherits(CMP, _React$Component);
    
      function CMP(props) {
        _classCallCheck(this, CMP);
    
        return _possibleConstructorReturn(this, (CMP.__proto__ || Object.getPrototypeOf(CMP)).call(this, props));
      }
    
      _createClass(CMP, [{
        key: "render",
        value: function render() {
          return React.createElement(
            "div",
            null,
            React.createElement(
              "div",
              null,
              "Hello from ",
              React.createElement(
                "strong",
                null,
                "FIRST"
              ),
              " remote component!"
            ),
            React.createElement(
              "div",
              null,
              this.props.now
            )
          );
        }
      }]);
    
      return CMP;
    }(React.Component);
    
    module.exports = CMP;

    Этот файл уже можно отдавать статикой или из базы данных.


    Осталось загрузить этот файл в реакт-приложение, сделать из него компонент и отрендерить его.


    Компонент, который будет делать это назовём Remote. А для отображения списка назовём List.
    Логика примерно такая, List слушает событие пользователя click, определяет какой элемент списка был нажат и соответственно такое свойство component я и передаю в Remote в качестве props.
    Внутри Remote я использовал componentWillReceiveProps() функцию. Так я определял, что свойство изменилось и мне необходимо отрендерить детальный просмотр переданного удаленного компонента. Для этого я проверяю, есть ли он в кеше компонента, и если нет то подгружаю.


    Подгрузить наш удаленный компонент не составляет труда (использую более высокоуровневую обертку над XMLHttpRequest для наглядности).
    Вся магия происходит дальше:


          ajax.load('/remote/' + requiredComponent)
            .then((str_component) => {
              let component;
              try {
                let
                  module = {},
                  Template = new Function('module', 'React', str_component);
    
                Template(module, React);
                component = React.createFactory(module.exports);
              } catch (e) {
                console.error(e);
              }
            })

    Из подгруженной строки/компонента я делаю функцию-шаблон new Function(). В качестве входных параметров определяем две строковые переменные module и React. Этот шаблон я теперь "вызываю" Template(module, React).


    Создаю новый объект module = {} и передаю его на первое место, а на второе место передаю модуль реакт.


    Таким образом, если вспомнить что мы писали внутри удаленного компонента:


    ... extends React ...

    и


    module.exports = ...

    При "вызове" нашей функции/шаблона мы передаем эти две переменные, ошибки быть не должно т.к. мы определили эти две переменные.


    В результате в наш объект module = {} присвоиться результат в свойство exports. Так мы решаем две проблемы, обходим ошибки на этапе компиляции компонента, используя module.exports и React. И определив их как входные параметры "выполняем" наш шаблон уже в браузере.


    Осталось создать наш компонент component = React.createFactory(module.exports) и отрендерить его:


      render() {
        let component = ...;
        return <div>
          {component ?
            component({ now: Date.now() }) :
            null}
        </div>
      }

    При вызове нашего компонента можно передать любые параметры component({ now: Date.now() }), которые будут видны как props.


    Наш удаленный компонент работает как родной!


    Код упрошенного приложения выложил на гитхаб react-remote-component:
    Для запуска выполняем следующее:


    npm install устанавливаем все модули


    npm run build-cmp компилиреум наши удаленные компоненты в dist/remote


    npm run server-dev запускаем дев сервер вебпака, кторый будет собирать все приложение в оперативную память и раздавать оттуда, а удаленные компоненты будет раздавать как статику.


    Заходим на http://localhost:8099/ в браузере.

    Share post
    AdBlock has stolen the banner, but banners are not teeth — they will be back

    More
    Ads

    Comments 17

      +2
      Обратите внимание, это листинг всего исходного удаленного компонента, здесь нет никаких подгрузок модулей import… from… или… = require(...) из node_modules, иначе работать не будет.


      А в чем проблема доверить сборку тому же webpack-у и держать на стороне сервера готовый бандл?
      Да, и компонент может быть и функцией (что вполне оправдано если у нас stateless компоненты)

      Я без троллинга, вопрос реальный — были подводные камни или просто не рассматривали такой вариант?
        0

        Если не добавлять в компонент require('react') или import React from 'react', нет смысла гонять это вебпаком, если внутри вебпак все равно использует бабел. Вот поэтому я напрямую бабелем и подготавливаю.


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

          +1
          >если внутри вебпак все равно использует бабел.

          Ну использовать бабель вы ему сами говорите. Если не нужен — можно и не использовать, но это, имхо, не столь важно. Речь о том что Ваше решение отбрасывает любую возможность использовать 3-party компоненты, что как то и не айс — все самому что ли ваять ручками?

          >А если добавлять, тогда каждый компонент будет включать в себя реакт, это не хорошо.
          Выше отметил — речь не конкретно о реакте а о включении сторонних модулей.
            0

            Да, согласен, у меня к концу проекта удаленный компонент разросся до 2к строк и встал вопрос о третесторонних модулях, о том как вынести общий функционал для некоторых компонентов.
            Как вариант да — делать ручками, но я не решился.
            Это одна из причин написания статьи, может быть кто-то делал по-другому и решал такие проблемы.

            0

            Чтобы не включать всюду реакт вебпаку в конфигурации можно указать externals.
            В целом же, по-моему, затея сомнительная и не понятно как решить какие данные ему скормить (в примере только текущая дата).
            Интересно узнать какая за этим бизнес-задача стояла :)

              0
              вопрос видимо автору ) volodalexey
                +1

                Могу рассказать, что стояла задача примерно такая.
                Есть список пользователей.
                При просмотре каждого пользователя формируется url:
                /user/{template}/{user_id}
                Нужно было внедрить шаблонизатор для просмотра пользователя.
                допустим
                /user/table/1 — показать 1-го пользователя в табличной верстке
                /user/flex/1 — показать 1-го пользователя в резиновой верстке
                /user/.../1 — показать 1-го пользователя в…
                и так далее, используя удаленные компоненты "представление" пользователя можно сделать независимым, его может делать другой человек и можно сделать сколько угодно таких тем для показа пользователя.

                  0

                  Так обычно просто команда работает с разными ветками и потом просто сливает все в одну, совместив таким образом все представления?

                    0

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

              0
              Можно использовать externals.
            +1

            Я не спец по безопасности, но делать руками new Function() это как eval вызывать. Я бы сто раз подумал, прежде чем пытаться такое провернуть.
            Есть SystemJS. Он всё, что вам нужно, умеет.

              0

              Я тоже не спец, я сразу сказал заказчику, что это небезопасно.
              Хотел об этом пару строк написать в статье, но думаю многие знают.

              0

              А в react native так можно?

                +1

                А для чего было сделано это извращение?
                Зачем нужно иметь возможность менять компоненты на сервере?
                Почему не стандартный способ с обычными компонентами + json по сети?


                Расскажите, пожалуйста, про применение этого способа.

              Only users with full accounts can post comments. Log in, please.