Здесь будет идти речь о том, как отдельно от всего реакт-приложения подгрузить удаленный реакт компонент и отрендерить его! Я покажу как решил эту проблему, т.к. год спустя я так и не могу найти аналогичные решения кроме как 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
:
"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)
MrCheater
16.01.2017 21:18+1Я не спец по безопасности, но делать руками
new Function()
это какeval
вызывать. Я бы сто раз подумал, прежде чем пытаться такое провернуть.
Есть SystemJS. Он всё, что вам нужно, умеет.volodalexey
16.01.2017 22:33Я тоже не спец, я сразу сказал заказчику, что это небезопасно.
Хотел об этом пару строк написать в статье, но думаю многие знают.
mannaro
17.01.2017 11:08+1А для чего было сделано это извращение?
Зачем нужно иметь возможность менять компоненты на сервере?
Почему не стандартный способ с обычными компонентами + json по сети?
Расскажите, пожалуйста, про применение этого способа.
gearbox
Обратите внимание, это листинг всего исходного удаленного компонента, здесь нет никаких подгрузок модулей import… from… или… = require(...) из node_modules, иначе работать не будет.
А в чем проблема доверить сборку тому же webpack-у и держать на стороне сервера готовый бандл?
Да, и компонент может быть и функцией (что вполне оправдано если у нас stateless компоненты)
Я без троллинга, вопрос реальный — были подводные камни или просто не рассматривали такой вариант?
volodalexey
Если не добавлять в компонент
require('react')
илиimport React from 'react'
, нет смысла гонять это вебпаком, если внутри вебпак все равно использует бабел. Вот поэтому я напрямую бабелем и подготавливаю.А если добавлять, тогда каждый компонент будет включать в себя реакт, это не хорошо.
gearbox
>если внутри вебпак все равно использует бабел.
Ну использовать бабель вы ему сами говорите. Если не нужен — можно и не использовать, но это, имхо, не столь важно. Речь о том что Ваше решение отбрасывает любую возможность использовать 3-party компоненты, что как то и не айс — все самому что ли ваять ручками?
>А если добавлять, тогда каждый компонент будет включать в себя реакт, это не хорошо.
Выше отметил — речь не конкретно о реакте а о включении сторонних модулей.
volodalexey
Да, согласен, у меня к концу проекта удаленный компонент разросся до 2к строк и встал вопрос о третесторонних модулях, о том как вынести общий функционал для некоторых компонентов.
Как вариант да — делать ручками, но я не решился.
Это одна из причин написания статьи, может быть кто-то делал по-другому и решал такие проблемы.
MrCheater
Например, вот решение Webpack + SystemJS
А вообще Webpack 2 очень дружит с SystemJS и сам бы вам всё автоматом на чанки разбил.
MrCheater
https://gist.github.com/sokra/27b24881210b56bbaff7#code-splitting-with-es6
brusher
Чтобы не включать всюду реакт вебпаку в конфигурации можно указать externals.
В целом же, по-моему, затея сомнительная и не понятно как решить какие данные ему скормить (в примере только текущая дата).
Интересно узнать какая за этим бизнес-задача стояла :)
gearbox
вопрос видимо автору ) volodalexey
volodalexey
Могу рассказать, что стояла задача примерно такая.
Есть список пользователей.
При просмотре каждого пользователя формируется
url
:/user/{template}/{user_id}
Нужно было внедрить шаблонизатор для просмотра пользователя.
допустим
/user/table/1
— показать 1-го пользователя в табличной верстке/user/flex/1
— показать 1-го пользователя в резиновой верстке/user/.../1
— показать 1-го пользователя в…и так далее, используя удаленные компоненты "представление" пользователя можно сделать независимым, его может делать другой человек и можно сделать сколько угодно таких тем для показа пользователя.
mannaro
Так обычно просто команда работает с разными ветками и потом просто сливает все в одну, совместив таким образом все представления?
volodalexey
В моём случае, мы должны были сделать реакт-приложение и отдать его, а заказчик на своей стороне, может даже через пол года, хотел постоянно менять шаблоны своими силами не трогая само приложение.
princed
Можно использовать externals.