image

Лицо моей жены, когда она вычитывала эту статью


Я решил написать цикл статей, который и сам был бы счастлив найти где-то полгода назад. Он будет интересен в первую очередь тем, кто хотел бы начать разрабатывать классные приложения на React.js, но не знает, как подступиться к зоопарку разных технологий и инструментов, которые необходимо знать для полноценной front-end разработки в наши дни.


Я хочу с нуля реализовать, пожалуй, наиболее востребованный сценарий: у нас есть серверная часть, которая предоставляет REST API. Часть его методов требует, чтобы пользователь веб-приложения был авторизован.


Оглавление


1) Собираем базовый стек изоморфного приложения
2) Делаем простое приложение с роутингом и bootstrap
3) Реализуем взаимодействие с API и авторизацию


За скобками останутся такие вопросы как интернационализация, написание тестов, деплой и варианты работы с CSS, так как эти вопросы гораздо более вариативны и каждый из них тянет на отдельный блок статей. Возможно, я вернусь к ним позже, если будет спрос.


Примечание: на github есть множество замечательных boilerplate или уже собранных стеков, которые можно использовать в качестве фундамента для своего приложения, но мне кажется, гораздо правильнее собирать свой стек самостоятельно, понимая, что делает каждый пакет и каждая строка вашего кода. Научившись делать это один раз, в следующий раз сбор стека займет едва ли более 5 минут.


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


Итак, поехали!


1. Мы будем разрабатывать изоморфное веб-приложение.


Изоморфное или универсальное приложение означает, что JavaScript код приложения может быть выполнен как на сервере, так и на клиенте. Этот механизм является одной из сильных сторон React и позволяет пользователю получить доступ к контенту существенно быстрее. Ниже я буду использовать термин "изоморфное", так как он пока еще встречается чаще, но важно понимать, что "изоморфное" и "универсальное" — это одно и то же.


Подробнее о изоморфных приложениях
image

С точки зрения пользователя взаимодействие с веб-приложением выглядит следующим образом


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% исходного кода всего приложения — в этом и заключается вся суть изоморфного / универсального подхода. В процессе реализации мы увидим, как это работает на практике.


Создаем новый проект


Установка Node.js и менеджера пакетов npm

На первый взгляд версионность ноды может показаться немного странной. Чтобы не запутаться, достаточно знать, что 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"
  ]
}

3. Сборка


Мы установили Babel с нужными нам плагинами, но кто и когда должен его запускать? Настало время перейти к этому вопросу.


Примечание: проекты веб-приложений в наши дни с виду мало отличаются от проектов десктопных или мобильных приложений: они будут содержать внешние библиотеки, файлы, скорее всего соответствующие парадигме MVC, ресурсы, файлы стилей и многое другое. Такое представление будет очень удобно для программиста, но не для пользователя. Если взять весь исходный код JavaScript проекта, а также используемых библиотек, выкинуть все лишнее, объединить в один большой файл и применить минификацию, то полученный на выходе один файл может занимать в 10 и более раз меньше, чем изначальный набор. Также потребуется всего лишь один, а не сотни, запрос браузера, чтобы скачать всю логику нашего приложения. И то, и другое очень важно для производительности. К слову, та же логика применима и для CSS-ресурсов, включая разные диалекты (LESS, SASS и пр.).


Эту полезную работу будет выполнять webpack.


Примечание: для этой же цели можно применять сборщики: grunt, gulp, bower, browserify и другие, но исторически для React чаще всего используется именно webpack.


Подробнее о webpack и webpack-dev-server

webpack


image

Алгоритм работы 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"
  }

Update: пользователи wrewolf и Nerop в личке и комментариях соответственно сообщили, что в Windows скрипты должны выглядеть иначе.


  "scripts": {
    "build": "set 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 и правилах можно прочитать на сайте проекта.


.eslint.rc
{
    "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
    }
}

Примечание: в Windows правило


"linebreak-style": [2, "unix"],

надо заменить на


"linebreak-style": [2, "windows"],

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).


Устанавливаем express


npm i --save express

Создаем в корне файл 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",

Для Windows:


+++ "nodemon": "set NODE_PATH=./src; && node node_modules/nodemon/bin/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

Начиная с 3 версии, react-hot-loader рекомендовано прописывать в .babelrc в качестве одного из плагинов. Ранее же он передавался в конфигурацию webpack в качестве одного из конвейеров для JavaScript-файлов.


.babelrc


---   "transform-decorators-legacy"
+++ "transform-decorators-legacy",
+++ "react-hot-loader/babel"

