Pull to refresh

Как мы готовим React, Require и Backbone

Reading time 5 min
Views 35K
Как следует из официальной документации, React.js — V из MVC, и, как правило, вместе с ним применяются другие решения, в данном случае — Backbone.js и Require.js. А еще Jasmine, Karma и Grunt. Сегодня я поделюсь наброском проекта с применением этих инструментов.

Ссылка для нетерпеливых.

Хотелки


  • Прозрачная структура проекта;
  • Автоматизация всей рутинной работы;
  • Автоматизация тестирования;
  • Модульность;
  • Повторное использования кода;
  • Производительность.


Чего добились


Примерно так выглядит «дерево» проекта:

.
├── app
│   ├── app.js # Главный файл приложения
│   ├── bower_components # Зависимости, описанные в bower.json
│   │       └── ...
│   ├── index.html 
│   ├── scripts
│   │   ├── controllers # Backbone контроллеры 
│   │   │   └── src
│   │   │       ├── hello.jsx
│   │   │       ├── main.jsx
│   │   │       └── notfound.jsx
│   │   ├── router.js # Конфигурация роутинга
│   │   └── ui-components # React компоненты
│   │       └── src 
│   │           └── panel
│   │               ├── panel.jsx
│   │               └── panel.less
│   └── styles # Стили
│       └── src
│           └── main.less
├── bower.json # Описание зависимостей
├── Gruntfile.js 
├── install-deps.bat # Скрипты, 
├── install-deps.sh  # устанавливающие
├── install-env.bat   # зависимости
├── install-env.sh    # и окружение
├── package.json   # зависимости рабочего окружения node.js, на продакшене не нужны(Конечно, если серверная часть не на node.js)
├── server 
└── test # Тесты
    ├── test.config.js
    └── ui-components
        └── src
            └── panel.test.jsx

А так код


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

/*
 * app/app.js
 * Только этот файл подключается в index.html, все остальное делает require
 * Описывает пути к файлам проекта и запускает роутинг(Backbone).
 */
'use strict';
requirejs.config({
  baseUrl: './',
  paths: {
    app: './scripts',
    controllers: './scripts/controllers/dest', // dest - папки с результатами "компиляции" .jsx и .less
    ui: './scripts/ui-components/dest',  // В системе контроля версий не хранятся
    underscore: './bower_components/underscore/underscore',
    backbone: './bower_components/backbone/backbone',
    jquery: './bower_components/jquery/dist/jquery.min',
    react: './bower_components/react/react'
  }
});


Собственно, сам роутинг. Комментарии, думаю, излишни.

/*
 * app/scripts/router.js
 */
'use strict';
define(function(require) {
  var Backbone = require('backbone');

  var AppRouter = Backbone.Router.extend({
    routes: {
      '': 'MainCtrl',
      'hello/:name(/)': 'HelloCtrl',
      '*actions': 'NotFoundCtrl'
    },

    MainCtrl: require('controllers/main'),
    HelloCtrl: require('controllers/hello'),
    NotFoundCtrl: require('controllers/notfound')
  });

  return new AppRouter();
});


В ui-components описываются обычные React-компоненты в синтаксисе .jsx и таблицы стилей для каждого отдельного компонента. Есть нечто общее с БЭМ. Каждый компонент лежит в отдельной папке и зависит только от самого React'а.

Не только компоненты интерфейса, но и контроллеры пишутся в синтаксисе .jsx, чтобы можно было сделать вот так:

/*
 * app/scripts/controllers/src/hello.jsx
 */
'use strict';
define(['react', 'ui/panel/panel'], function(React, Panel){
  /* Аргумент из строки запроса */
  return function(name){
   /*
    * Реализуем логику приложения, например, отправляя запрос к серверу.
    * А потом рендерим компонент(ы).
    */
    React.render(
      <Panel title="Hello controller">
        <h1>Hello, {name}!</h1>
      </Panel>, document.body);
  };
});
.

Тесты


