Как следует из официальной документации, 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 генератора для скаффолдинга котроллеров и компонентов.
Автор: iLikeKoffee