Теперь перейдем к написанию кода нашего первого компонента App.jsx, — точки входа в изоморфную часть нашего веб-приложения.


src/components/App.jsx


import React, { PropTypes, Component } from 'react';

import './App.css';

const propTypes = {
  initialName: PropTypes.string
};

const defaultProps = {
  initialName: 'Аноним'
};

class App extends Component {
  constructor(props) {
    super(props);

    this.handleNameChange = this.handleNameChange.bind(this);
    this.renderGreetingWidget = this.renderGreetingWidget.bind(this);

    this.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() {
    if (!this.state.touched) {
      return null;
    }

    return (
      <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>
    );
  }
}

App.propTypes = propTypes;
App.defaultProps = defaultProps;

export default App;

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. Полезные ресурсы и материалы


  1. Основы JavaScript ES2015 — https://learn.javascript.ru/es-modern
  2. Документация webpack — http://webpack.github.io/docs/
  3. Шикарный скринкаст по изучению webpack на русском языке — https://learn.javascript.ru/screencast/webpack
  4. Документация Babel — https://babeljs.io/
  5. Документация ESLint — http://eslint.org/
  6. Документация Express — https://expressjs.com/
  7. Документация Express на русском — http://jsman.ru/express/
  8. Документация React — https://facebook.github.io/react/

P.s. Если в тексте присутствуют ошибки или неточности, пожалуйста, напишите мне сначала в личные сообщения. Заранее спасибо!

Поделиться с друзьями
-->

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


  1. inoyakaigor
    14.09.2016 12:29

    Линтер можно добавить как плагин к тому же Саблайму, например. Это ускорит процесс разработки в том плане, что не придётся исправлять то, на что будет ругаться eslint после сохранения всех изменений. Хотя, с другой стороны, если самому определять правила в .eslint.rc, то он, линтер то бишь, может и не понадобиться.


    1. yury-dymov
      14.09.2016 12:39

      Верно. Различные lint плагины есть к Atom, WebStorm, TextMate. Для других популярных IDE и редакторов тоже должны быть плагины, но у меня пока есть опыт работы с этой тройкой.

      Каждый плагин умеет что-то свое, но еще нет такого, который бы исправлял прямо все проблемы кода разом, да это и невозможно. Насколько мне известно, команда eslint сейчас работает над инструментами, которые бы частично решали эту проблему.


  1. fr_ant
    14.09.2016 12:39

    Было бы неплохо для примера добавить css-modules


    1. yury-dymov
      14.09.2016 12:50
      +2

      Во второй части будет немного CSS, но в упрощенном виде. Дело в том, что в React-сообществе до сих пор нет единого «правильного» подхода к стилям.

      Один из core-разработчик, работая над react-native решил использовать inline-стили, чтобы не возиться с написанием css-парсера на тот момент. Неожиданно этот «анти-паттерн» оказался достаточно популярным среди сообщества и сейчас эта идея получила широкое развитие.

      Мне нравится идея, что стили компонента лежат рядом с ним, а не определяются глобально. Другое дело, что inline-стили не кешируются и работают медленнее, чем классы.

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

      Так как статья рассчитана в основном на новичков, то мне показалось, что лучше на этом этапе тему css опустить и написать потом отдельную статью, если будет много интереса к этой теме.


  1. Nerop
    14.09.2016 14:11

    Для тех, у кого Windows:


    1. Nerop
      14.09.2016 14:17
      +1

      Строки build и nodemon в package.json должны выглядеть так:
      «set NODE_ENV='production' node node_modules/webpack/bin/webpack -p»
      «set NODE_PATH=./src node node_modules/nodemon/bin/nodemon server.js»


      А в файле .eslintrc замените строку «linebreak-style»: [2, «windows»]
      P.S. Извиняюсь за двойной комментарий, мой косяк.-


      1. yury-dymov
        14.09.2016 14:19

        Спасибо за дополнение


      1. wrewolf
        14.09.2016 14:32
        +1

        все же
        set NODE_ENV='production' && node node_modules/webpack/bin/webpack -p

        А второй кроссплатформенно заменяется на

        node node_modules/nodemon/bin/nodemon server.js ./src
        


        Ну и от себя, webpack и nodemon будут нужны не в одном проекте и имеет смысл их ставить глобально.
        Тогда еще проще
        set NODE_ENV='production' && webpack -p
        nodemon server.js ./src
        


        1. justboris
          14.09.2016 17:28
          +1

          В а чем преимущество глобальной установки, кроме экономии на символах?
          Во-первых, на linux для глобальной установки нужно sudo
          Во-вторых, не всегда все проекты используют одну и ту же версию библиотеки. Особенно это актуально при скором релизе webpack 2.


