Здесь будет идти речь о том, как отдельно от всего реакт-приложения подгрузить удаленный реакт компонент и отрендерить его! Я покажу как решил эту проблему, т.к. год спустя я так и не могу найти аналогичные решения кроме как 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/ в браузере.

Поделиться с друзьями
-->

Комментарии (17)


  1. gearbox
    16.01.2017 19:54
    +2

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


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

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


    1. volodalexey
      16.01.2017 22:30

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


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


      1. gearbox
        16.01.2017 22:39
        +1

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

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

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


        1. volodalexey
          16.01.2017 22:46

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


          1. MrCheater
            16.01.2017 23:15

            Например, вот решение Webpack + SystemJS
            А вообще Webpack 2 очень дружит с SystemJS и сам бы вам всё автоматом на чанки разбил.



        1. brusher
          17.01.2017 10:21

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


          1. gearbox
            17.01.2017 10:48

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


          1. volodalexey
            17.01.2017 11:19
            +1

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


            1. mannaro
              17.01.2017 11:44

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


              1. volodalexey
                17.01.2017 11:49

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


      1. princed
        17.01.2017 10:40

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


  1. MrCheater
    16.01.2017 21:18
    +1

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


    1. volodalexey
      16.01.2017 22:33

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


  1. Sayonji
    17.01.2017 06:25

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


  1. mannaro
    17.01.2017 11:08
    +1

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


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


    1. volodalexey
      17.01.2017 11:21