Тестировать UI сложно, поэтому Facebook любезно предоставил TestUtils специально для тестирования React компонентов, тесты для которых могут выглядеть как-то так:

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

/*
 * app/scripts/ui-components/src/panel.jsx
 */
define(['react'], function(React){
  'use strict';
  var Panel = React.createClass({
    render: function(){
      return (
      <div className="panel panel-default">
        <div className="panel-heading">
          <h1>{this.props.title}</h1>
        </div>
        <div className="panel-body">
            {this.props.children}
       </div>
     </div>);
    }
  });

  return Panel;
	
});


А это — тесты для panel, написанные с применением Jasmine, можно использовать любой фреймворк который вам нравится, например, разработчики React используют Jest. Тесты запускаются при помощи Karma, к сожалению пока и не смог завести PhantomJS для этих тестов, так что приходится мириться с постоянно всплывающим хромом.

/*
 * test/ui-components/src/panel.test.jsx
 */
'use strict';
define(['react', 'ui/panel/panel'], function(React, Panel) {
  describe('Panel behaviour tests', function() {
    var TestUtils = React.addons.TestUtils;
    var panel;
    var p;
    /*
     * Аналог this.setUp() из xxxUnit
     */
    beforeEach(function(){
      panel = TestUtils.renderIntoDocument((
        <Panel title="Test">
          <p>Paragraph content</p>
        </Panel>));
    });

    /* Проверяем что компонент вообще рендерится */
    it('Should render itself into DOM', function(){
      expect(TestUtils.isCompositeComponent(panel)).toBe(true);
    });

    /* И что заголовок, переданный атрибутом отображается */
    it('Should render title from props', function(){
      var h1 = TestUtils.findRenderedDOMComponentWithTag(panel, 'h1');
      expect(h1.getDOMNode().innerHTML).toBe('Test');
    });
    
    /* А также потомки никуда не исчезли */
    it('Should render children from props', function(){
      var paragraph = TestUtils.findRenderedDOMComponentWithTag(panel, 'p');
      /* 
       * Specific react feature, it does not render text node directly, 
       * but renders <span ... >Paragraph content</span>
       */
      expect(paragraph.getDOMNode().innerHTML).toContain('Paragraph content');
    });

  });
});


Кстати, index.html выглядит довольно коротко и аккуратно:

<html>

<head>
    <title>React+Backbone</title>
    <link rel="stylesheet" href="bower_components/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="styles/dest/styles.css">
</head>

<body>
    <div id="main"></div>
    <script type="text/javascript" src="bower_components/requirejs/require.js"></script>
    <script type="text/javascript" src="app.js"></script>
    <script src="//localhost:35729/livereload.js"></script>
</body>

</html>


Автоматизация


С ней прекрасно справляется grunt который «компилирует» less и jsx, прогоняет тесты, обновляет страничку в браузере при сохранении файлов и делает еще много прикольных вещей.

Повторное использование и модульность


В принципе, любой компонент UI можно просто взять и скопировать в другой проект, вместе со стилями и тестами (разумеется, там тоже нужен React). И он(компонент) заработает сразу, без лишних телодвижений. Особенно это актуально это для админок и типовых компонентов, там даже стили менять не надо.

И зачем все это нужно?


Во-первых, хотелось собрать все нужные инструменты в одном месте, чтобы они еще и работали. Во-вторых, я очень люблю React, использовать его с Backbone, наверное, стоит, оба легкие, шустрые и расширяемые, а Require может сделать структуру приложения прозрачнее. В-третьих получился (хочется верить) небольшой «шаблон» типового проекта, начиная разработку можно просто стянуть репозиторий и «всё сразу заработает (с)».

И что дальше?


Всё и сразу пока не работает. В ближайших планах реализовать сборку проекта на продакшен, с минификацией всего, что можно минифицировать. В чуть более далеких — написание yoman генератора для скаффолдинга котроллеров и компонентов.
Tags:
Hubs:
+17
Comments 34
Comments Comments 34

Articles