          1. wrewolf
            14.09.2016 18:10

            Для обучения оно ставится 1 раз и не нужно ждать пока его поставит в очередной локальный проект, скорее экономия времени.


            1. justboris
              14.09.2016 18:19
              +2

              Кажется экономия на паре пакетов в npm install все равно не стоит того, что придется потратить на разборки, почему ничего не собирается, если пытаться запустить webpack 1 на проекте в webpack 2. Явная декларация версии webpack и всех остальных иструментов тоже экономит время.


              Но это мое мнение из собственного опыта.


              1. yury-dymov
                14.09.2016 18:26

                Я с Вами согласен, поэтому в статье писал ровно такие скрипты, которые использую сам в проектах, размещаю в npm и деплою продакшен, в которых используются только локальные пакеты. Потому что это единственный по-настоящему правильный способ.


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


                А остальное — это наживное и с опытом придет.


              1. wrewolf
                14.09.2016 18:34

                Веский довод.


          1. springimport
            14.09.2016 21:52

            Я лично привык сразу все ставить глобально (на локальной машине), потом просто используешь инструменты даже не задумываясь об установке.


      1. AlexWade
        14.09.2016 15:46
        +1

        Возможно кому то поможет. У меня nodemon, смог правильно подключить server.js только после того как в конце NODE_PATH была добавлена точка с запятой, то есть получилось так:
        set NODE_PATH=./src; && nodemon server.js


        1. yury-dymov
          14.09.2016 15:47

          Спасибо, сейчас обновлю статью


  1. asvechkar
    14.09.2016 15:45
    +1

    А добавьте, пожалуйста, к Hello World авторизацию с JWT, компонент авторизации в React.js и проверку на валидность токена. А то TODO-tutorials много, а толковых примеров нет.


    1. yury-dymov
      14.09.2016 15:55
      +1

      Я бы рад, но в моем основном проекте я начал использовать devise_token_auth на бэкенде, соответственно на стороне front-end — redux-auth. Спустя некоторое время я его переписал и опубликовал в виде redux-oauth, так как redux-auth не поддерживал ряд важных мне фич (например, API requests для server-side rendering), плюс он непомерно тяжелый для того, что умеет. Я планировал в своем цикле осветить именно этот стек, так как он работает у меня в продакшене примерно полгода без видимых проблем, и я им более, чем доволен.


      Возможно в будущих проектах я перейду на JWT, так как будет другой бэкенд и тогда опубликую условный redux-jwt, если не найду ничего готового, но пока у меня такого опыта, которым я мог бы авторитетно поделиться.


  1. G-M-A-X
    14.09.2016 23:01

