Лицо моей жены, когда она вычитывала эту статью
Я решил написать цикл статей, который и сам был бы счастлив найти где-то полгода назад. Он будет интересен в первую очередь тем, кто хотел бы начать разрабатывать классные приложения на React.js, но не знает, как подступиться к зоопарку разных технологий и инструментов, которые необходимо знать для полноценной front-end разработки в наши дни.
Я хочу с нуля реализовать, пожалуй, наиболее востребованный сценарий: у нас есть серверная часть, которая предоставляет REST API. Часть его методов требует, чтобы пользователь веб-приложения был авторизован.
Оглавление
1) Собираем базовый стек изоморфного приложения
2) Делаем простое приложение с роутингом и bootstrap
3) Реализуем взаимодействие с 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 для вашей платформы.
mkdir habr-app && cd habr-app
npm init
На все вопросы 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 диалектах соответственно.
npm i --save babel-core babel-plugin-transform-decorators-legacy babel-polyfill babel-preset-es2015 babel-preset-react babel-preset-stage-0
Эти зависимости надо устанавливать как зависимости проекта, так как серверной части приложения тоже нужен babel.
Теперь установим пакеты, которые нужны для сборки клиентского JavaScript. Их мы установим в качестве зависимостей разработки, так как в продакшене они не нужны.
.babelrc
При запуске babel будет обращаться к файлу .babelrc в корне проекта, в котором хранится конфигурация и список используемых preset'ов и плагинов.
Создадим этот файл
{
"presets": [
"es2015",
"react",
"stage-0"
],
"plugins": [
"transform-decorators-legacy",
"react-hot-loader/babel"
]
}
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, но явно указываете, что вам нужна только определенная функция, например
import assign from 'lodash/assign';
то в итоговую сборку войдет лишь используемая часть библиотеки, а не вся она целиком, что может существенно уменьшить размер собранного файла.
Примечание: это будет работать, только если используемая библиотека поддерживает модульность. По этой причине автор отказался от использования в своих проектах библиотек Moment.js, XRegExp и ряда других.
Для разного типа файлов нашего проекта в конфигурации webpack мы определим свой loader или цепочку loader'ов, которые будут его обрабатывать.
webpack-dev-server
Каждый раз пересобирать весь проект может быть весьма накладно: для проекта среднего размера сборка легко может достигать 30 и более секунд. Чтобы решить эту проблему, во время разработки очень удобно использовать webpack-dev-server. Это стороннее серверное приложение, которое при запуске произведет полную сборку ресурсов и при обращении к ним будет отдавать последнюю их версию из оперативной памяти. В процессе разработки при изменении отдельных файлов webpack-dev-server оно на лету будет перекомпилировать только тот файл, который изменился, и подменять старый модуль на новый в итоговой сборке. Так как пересобирать требуется не весь проект, а только один файл, то это редко занимает более секунды.
Webpack и webpack-dev-server мы установим в качестве зависимостей разработки, так как мы, разумеется, не будем заниматься сборкой на продакшене.
npm i --save-dev webpack webpack-dev-server
Хорошо, теперь нам необходимо написать файл конфигурации для сборки. Создаем файл в корне проекта
webpack.config.js
global.Promise = require('bluebird');
var webpack = require('webpack');
var path = require('path');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
var CleanWebpackPlugin = require('clean-webpack-plugin');
var publicPath = '/public/assets';
var cssName = process.env.NODE_ENV === 'production' ? 'styles-[hash].css' : 'styles.css';
var jsName = process.env.NODE_ENV === 'production' ? 'bundle-[hash].js' : 'bundle.js';
var plugins = [
new webpack.DefinePlugin({
'process.env': {
BROWSER: JSON.stringify(true),
NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development')
}
}),
new ExtractTextPlugin(cssName)
];
if (process.env.NODE_ENV === 'production') {
plugins.push(
new CleanWebpackPlugin([ 'public/assets/' ], {
root: __dirname,
verbose: true,
dry: false
})
);
plugins.push(new webpack.optimize.DedupePlugin());
plugins.push(new webpack.optimize.OccurenceOrderPlugin());
}
module.exports = {
entry: ['babel-polyfill', './src/client.js'],
debug: process.env.NODE_ENV !== 'production',
resolve: {
root: path.join(__dirname, 'src'),
modulesDirectories: ['node_modules'],
extensions: ['', '.js', '.jsx']
},
plugins,
output: {
path: `${__dirname}/public/assets/`,
filename: jsName,
publicPath
},
module: {
loaders: [
{
test: /.css$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader')
},
{
test: /.less$/,
loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!less-loader')
},
{ test: /.gif$/, loader: 'url-loader?limit=10000&mimetype=image/gif' },
{ test: /.jpg$/, loader: 'url-loader?limit=10000&mimetype=image/jpg' },
{ test: /.png$/, loader: 'url-loader?limit=10000&mimetype=image/png' },
{ test: /.svg/, loader: 'url-loader?limit=26000&mimetype=image/svg+xml' },
{ test: /.(woff|woff2|ttf|eot)/, loader: 'url-loader?limit=1' },
{ test: /.jsx?$/, loader: 'babel', exclude: [/node_modules/, /public/] },
{ test: /.json$/, loader: 'json-loader' },
]
},
devtool: process.env.NODE_ENV !== 'production' ? 'source-map' : null,
devServer: {
headers: { 'Access-Control-Allow-Origin': '*' }
}
};
Примечание: это пример конфига для продуктивного проекта, поэтому он выглядит немного сложнее, чем мог бы.
Итак,
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 означает, что когда мы пишем в коде нашего приложения
import SomeClass from './SomeClass';
webpack будет искать SomeClass в файлах SomeClass.js или SomeClass.jsx прежде, чем сообщит, что не может найти указанный файл.
7) Далее мы передаем список плагинов и указываем output — директорию, в которую webpack положит файлы после сборки.
8) Самое интересное — список loaders. Здесь мы определяем конвейеры, о которых говорилось выше. Они будут применены для файлов с соответствующими расширениями. Более подробно с форматом определения лоадеров и их параметрами лучше ознакомиться в документации, так как этот вопрос тянет на отдельную статью. Чтобы не быть уж совсем голословным, остановлюсь на конструкции лоадера.
test: /.less$/, loader: ExtractTextPlugin.extract('style-loader', 'css-loader!postcss-loader!less-loader')
Здесь мы говорим, что если встретится файл с расширением 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, все остальное нужно исключительно для сборки.
npm i --save bluebird
npm i --save-dev babel-loader clean-webpack-plugin css-loader extract-text-webpack-plugin file-loader html-loader json-loader less less-loader postcss-loader style-loader url-loader
Также настало время добавить несколько новых скриптов package.json: для запуска сборки и вебпак-дев-сервера.
"scripts": {
"build": "NODE_ENV='production' node node_modules/webpack/bin/webpack -p",
"webpack-devserver": "node node_modules/webpack-dev-server/bin/webpack-dev-server --debug --hot --devtool eval-source-map --output-pathinfo --watch --colors --inline --content-base public --port 8050 --host 0.0.0.0"
}
Создадим в корне проекта папку src, а в ней — пустой файл client.js.
Протестируем наши скрипты: введем в консоли npm run build, а в другом окне консоли — npm run webpack-devserver. Если нет ошибок — двигаемся дальше.
4. ESLint
Это необязательный пункт, но я нахожу его очень полезным. ESLint — это совокупность правил, которые предъявляются к исходному коду. Если одно или несколько из этих правил нарушаются программистом в процессе написания кода, во время сборки webpack'ом мы увидим ошибки. Таким образом весь код веб-приложения будет написан в едином стиле, включая именование переменных, отступы, запрет использования определенных конструкций и так далее.
Список правил я положу в файл .eslintrc в корне проекта. Более подробно о ESLint и правилах можно прочитать на сайте проекта.
{
"parser": "babel-eslint",
"plugins": [
"react"
],
"env": {
"browser": true,
"node": true,
"mocha": true,
"es6": true
},
"ecmaFeatures": {
"arrowFunctions": true,
"blockBindings": true,
"classes": true,
"defaultParams": true,
"destructuring": true,
"forOf": true,
"generators": false,
"modules": true,
"objectLiteralComputedProperties": true,
"objectLiteralDuplicateProperties": false,
"objectLiteralShorthandMethods": true,
"objectLiteralShorthandProperties": true,
"restParams": true,
"spread": true,
"superInFunctions": true,
"templateStrings": true,
"jsx": true
},
"rules":{
// Possible errors
"comma-dangle": [2, "never"],
"no-cond-assign": [2, "always"],
"no-constant-condition": 2,
"no-control-regex": 2,
"no-dupe-args": 2,
"no-dupe-keys": 2,
"no-duplicate-case": 2,
"no-empty-character-class": 2,
"no-empty": 2,
"no-extra-boolean-cast": 0,
"no-extra-parens": [2, "functions"],
"no-extra-semi": 2,
"no-func-assign": 2,
"no-inner-declarations": 2,
"no-invalid-regexp": 2,
"no-irregular-whitespace": 2,
"no-negated-in-lhs": 2,
"no-obj-calls": 2,
"no-regex-spaces": 2,
"no-sparse-arrays": 2,
"no-unreachable": 2,
"use-isnan": 2,
"valid-typeof": 2,
"no-unexpected-multiline": 0,
// Best Practices
"block-scoped-var": 2,
"complexity": [2, 40],
"curly": [2, "multi-line"],
"default-case": 2,
"dot-notation": [2, { "allowKeywords": true }],
"eqeqeq": 2,
"guard-for-in": 2,
"no-alert": 1,
"no-caller": 2,
"no-case-declarations": 2,
"no-div-regex": 0,
"no-else-return": 2,
"no-eq-null": 2,
"no-eval": 2,
"no-extend-native": 2,
"no-extra-bind": 2,
"no-fallthrough": 2,
"no-floating-decimal": 2,
"no-implied-eval": 2,
"no-iterator": 2,
"no-labels": 2,
"no-lone-blocks": 2,
"no-loop-func": 2,
"no-multi-str": 2,
"no-native-reassign": 2,
"no-new": 2,
"no-new-func": 2,
"no-new-wrappers": 2,
"no-octal": 2,
"no-octal-escape": 2,
"no-param-reassign": [2, { "props": true }],
"no-proto": 2,
"no-redeclare": 2,
"no-script-url": 2,
"no-self-compare": 2,
"no-sequences": 2,
"no-unused-expressions": 2,
"no-useless-call": 2,
"no-with": 2,
"radix": 2,
"wrap-iife": [2, "outside"],
"yoda": 2,
// ES2015
"arrow-parens": 0,
"arrow-spacing": [2, { "before": true, "after": true }],
"constructor-super": 2,
"no-class-assign": 2,
"no-const-assign": 2,
"no-this-before-super": 0,
"no-var": 2,
"object-shorthand": [2, "always"],
"prefer-arrow-callback": 2,
"prefer-const": 2,
"prefer-spread": 2,
"prefer-template": 2,
// Strict Mode
"strict": [2, "never"],
// Variables
"no-catch-shadow": 2,
"no-delete-var": 2,
"no-label-var": 2,
"no-shadow-restricted-names": 2,
"no-shadow": 2,
"no-undef-init": 2,
"no-undef": 2,
"no-unused-vars": 2,
// Node.js
"callback-return": 2,
"no-mixed-requires": 2,
"no-path-concat": 2,
"no-sync": 2,
"handle-callback-err": 1,
"no-new-require": 2,
// Stylistic
"array-bracket-spacing": [2, "never", {
"singleValue": true,
"objectsInArrays": true,
"arraysInArrays": true
}],
"newline-after-var": [1, "always"],
"brace-style": [2, "1tbs"],
"camelcase": [2, { "properties": "always" }],
"comma-spacing": [2, { "before": false, "after": true }],
"comma-style": [2, "last"],
"computed-property-spacing": [2, "never"],
"eol-last": 2,
"func-names": 1,
"func-style": [2, "declaration"],
"indent": [2, 2, { "SwitchCase": 1 }],
"jsx-quotes": [2, "prefer-single"],
"linebreak-style": [2, "unix"],
"max-len": [2, 128, 4, {
"ignoreUrls": true,
"ignoreComments": false,
"ignorePattern": "^\s*(const|let|var)\s+\w+\s+\=\s+\/.*\/(|i|g|m|ig|im|gm|igm);?$"
}],
"max-nested-callbacks": [2, 4],
"new-parens": 2,
"no-array-constructor": 2,
"no-lonely-if": 2,
"no-mixed-spaces-and-tabs": 2,
"no-multiple-empty-lines": [2, { "max": 2, "maxEOF": 1 }],
"no-nested-ternary": 2,
"no-new-object": 2,
"no-spaced-func": 2,
"no-trailing-spaces": 2,
"no-unneeded-ternary": 2,
"object-curly-spacing": [2, "always"],
"one-var": [2, "never"],
"padded-blocks": [2, "never"],
"quotes": [1, "single", "avoid-escape"],
"semi-spacing": [2, { "before": false, "after": true }],
"semi": [2, "always"],
"keyword-spacing": 2,
"space-before-blocks": 2,
"space-before-function-paren": [2, { "anonymous": "always", "named": "never" }],
"space-in-parens": [2, "never"],
"space-infix-ops": 2,
"space-unary-ops": [2, { "words": true, "nonwords": false }],
"spaced-comment": [2, "always", {
"exceptions": ["-", "+"],
"markers": ["=", "!"]
}],
// React
"react/jsx-boolean-value": 2,
"react/jsx-closing-bracket-location": 2,
"react/jsx-curly-spacing": [2, "never"],
"react/jsx-handler-names": 2,
"react/jsx-indent-props": [2, 2],
"react/jsx-indent": [2, 2],
"react/jsx-key": 2,
"react/jsx-max-props-per-line": [2, {maximum: 3}],
"react/jsx-no-bind": [2, {
"ignoreRefs": true,
"allowBind": true,
"allowArrowFunctions": true
}],
"react/jsx-no-duplicate-props": 2,
"react/jsx-no-undef": 2,
"react/jsx-pascal-case": 2,
"react/jsx-uses-react": 2,
"react/jsx-uses-vars": 2,
"react/no-danger": 2,
"react/no-deprecated": 2,
"react/no-did-mount-set-state": 0,
"react/no-did-update-set-state": 0,
"react/no-direct-mutation-state": 2,
"react/no-is-mounted": 2,
"react/no-multi-comp": 2,
"react/no-string-refs": 2,
"react/no-unknown-property": 2,
"react/prefer-es6-class": 2,
"react/prop-types": 2,
"react/react-in-jsx-scope": 2,
"react/self-closing-comp": 2,
"react/sort-comp": [2, {
"order": [
"lifecycle",
"/^handle.+$/",
"/^(get|set)(?!(InitialState$|DefaultProps$|ChildContext$)).+$/",
"everything-else",
"/^render.+$/",
"render"
]
}],
"react/jsx-wrap-multilines": 2,
// Legacy
"max-depth": [0, 4],
"max-params": [2, 4],
"no-bitwise": 2
},
"globals":{
"$": true,
"ga": true
}
}
npm i --save-dev babel-eslint eslint eslint-loader eslint-plugin-react
Мы добавим eslint-loader в конфигурацию webpack, таким образом перед тем, как babel транслирует наш код в ES5, весь наш код будет проверен на соответствие заданным правилам.
webpack.config.js
В module.exports.module.loaders:
--- { test: /.jsx?$/, loader: 'babel', exclude: [/node_modules/, /public/] },
+++ { test: /.jsx?$/, loader: 'babel!eslint-loader', exclude: [/node_modules/, /public/] },
В module.exports:
+++ eslint: { configFile: '.eslintrc' },
5. Express и Server.js
К текущему моменту мы настроили сборку проекта, трансляцию кода в JS ES5, описали и реализовали проверку исходного кода "на вшивость". Пришло время перейти к написанию самого приложения, а точнее — его серверной части.
Примечание: я использую Express, и меня он полностью устраивает, но, разумеется, есть множество других аналогичных пакетов (это же Node.js).
Создаем в корне файл server.js со следующим содержимым
server.js
require('babel-core/register');
['.css', '.less', '.sass', '.ttf', '.woff', '.woff2'].forEach((ext) => require.extensions[ext] = () => {});
require('babel-polyfill');
require('server.js');
Здесь мы указываем, что нам нужен babel для поддержки ES6/ES7, а также что если node встретит конструкции вида
import 'awesome.css';
то эту строчку нужно просто проигнорировать, так как это не JavaScript или один из его диалектов.
Сам код серверной части будет в файле src/server.js, в котором мы теперь можем свободно использовать ES6/ES7 синтаксис.
src/server.js
import express from 'express';
const app = express();
app.use((req, res) => {
res.end('<p>Hello World!</p>');
});
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server listening on: ${PORT}`);
});
Здесь все достаточно просто: мы импортируем веб-сервер express, запускаем его на порту, который был передан в переменной окружения PORT или 3001. Сам сервер на любой запрос будет отдавать ответ: "Hello World"
Мы установим пакет nodemon в зависимости разработки, чтобы запустить серверный JavaScript код. Он удобен тем, что выводит любые ошибки с подробными Stack Traces сразу в консоль по мере их возникновения.
npm i --save-dev nodemon
Добавим еще один скрипт в package.json
+++ "nodemon": "NODE_PATH=./src nodemon server.js",
И запустим в консоли
npm run nodemon
Откроем браузер и попробуем открыть страницу http://localhost:3001. Если все хорошо, то мы увидим Hello World.
6. React и ReactDOM
Поздравляю! Почти все приготовления завершены, и мы можем наконец-то переходить к реакту.
Установим соответствующие библиотеки:
npm i --save react react-dom
Также установим react-hot-loader: при изменении исходного кода компонентов в процессе разработки, браузер будет перезагружать страницу автоматически. Это очень удобная фишка, особенно если у вас несколько мониторов.
npm i --save-dev react-hot-loader
Внесем изменения в webpack.config.js, чтобы эта фича заработала:
webpack.config.js
В module.exports.module.loaders изменим:
--- { test: /.jsx?$/, loader: 'babel!eslint-loader', exclude: [/node_modules/, /public/] },
+++ { test: /.jsx?$/, loader: 'react-hot!babel!eslint-loader', exclude: [/node_modules/, /public/] },
Теперь перейдем к написанию кода нашего первого компонента App.jsx, — точки входа в изоморфную часть нашего веб-приложения.
src/components/App.jsx
import React, { PropTypes, Component } from 'react';
import './App.css';
export default class extends Component {
static propTypes = {
initialName: PropTypes.string
}
static defaultProps = {
initialName: 'Аноним'
};
state = {
name: this.props.initialName,
touched: false,
greetingWidget: () => null
};
handleNameChange = (val) => {
const name = val.target.value;
this.setState({ touched: true });
if (name.length === 0) {
this.setState({ name: this.props.initialName });
} else {
this.setState({ name });
}
};
renderGreetingWidget = () => (!this.state.touched ? null :
<div>
<hr />
<p>Здравствуйте, {this.state.name}!</p>
</div>
);
render() {
return (
<div className='App'>
<h1>Hello World!</h1>
<div>
<p>Введите Ваше имя:</p>
<div><input onChange={this.handleNameChange} /></div>
{this.renderGreetingWidget()}
</div>
</div>
);
}
}
src/components/App.css
.App {
padding: 20px;
}
.App h1 {
font-size: 26px;
}
.App input {
padding: 10px;
}
.App hr {
margin-top: 20px;
}
Здесь все достаточно просто: просим пользователя ввести свое имя и здороваемся с ним.
Чтобы наше приложение отобразило этот компонент, внесем в серверный и клиентский код следующие изменения.
src/client.js
import React from 'react';
import ReactDOM from 'react-dom';
import App from 'components/App';
ReactDOM.render(<App />, document.getElementById('react-view'));
После инициализации JavaScript реакт найдет основной контейнер приложения react-view и "оживит" его.
src/server.js
import express from 'express';
import React from 'react';
import ReactDom from 'react-dom/server';
import App from 'components/App';
const app = express();
app.use((req, res) => {
const componentHTML = ReactDom.renderToString(<App />);
return res.end(renderHTML(componentHTML));
});
const assetUrl = process.env.NODE_ENV !== 'production' ? 'http://localhost:8050' : '/';
function renderHTML(componentHTML) {
return `
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Hello React</title>
<link rel="stylesheet" href="${assetUrl}/public/assets/styles.css">
</head>
<body>
<div id="react-view">${componentHTML}</div>
<script type="application/javascript" src="${assetUrl}/public/assets/bundle.js"></script>
</body>
</html>
`;
}
const PORT = process.env.PORT || 3001;
app.listen(PORT, () => {
console.log(`Server listening on: ${PORT}`);
});
Серверный код изменился сильнее: результатом выполнения JavaScript функции RenderDom.renderToString(<App />) будет HTML-код, который мы вставим в шаблон, формируемый функцией renderHTML. Обратите внимание на константу assetUrl: для ландшафта разработки приложение будет запрашивать ресурсы, обращаясь к серверу webpack-dev-server.
Чтобы наше приложение заработало, необходимо запустить одновременно в двух табах консоли следующие команды:
npm run nodemon
npm run webpack-devserver
Теперь откроем ссылку в браузере: http://localhost:3001 и… наше первое приложение наконец-то готово!
Убедимся, что оно изоморфно.
1) Сначала проверим работу server-side rendering. Для этого остановим webpack-dev-server и перезагрузим страницу в браузере. Наше приложение загрузилось без стилей и ничего не происходит при вводе данных в форму, но сам интерфейс приложения отрендерился сервером, как мы и ожидали.
2) Теперь проверим работу client-side rendering. Для этого внесем изменения в файл src/server.js, убрав код, который рендерит компонент на сервере.
--- <div id="react-view">${componentHTML}</div>
+++ <div id="react-view"></div>
Еще раз обновим страницу с нашим приложением в браузере. Оно снова отрендерилось, хоть и с едва заметной задержкой. Все работает!
Примечание: если этого не случилось, убедитесь, что вы не забыли запустить npm run webpack-devserver, который был остановлен на первом шаге.
GitHub
Репозиторий проекта: https://github.com/yury-dymov/habr-app
https://github.com/yury-dymov/habr-app/tree/v1 — ветка v1 соответствует приложению первой статьи
ветка v2 соответствует приложению второй статьи [To be done]
ветка v3 соответствует приложению третьей статьи [To be done]
Что дальше?
Не слишком ли сложно для простого Hello World? Что ж, мы долго запрягали, но зато дальше быстро поедем!
В следующей части мы добавим react-bootstrap, роутинг, несколько страниц, а также узнаем, что такое концепция flux и почему все так любят redux.
7. Полезные ресурсы и материалы
- Основы JavaScript ES2015 — https://learn.javascript.ru/es-modern
- Документация webpack — http://webpack.github.io/docs/
- Шикарный скринкаст по изучению webpack на русском языке — https://learn.javascript.ru/screencast/webpack
- Документация Babel — https://babeljs.io/
- Документация ESLint — http://eslint.org/
- Документация Express — https://expressjs.com/
- Документация Express на русском — http://jsman.ru/express/
- Документация React — https://facebook.github.io/react/
P.s. Если в тексте присутствуют ошибки или неточности, пожалуйста, напишите мне сначала в личные сообщения. Заранее спасибо!
Автор: yury-dymov