Я встречаю много статей, где описываются плюсы применения React вместе с Meteor. Ни разу не видел, чтобы кто-то шёл дальше «плюсов» и описал то, как, собственно, это сделать.

При имплементации возникает пара серьезных проблем.

Первая — перенос существующего фронтенда на React по частям, вторая — использование сторонних библиотек для React.

Решение проблем под катом.



Использование React компонентов в существующем Meteor приложении



Для того, чтобы установить поддержку React, достаточно поставить этот пакет: github.com/reactjs/react-meteor

Он добавляет обработчик jsx файлов и глобальный объект React на фронтенд и на бекенд. Так же он добавляет свою обертку для компонентов, но она в данный момент плоха, так как не работает с существующим в Meteor роутингом.

Выражается это в том, что при удалении родительского Blaze шаблона (при переключении пути) React компонент как ни в чем не бывало остается на странице.

А данный момент я использую свою обертку, позволяющую сделать из React компонента Blaze шаблон, который можно включать в существующие шаблоны, по частям переводя фронтенд с Blaze на React.

Пока я это писал, в комментариях к англоязычной статье подсказали github.com/grovelabs/meteor-react. Обновлю, как только протестирую.

Моя обертка выглядит так:

_ReactUtils.createClass = function(opts) {
  var templateName = opts.templateName;
  var templateClass = new Template(
    templateName,
    function() {
      return new HTML.DIV;
    }
  );
  Template[templateName] = templateClass;
  var Component = React.createClass(opts);
  templateClass.onRendered(function() {
    var template = this;
    var data = this.data || {};
    var c = React.createElement(Component, data);
    c._meteorTemplate = template;
    template._reactComponent = React.render(c, template.firstNode);
  });
  templateClass.onDestroyed(function() {
    var template = this;
    React.unmountComponentAtNode(template._reactComponent.getDOMNode());
  });
  return Component;
};


И позволяет создать Blaze шаблон из React компонента, при этом компонент будет знать о том, уничтожился ли шаблон и в этом случае удаляться со страницы.

Так же названные выше библиотеки предлагают свои варианты получения данных компонентом из рективных источников (Mongo курсоры, которые связывают бекенд и фронтенд Метеора, или Session/ReactiveVars, которые могут быть использованы для коммуникации внутри фронтенд приложения).

Так как в данный момент я использую первую библиотеку и ее вариант мне не нравится, я просто подписываюсь на реактивные источники сам, напрямую или через сервисы (в сервисах использую BaconJS, чтобы возвращать Streams а данными).

Другим, более «чистым» вариантом, было бы использование ванильного flux'a или reflux / fluxxor / любые другие его имплементации. Я не буду описывать это в деталях, так как пока толком не разобрался сам.

Сейчас делаю так:

componentWillMount: function() {
    this._cancelSubscription = Tracker.autorun(...); 
}

componentWillUnmount: function() {
    this._cancelSubscription();
}


Использование сторонних React библиотек в Meteor



Главная проблема здесь в том, что, чтобы использовать какую-либо библиотеку в Meteor, её надо либо засунуть в код твоего приложения (сразу отбрасываем), либо сделать специальный Meteor Package.

Meteor Packages необходимы, но неудобны (для создателей, не для пользователей) по двум причинам.

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

Вторая — связанная непосредственно с React, а, вернее, с повсеместным спользованием в React библиотеках webpack/browserify билдов — Meteor пакет не умеет работать с this. Экспортировать объект библиотеки надо глобально, в стиле:

MyLibrary = {...};


Это обязательное требование, и пока что разработчики не собираются его менять.

Webpack/Browserify же делают так:

(function(...){...})(this, ...);


И это не работает. Каждый билд вручную необходимо вбивать специальный костыль в стиле

var meteorHack = {
  React: React
};
(function webpackUniversalModuleDefinition(root, factory) {
	...
})(meteorHack, ...) // вместо this - meteorHack!

...

MyLibrary = meteorHack.MyLibrary;


Тогда все будет хорошо. Обращаю внимание, что React в билде игнорируется, так как мы зависим от пакета, который его уже инициализировал:

Package.onUse(function(api) {
  api.use('reactjs:react@0.2.1', ['client', 'server']); // может быть, здесь я позже использую другую библиотеку, выкинув reactjs:react@0.2.1, так как он довольно несуразный
  api.addFiles('dist/meteor-dist.js', ['client', 'server']);
  api.export('MyLibrary', ['client', 'server']);
});


Соответственно, при билде React так же игнорируется, пример для webpack:

module.exports = {
    entry: "./index.js",
    output: {
        path: __dirname,
        filename: "dist/meteor-dist.js",
        libraryTarget: "umd",
        library: "MyLibrary"
    },
    externals: {
        react: 'React'
    }
};


Моей последней рекомендацией будет форкать библиотеку, которую хочется перенести в Meteor, а не создавать свой собственный репозиторий. Так чище.

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


  1. KeepYourMind
    19.04.2015 11:53
    +1

    Webpack/Browserify же делают так: И это не работает. Каждый билд вручную необходимо вбивать специальный костыль в стиле

    Стоит упомянуть, что для webpack костыль не нужен, и он умеет это делать сам:

    {output: {library: 'MyLibrary'}, externals: {React: 'react'}}
    


    1. Firfi Автор
      19.04.2015 13:10

      Так и делаю, вот пример моего конфига:

      module.exports = {
          entry: "./index.js",
          output: {
              path: __dirname,
              filename: "dist/meteor-dist.js",
              libraryTarget: "umd",
              library: "t"
          },
          externals: {
              react: 'React'
          }
      };
      
      


      Проблема в том, что в Meteor нельзя получить глобальный объект, что подтверждено разработчиками (в стиле «нам это не нужно, сами разбирайтесь»), можно только назначить во так:

      MyLibrary = ...
      


      То есть без var, this, global, etc...; this в данном случае — не то же самое, и не содержит необходимых ссылок.