    Для каких сайтов / бюджетов оправдано городит огород из React.js?
    Много разработчиков его понимают или только избранные? :)

    Этот механизм позволяет пользователю получить доступ к контенту существенно быстрее.

    Каким образом?

    Самое главное, что пользователь имеет доступ к контенту почти сразу, а не спустя две и более секунды, как это бывает в случае традиционных client-sideJavaScript приложений.

    Что входит в эти 2 секунды?
    Это время генерации страницы?

    Выигрыш получается за счет того, что не надо дожидаться скачивания клиентского JavaScript, а это 200кб и более с учетом минификации и сжатия.

    Почему же не нужно?

    изоморфный подход делает ваше приложение гораздо приятнее для пользователя.

    В чем приятность, если отвалились скрипты?

    Если ваше клиентское JavaScript приложение перестало работать из-за ошибки, то ваш сайт скорее всего станет бесполезным для пользователя. В изоморфном же случае есть хороший шанс, что пользователь все же сможет сделать то, что он хочет.

    Почему?


    1. yury-dymov
      15.09.2016 05:53

      1. То, что описано в статье разработчик с опытом собирает в течение 5-10 минут. Написание самого приложения по скорости мало отличается от других технологий. Facebook, AirBnB, Twitter, порталы Yahoo, девпортал Apple и масса других сайтов — это все сейчас на реакте
      2. В 2 секунды входит загрузка JS и инициализация, причем если временем загрузки можно пренебречь, то вот с инициализацией вы ничего сделать не сможете. Если у вас client-side rendering, то вот эти 2+ секунды JS будет запускаться и формировать страницу, а пользователь — любоваться на заставку "Loading".
      3. Не нужно ждать, потому что сервер Node.js отдает HTML с контентом. Да, в течение этих 2 секунд кнопочки работать не будут, но скорее всего пользователь и не успеет ничего сделать
      4. Если отвалились скрипты, то client-side приложение даже ничего не покажет, изоморфное же в теории работать дальше, как nojs приложения

      Я Вас понимаю, я много лет относился к node и серверному JavaScript с огромным скепсисом и до сих пор считаю, что писать backend на ноде не надо вот совсем, но после того, как сам попробовал реакт на одном из своих проектов и генерация view слоя упала с 20 секунд до 0.5 при переходе с rails на rails-api + node.js, выяснилось, что за последние несколько лет очень многое изменилось в индустрии и что производительность JavaScript может доходить до 30% от C, а не 1%, как когда-то было.


      Если Вы найдете время и желание и погрузитесь в это, все Ваши вопросы отпадут сами собой достаточно быстро.


      1. WarAngel_alk
        15.09.2016 08:29

        Не нужно ждать, потому что сервер Node.js отдает HTML с контентом. Да, в течение этих 2 секунд кнопочки работать не будут, но скорее всего пользователь и не успеет ничего сделать

        А как обрабатывается случай, когда всё таки успеет?

        И насколько сложнее сделать изоморфное приложение с бэкэндом не на Node? То есть понятно, что теоретически можно выполнить JS-код на чём угодно, но есть ли для этого готовые инструменты хотя бы в мейнстримных Java, C#, Python, etc.?


        1. yury-dymov
          15.09.2016 08:51
          +1

          1. It depends. Если у Вас кнопка, которая чистый JavaScript (ну там выбор цвета аватарки в виджете, который генерит аватарки), то упс. Если это ссылка или кнопка формы, то вместо того, чтобы логика обрабатывалась клиентским JS, который вероятно сделает что-нибудь полезное (отрендерит часть страницы и покажет loading indicator, например, или сделает валидацию), будет выполнено "классическое вебовское действие", то есть запрос уйдет на сервер и форма будет отправлена POST'ом соответственно. Важно понимать, что эти fallbacks не совсем бесплатны с точки зрения работы программиста и нужно их реализовывать там, где надо, а не везде. Но это здорово, что есть такая возможность, и в теории можно сделать даже так, что сайт вполне себе работал даже если у клиента JS отключен совсем.


          2. Я очень часто сталкиваюсь с таким вопросом, когда рассказываю знакомым про изоморфные приложения. В примере и в следующих частях я пишу как раз такое приложение: фронт изоморфный, а бек — на rails-api.

          То есть у Вашего веб-приложения будет 2+ серверной части: backend, который предоставляет REST API и node.js часть, которая рендерит HTML.


          Грубо говоря, раньше у Вас было rails или там php, C#, Python, whatever приложение, которое а) обрабатывало запросы б) рендерило HTML, который отдавало клиенту.


          Теперь у Вас разделились сущности и это, на самом деле, очень здорово: а) приложение на whatever технологии обрабатывает запросы и отдает JSON ответ и б) приложение на node.js, которое обрабатывает запросы, делает запросы к API при необходимости и отдает HTML клиенту. И вот б) в этой схеме нам нужен, чтобы сайт работал быстрее с точки зрения пользователя, отдавал поисковикам контент без необходимости выполнять JS и "сглаживал" ошибки клиентского JS. Тут важно отметить, что разницы между клиентским и изоморфном подходом с точки зрения кода очень мало: код клиентской и серверной части совпадает на 95%+, то есть реализация этой фичи обходится очень дешево, принимая во внимание, сколько пользы она приносит


          Тут еще добавлю, что разделять front и back очевидно хорошо, потому что


          • тот же API может быть использован мобильными приложениями
          • удобно разделить разработку на две команды
          • проще тестировать


      1. G-M-A-X
        15.09.2016 23:58

        Не совсем понял соответствие пунктов ответов вопросам… :)

        1. То-то я вижу, они тупят :)
        2. Хм, то есть по факту не на 2с быстрее, а скорее всего на 2с медленнее :) В классике js инициализируется моментально после загрузки.
        3. А PHP не отдает что ли? :) Ни чем не лучше.
        4. Разницы нету…

        5. Я не то что отношусь со скепсисом.
        NodeJS на самом деле очень быстрый, но не так просто переносить на асинхронную модель синхронный код.
        Может есть наработки это упрощающие, хз.
        6. Что это за приложение с генерацией 20 с? Это на сервере столько шаблон отрабатывал? Да, Руби тот еще скороход :)
        7. Зачем мне погружаться, если я слышу только голословные утверждения?


  1. bobrov1989
    15.09.2016 05:38
    +1

    очень интересная статья — жду продолжения с нетерпением


    1. yury-dymov
      15.09.2016 05:55

      Спасибо, рад что понравилось. Я планирую опубликовать в середине следующей недели вторую часть и еще одной неделей позже — третью


  1. paratagas
    15.09.2016 19:31
    +1

    У меня в разделе 5 после команды

    npm run nodemon
    выдало ошибку, что Express не найден. Добавьте, пожалуйста, команду
    npm install express
    чтобы обозначить установку фрэймворка


    1. yury-dymov
      15.09.2016 19:31

      Спасибо, удивительно, что просмотрел


  1. BoryaMogila
    16.09.2016 10:11

    Ремарка по поводу сервера, его тоже надо собирати, иначе при первом запросе будет просидание по скорости. я так понимаю из-за babel-loader.
    Вот пример зборки на сервере

    var webpack = require('webpack');
    var path = require('path');
    var fs = require('fs');
    
    var nodeModules = {};
    fs.readdirSync('node_modules')
        .filter(function(x) {
            return ['.bin'].indexOf(x) === -1;
        })
        .forEach(function(mod) {
            nodeModules[mod] = 'commonjs ' + mod;
        });
    
    var babelPlugins = ['transform-runtime'];
    
    module.exports = {
        entry: './index.js',
        target: 'node',
        output: {
            path: path.join(__dirname, 'build'),
            filename: 'backend.js'
        },
        module      : {
            loaders: [
                {
                    loader : 'babel',
                    exclude: /node_modules/,
                    query: {
                        plugins: babelPlugins,
                        presets: ["stage-0", "react","es2015-node5"],
                    }
                },
                {
                    test: /\.css$/, loader: "style-loader!css-loader"
                },
            ]
        },
        externals: nodeModules,
        plugins: [
            new webpack.IgnorePlugin(/\.(css|less)$/),
            new webpack.BannerPlugin('require("source-map-support").install();',
                { raw: true, entryOnly: false })
        ],
        devtool: 'sourcemap'
    }
    

    не забудте забрать полифил и babel-register с серверного кода


    1. yury-dymov
      16.09.2016 10:15

      Спасибо за дополнение. Я не включил скрипт для сборки серверной части, так как не планировал рассматривать процесс деплоя в этом цикле, тем не менее, считаю, что Ваш комментарий может оказаться читателям весьма полезным


      1. BoryaMogila
        16.09.2016 10:30

        спасибо


  1. BamaBoy
    16.09.2016 16:07
    +1

    Отличная статья! Хочу задать наверное глупый вопрос. Почему в классе App handleNameChange не обычный метод, а class property?


    1. yury-dymov
      16.09.2016 16:10

      Более, чем уместный вопрос. Потому что автор, видимо, на что-то отвлекся в процессе реализации :)
      В течение часа-двух обновлю статью и гитхаб. Так конечно тоже можно писать, но это далеко не лучшая практика, мягко говоря.


      Спасибо


  1. Imbolc
    16.09.2016 16:46

    Если сюда добавить необходимость делать клиентские API запросы после инициализации и вспомнить, что на мобильном интернете часто можно столкнуться с весьма ощутимыми задержками

    Это вы скорее описываете преимущества серверного рендеринга, а не изоморфного. Ведь во втором случае это будет происходить только при загрузке первой страницы. Если js успешно подгрузится, конечно. А дальше всё те же запросы к API.


    1. yury-dymov
      16.09.2016 16:48

      Ну, я скорее сравниваю изоморфный подход с client-only.


      1. Imbolc
        16.09.2016 16:49

        Я бы добавил ещё поисковую оптимизацию в плюсы. Гугл индексирует js долго и не всегда правильно, яндекс — не индексирует совсем.


  1. Imbolc
    16.09.2016 17:12

    Правильно ли я понимаю, что babel-polyfill просто ищет упоминания всяких Promise в коде и добавляет полифилы в бандл? То есть даже если браузер современный ему всё равно всё это придётся скачать? Или возможно создавать разные бандлы под современные и старые браузеры?


    1. yury-dymov
      17.09.2016 05:19
      +1

      https://babeljs.io/docs/usage/polyfill/


      В принципе да, примерно это он и делает. Я не экспериментировал, поэтому пока не имею такого опыта, но технически не вижу проблем сделать 2 сборки: с полифиллом и без. Соответственно express по User-Agent понимает, кто к нему пришел и отдает HTML шаблон с соответствующим бандлом.


  1. Imbolc
    16.09.2016 17:15

    А почему вы решили описывать настройки в .babelrc, а не в конфиге вебпака?


    1. yury-dymov
      17.09.2016 05:21
      +1

      reuse же. Babel запускается и для node, и для webpack, и для тестов, и для много чего еще. Все они делят общий .babelrc конфиг