React.js: собираем с нуля изоморфное / универсальное приложение. Часть 1: собираем стек
Я решил написать цикл статей, который и сам был бы счастлив найти где-то полгода назад. Он будет интересен в первую очередь тем, кто хотел бы начать разрабатывать классные приложения на React.js, но не знает, как подступиться к зоопарку разных технологий и инструментов, которые необходимо знать для полноценной front-end разработки в наши дни.
Я хочу с нуля реализовать, пожалуй, наиболее востребованный сценарий: у нас есть серверная часть, которая предоставляет REST API. Часть его методов требует, чтобы пользователь веб-приложения был авторизован.
За скобками останутся такие вопросы как интернационализация, написание тестов, деплой и варианты работы с CSS, так как эти вопросы гораздо более вариативны и каждый из них тянет на отдельный блок статей. Возможно, я вернусь к ним позже, если будет спрос.
Примечание: на github есть множество замечательных boilerplate или уже собранных стеков, которые можно использовать в качестве фундамента для своего приложения, но мне кажется, гораздо правильнее собирать свой стек самостоятельно, понимая, что делает каждый пакет и каждая строка вашего кода. Научившись делать это один раз, в следующий раз сбор стека займет едва ли более 5 минут.
Примечание 2: я предполагаю, что у разных читателей разный уровень подготовки, поэтому длинное описание инструментов я буду прятать под кат, чтобы статья не казалась бесконечно длинной.
1. Мы будем разрабатывать изоморфное веб-приложение.
Изоморфное или универсальное приложение означает, что JavaScript код приложения может быть выполнен как на сервере, так и на клиенте. Этот механизм является одной из сильных сторон React и позволяет пользователю получить доступ к контенту существенно быстрее. Ниже я буду использовать термин "изоморфное", так как он пока еще встречается чаще, но важно понимать, что "изоморфное" и "универсальное" — это одно и то же.
С точки зрения пользователя взаимодействие с веб-приложением выглядит следующим образом1) Браузер выполняет запрос к нашему веб-приложению. 2) Серверная часть Node.js выполняет JavaScript. Если необходимо, в процессе также выполняет запросы к API. В результате получается готовая HTML-страница, которая отправляется клиенту. 3) Пользователь получает контент страницы почти мгновенно. В это время в фоне скачивается и инициализируется клиентский JavaScript, и приложение "оживает". Самое главное, что пользователь имеет доступ к контенту почти сразу, а не спустя две и более секунды, как это бывает в случае традиционных client-sideJavaScript приложений. 4а) Если JavaScript не успел загрузиться или выполнился на клиенте с ошибкой, то при переходе по ссылке выполнится обычный запрос к серверу, и мы вернемся на первый шаг процесса. 4б) Если все в порядке, то переход по ссылке будет перехвачен нашим приложением. Если необходимо, выполнится запрос к API и клиентский JavaScript* сформирует и отрендерит запрошенную страницу. Такой подход уменьшает трафик и делает приложение более производительным.
Почему это круто? 1) Пользователь получает контент быстрее на две и более секунды. Особенно это актуально, если у вас не очень хороший мобильный интернет или вы в условном Китае. Выигрыш получается за счет того, что не надо дожидаться скачивания клиентского JavaScript, а это 200кб и более с учетом минификации и сжатия. Также инициализация JavaScript может занимать определенное время. Если сюда добавить необходимость делать клиентские API запросы после инициализации и вспомнить, что на мобильном интернете часто можно столкнуться с весьма ощутимыми задержками, то становится очевидно, что изоморфный подход делает ваше приложение гораздо приятнее для пользователя. 2) Если ваше клиентское JavaScript приложение перестало работать из-за ошибки, то ваш сайт скорее всего станет бесполезным для пользователя. В изоморфном же случае есть хороший шанс, что пользователь все же сможет сделать то, что он хочет.
С точки зрения реализацииУ нас есть две точки входа: server.js и client.js. Server.js будет использован сервером node. В нем мы запустим express или другой веб-сервер, в него же поместим обработку запросов и другую специфичную для сервера бизнес-логику. Client.js — точка входа для браузера. Сюда мы поместим бизнес-логику, специфичную для клиента. React-приложение будет общим как для клиента, так и для сервера. Оно составляет более 90-95% исходного кода всего приложения — в этом и заключается вся суть изоморфного / универсального подхода. В процессе реализации мы увидим, как это работает на практике.
Создаем новый проектНа первый взгляд версионность ноды может показаться немного странной. Чтобы не запутаться, достаточно знать, что v4.x — это LTS ветка, v5.x — экспериментальная, а v6.x — будущая LTS, начиная с 1 октября 2016 года. Я рекомендую устанавливать последнюю LTS версию, то есть на день публикации статьи — это 4ая, так как это убережет от хоть и маловероятного, но крайне неприятного столкновения с багами самой платформы. Для наших целей особой разницы между ними все равно нет.
Перейдя по ссылке https://nodejs.org/en/download/ можно скачать и установить node.js и пакетный менеджер npm для вашей платформы.
На все вопросы npm можно смело нажимать кнопку enter, чтобы выбирать значения по умолчанию. В результате в корневой директории проекта появится файл package.json.
2. Процесс разработки
JavaScript в последние годы развивается семимильными шагами, что нельзя сказать о пользователях. К сожалению, мы не можем поддерживать только последние версии браузеров, так как мало кто среди заказчиков готов пожертвовать существенной частью аудитории в угоду "классным техническим фичам". К счастью, придуманы инструменты, которые позволяют программисту использовать наиболее современные конструкции языка и писать так, как ему удобно, а в результате будет получаться код, который будет работать правильно даже в очень древних браузерах.
Babel
Babel — это компилятор, который транслирует любой диалект JavaScript, включая CoffeeScript, TypeScript и другие надстройки над языком в JavaScript ES5, который поддерживается почти всеми браузерами, включая IE8, если добавить babel-polyfill. Сила Babel в его модульности и расширяемости за счет плагинов. Например, уже сейчас можно использовать самые последние фишки JavaScript, не переживая, что они не будут работать в старых браузерах.
Для трансляции компонентов реакта мы будем использовать пресет babel-preset-react. Мне очень нравятся декораторы JavaScript, поэтому нам также понадобится пакет babel-plugin-transform-decorators-legacy. Чтобы наш код корректно работал в старых браузерах, мы установим пакет babel-polyfill, а babel-preset-es2015 и babel-preset-stage-0 нам нужны, чтобы писать на ES6/ES7 диалектах соответственно.
Эти зависимости надо устанавливать как зависимости проекта, так как серверной части приложения тоже нужен babel.
Теперь установим пакеты, которые нужны для сборки клиентского JavaScript. Их мы установим в качестве зависимостей разработки, так как в продакшене они не нужны.
.babelrcПри запуске babel будет обращаться к файлу .babelrc в корне проекта, в котором хранится конфигурация и список используемых preset'ов и плагинов.
Создадим этот файл
3. Сборка
Мы установили Babel с нужными нам плагинами, но кто и когда должен его запускать? Настало время перейти к этому вопросу.
Примечание: проекты веб-приложений в наши дни с виду мало отличаются от проектов десктопных или мобильных приложений: они будут содержать внешние библиотеки, файлы, скорее всего соответствующие парадигме MVC, ресурсы, файлы стилей и многое другое. Такое представление будет очень удобно для программиста, но не для пользователя. Если взять весь исходный код JavaScript проекта, а также используемых библиотек, выкинуть все лишнее, объединить в один большой файл и применить минификацию, то полученный на выходе один файл может занимать в 10 и более раз меньше, чем изначальный набор. Также потребуется всего лишь один, а не сотни, запрос браузера, чтобы скачать всю логику нашего приложения. И то, и другое очень важно для производительности. К слову, та же логика применима и для CSS-ресурсов, включая разные диалекты (LESS, SASS и пр.).
Эту полезную работу будет выполнять webpack.
Примечание: для этой же цели можно применять сборщики: grunt, gulp, bower, browserify и другие, но исторически для React чаще всего используется именно webpack.
webpackАлгоритм работы webpack
Проще всего представить работу webpack как конвейер. Webpack возьмет предоставленные точки входа и последовательно обойдет все зависимости, которые встретит на своем пути. Весь код, написанный на JavaScript или его диалектах, он пропустит через babel и слепит в один большой JavaScript ES5 файл. Здесь стоит более подробно остановиться на том, как это работает. Каждый require или import в вашем коде и коде используемых node_modules webpack выделит в свой отдельный небольшой модуль в итоговой сборке. Если ваш код или код библиотек, которые вы используете, зависят от одной и той же функции, то в итоговую сборку она попадет только один раз в виде модуля Webpack, а все куски кода, которые от него зависят, будут ссылаться на один и тот же модуль в итоговой сборке. Еще одна крутая особенность процесса сборки webpack заключается в том, что если вы используете огромную библиотеку, например lodash, но явно указываете, что вам нужна только определенная функция, например
то в итоговую сборку войдет лишь используемая часть библиотеки, а не вся она целиком, что может существенно уменьшить размер собранного файла.
Примечание: это будет работать, только если используемая библиотека поддерживает модульность. По этой причине автор отказался от использования в своих проектах библиотек Moment.js, XRegExp и ряда других.
Для разного типа файлов нашего проекта в конфигурации webpack мы определим свой loader или цепочку loader'ов, которые будут его обрабатывать.
webpack-dev-serverКаждый раз пересобирать весь проект может быть весьма накладно: для проекта среднего размера сборка легко может достигать 30 и более секунд. Чтобы решить эту проблему, во время разработки очень удобно использовать webpack-dev-server. Это стороннее серверное приложение, которое при запуске произведет полную сборку ресурсов и при обращении к ним будет отдавать последнюю их версию из оперативной памяти. В процессе разработки при изменении отдельных файлов webpack-dev-server оно на лету будет перекомпилировать только тот файл, который изменился, и подменять старый модуль на новый в итоговой сборке. Так как пересобирать требуется не весь проект, а только один файл, то это редко занимает более секунды.
Webpack и webpack-dev-server мы установим в качестве зависимостей разработки, так как мы, разумеется, не будем заниматься сборкой на продакшене.
Примечание: на момент написания и публикации статьи актуальна была версия webpack 1. С 22 сентября 2016 года по умолчанию устанавливается webpack 2 beta.
Хорошо, теперь нам необходимо написать файл конфигурации для сборки. Создаем файл в корне проекта
webpack.config.jsПримечание: это пример конфига для продуктивного проекта, поэтому он выглядит немного сложнее, чем мог бы.
Итак, 1) Мы объявляем, что реализацию промисов мы будем использовать из проекта bluebird. Де-факто стандарт. 2) Для продакшена мы хотим, чтобы у каждого файла был хеш сборки, чтобы эффективно управлять кешированием ресурсов. Чтобы старые версии собранных ресурсов нам не мешали, мы будем использовать clean-webpack-plugin, который будет очищать соответствующие директории до осуществления очередной сборки. 3) extract-text-webpack-plugin в процессе сборки будет выискивать все css/less/sass/whatever зависимости и в конце оформит их в виде одного CSS файла. 4) Мы используем DefinePlugin, чтобы задать глобальные переменные сборки, DedupePlugin и OccurenceOrderPlugin — оптимизационные плагины. Более подробно с этими плагинами можно ознакомиться в документации. 5) В качестве входной точки мы укажем babel-polyfill и client.js. Первый позволит нашему JavaScript коду выполняться в старых браузерах, второй — наша точки входа клиентского веб-приложения, мы напишем его позже. 6) resolve означает, что когда мы пишем в коде нашего приложения
webpack будет искать SomeClass в файлах SomeClass.js или SomeClass.jsx прежде, чем сообщит, что не может найти указанный файл. 7) Далее мы передаем список плагинов и указываем output — директорию, в которую webpack положит файлы после сборки. 8) Самое интересное — список loaders. Здесь мы определяем конвейеры, о которых говорилось выше. Они будут применены для файлов с соответствующими расширениями. Более подробно с форматом определения лоадеров и их параметрами лучше ознакомиться в документации, так как этот вопрос тянет на отдельную статью. Чтобы не быть уж совсем голословным, остановлюсь на конструкции лоадера.
Здесь мы говорим, что если встретится файл с расширением less, то его нужно передать ExtractTextPlugin, который будет использовать цепочку css-loader!postcss-loader!less-loader. Читать следует справа налево, то есть сначала less-loader обработает .less файл, результат передаст postcss-loader, который в свою очередь передаст обработанное содержимое css-loader. 9) О devtool также рекомендую прочитать в документации вебпака. 10) В конце мы дополнительно укажем параметры webpack-dev-server. В данном случае нам важно указать Access-Control-Allow-Origin, так как webpack-dev-server и наше приложение будут работать на разных портах, а значит нужно решить проблему CORS.
Библиотеки, на которые мы ссылаемся в конфигурации, сами себя не установят. В зависимости проекта у нас пойдет только bluebird, все остальное нужно исключительно для сборки.
Также настало время добавить несколько новых скриптов package.json: для запуска сборки и вебпак-дев-сервера.
Update: пользователи wrewolf и Nerop в личке и комментариях соответственно сообщили, что в Windows скрипты должны выглядеть иначе.
Создадим в корне проекта папку src, а в ней — пустой файл client.js.
Протестируем наши скрипты: введем в консоли npm run build, а в другом окне консоли — npm run webpack-devserver. Если нет ошибок — двигаемся дальше.
4. ESLint
Это необязательный пункт, но я нахожу его очень полезным. ESLint — это совокупность правил, которые предъявляются к исходному коду. Если одно или несколько из этих правил нарушаются программистом в процессе написания кода, во время сборки webpack'ом мы увидим ошибки. Таким образом весь код веб-приложения будет написан в едином стиле, включая именование переменных, отступы, запрет использования определенных конструкций и так далее.
Список правил я положу в файл .eslintrc в корне проекта. Более подробно о ESLint и правилах можно прочитать на сайте проекта.
Примечание: в Windows правило
надо заменить на
Мы добавим eslint-loader в конфигурацию webpack, таким образом перед тем, как babel транслирует наш код в ES5, весь наш код будет проверен на соответствие заданным правилам.
webpack.config.js5. Express и Server.js
К текущему моменту мы настроили сборку проекта, трансляцию кода в JS ES5, описали и реализовали проверку исходного кода "на вшивость". Пришло время перейти к написанию самого приложения, а точнее — его серверной части.
Примечание: я использую Express, и меня он полностью устраивает, но, разумеется, есть множество других аналогичных пакетов (это же Node.js).
Создаем в корне файл server.js со следующим содержимым
server.jsЗдесь мы указываем, что нам нужен babel для поддержки ES6/ES7, а также что если node встретит конструкции вида
то эту строчку нужно просто проигнорировать, так как это не JavaScript или один из его диалектов.
Сам код серверной части будет в файле src/server.js, в котором мы теперь можем свободно использовать ES6/ES7 синтаксис.
src/server.jsЗдесь все достаточно просто: мы импортируем веб-сервер express, запускаем его на порту, который был передан в переменной окружения PORT или 3001. Сам сервер на любой запрос будет отдавать ответ: "Hello World"
Мы установим пакет nodemon в зависимости разработки, чтобы запустить серверный JavaScript код. Он удобен тем, что выводит любые ошибки с подробными Stack Traces сразу в консоль по мере их возникновения.
Добавим еще один скрипт в package.json
И запустим в консоли
Откроем браузер и попробуем открыть страницу http://localhost:3001. Если все хорошо, то мы увидим Hello World.
6. React и ReactDOM
Поздравляю! Почти все приготовления завершены, и мы можем наконец-то переходить к реакту.
Установим соответствующие библиотеки:
Также установим react-hot-loader: при изменении исходного кода компонентов в процессе разработки, браузер будет перезагружать страницу автоматически. Это очень удобная фишка, особенно если у вас несколько мониторов.
Примечание: за время пребывания статьи в песочнице в npm изменилась версия пакета react-hot-loader с 1.3.x на 3.x.x-beta. На текущий момент третья версия плохо документирована, поэтому мы здесь и далее будем использовать первую.
webpack.config.jsТеперь перейдем к написанию кода нашего первого компонента App.jsx, — точки входа в изоморфную часть нашего веб-приложения.
src/components/App.jsx src/components/App.cssЗдесь все достаточно просто: просим пользователя ввести свое имя и здороваемся с ним.
Чтобы наше приложение отобразило этот компонент, внесем в серверный и клиентский код следующие изменения.
src/client.jsПосле инициализации JavaScript реакт найдет основной контейнер приложения react-view и "оживит" его.
src/server.jsСерверный код изменился сильнее: результатом выполнения JavaScript функции RenderDom.renderToString(<App />) будет HTML-код, который мы вставим в шаблон, формируемый функцией renderHTML. Обратите внимание на константу assetUrl: для ландшафта разработки приложение будет запрашивать ресурсы, обращаясь к серверу webpack-dev-server.
Чтобы наше приложение заработало, необходимо запустить одновременно в двух табах консоли следующие команды:
Теперь откроем ссылку в браузере: http://localhost:3001 и… наше первое приложение наконец-то готово!
Убедимся, что оно изоморфно.
1) Сначала проверим работу server-side rendering. Для этого остановим webpack-dev-server и перезагрузим страницу в браузере. Наше приложение загрузилось без стилей и ничего не происходит при вводе данных в форму, но сам интерфейс приложения отрендерился сервером, как мы и ожидали.
2) Теперь проверим работу client-side rendering. Для этого внесем изменения в файл src/server.js, убрав код, который рендерит компонент на сервере.
Еще раз обновим страницу с нашим приложением в браузере. Оно снова отрендерилось, хоть и с едва заметной задержкой. Все работает!
Примечание: если этого не случилось, убедитесь, что вы не забыли запустить npm run webpack-devserver, который был остановлен на первом шаге.
GitHubРепозиторий проекта: https://github.com/yury-dymov/habr-app https://github.com/yury-dymov/habr-app/tree/v1 — ветка v1 соответствует приложению первой статьи https://github.com/yury-dymov/habr-app/tree/v2 — ветка v2 соответствует приложению второй статьи ветка v3 соответствует приложению третьей статьи [To be done]
Что дальше?
Не слишком ли сложно для простого Hello World? Что ж, мы долго запрягали, но зато дальше быстро поедем! В следующей части мы добавим react-bootstrap, роутинг, несколько страниц, а также узнаем, что такое концепция flux и почему все так любят redux.