14 сентября 2016 в 10:45

React.js: собираем с нуля изоморфное / универсальное приложение. Часть 1: собираем стек из песочницы tutorial

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@1.13.2 webpack-dev-server

Примечание: на момент написания и публикации статьи актуальна была версия webpack 1. С 22 сентября 2016 года по умолчанию устанавливается webpack 2 beta.


Хорошо, теперь нам необходимо написать файл конфигурации для сборки. Создаем файл в корне проекта


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         = 'http://localhost:8050/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' webpack -p",
    "webpack-devserver": "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' && webpack -p",
    "webpack-devserver": "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 и правилах можно прочитать на сайте проекта.


.eslintrc
{
    "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; && 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@1.3.0

Примечание: за время пребывания статьи в песочнице в npm изменилась версия пакета react-hot-loader с 1.3.x на 3.x.x-beta. На текущий момент третья версия плохо документирована, поэтому мы здесь и далее будем использовать первую.


webpack.config.js


---     { test: /\.jsx?$/, loader: 'babel!eslint-loader', exclude: [/node_modules/, /public/] }, 
+++  { test: /\.jsx?$/, loader: process.env.NODE_ENV !== 'production' ? 'react-hot!babel!eslint-loader' : 'babel', exclude: [/node_modules/, /public/] },

Теперь перейдем к написанию кода нашего первого компонента 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 соответствует приложению первой статьи
https://github.com/yury-dymov/habr-app/tree/v2 — ветка v2 соответствует приложению второй статьи
ветка 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. Если в тексте присутствуют ошибки или неточности, пожалуйста, напишите мне сначала в личные сообщения. Заранее спасибо!

Юрий Дымов @yury-dymov
карма
33,0
рейтинг 0,0
Full Stack And Mobile Senior Developer
Похожие публикации
Самое читаемое Разработка

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

  • 0
    Линтер можно добавить как плагин к тому же Саблайму, например. Это ускорит процесс разработки в том плане, что не придётся исправлять то, на что будет ругаться eslint после сохранения всех изменений. Хотя, с другой стороны, если самому определять правила в .eslint.rc, то он, линтер то бишь, может и не понадобиться.
    • 0
      Верно. Различные lint плагины есть к Atom, WebStorm, TextMate. Для других популярных IDE и редакторов тоже должны быть плагины, но у меня пока есть опыт работы с этой тройкой.

      Каждый плагин умеет что-то свое, но еще нет такого, который бы исправлял прямо все проблемы кода разом, да это и невозможно. Насколько мне известно, команда eslint сейчас работает над инструментами, которые бы частично решали эту проблему.
  • 0
    Было бы неплохо для примера добавить css-modules
    • +2
      Во второй части будет немного CSS, но в упрощенном виде. Дело в том, что в React-сообществе до сих пор нет единого «правильного» подхода к стилям.

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

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

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

      Так как статья рассчитана в основном на новичков, то мне показалось, что лучше на этом этапе тему css опустить и написать потом отдельную статью, если будет много интереса к этой теме.
      • 0
        А вам удалось подружить jss с ExtractTextPlugin? Это вообще возможно?
        • 0

          Да, и оно как-то очень естественно вышло. Может мне повезло и я случайно обошел грабли :)

          • 0
            Можно кусок webpack-конфига на эту тему, пожалуйста? Очень лень самому всё раскапывать :)
            • 0

              Конфиг из статьи. Я до сих пор суть проблемы не понимаю. То, что вы определяете в jss-стилях, не экстрактится by design. То есть у вас:
              1) один большой css, который сформирован на базе классических импортов
              2) jss все свои стили при Server Side Rendering формирует и вставляет в HTML, если не используете SSR, то просто по одному блоку style на каждый тип компонента, который ренденрится на странице

              • 0

                Ну вы так быстро ответили "да", видимо не поняли вопроса. Я даже удивился :) Конфиг из статьи к jss не имеет никакого отношения.


                То, что by design — это понятно, но помечтать о том, чтобы jss выдрать build-time, вполне можно, если не использовать его динамические фичи. Вот, например, попытка (насколько я понял, заброшенная) сделать что-то на эту тему: https://github.com/markdalgleish/jss-loader

                • 0

                  Мм, тогда зачем вам jss? Вы же хотите избавиться от фишки, которая лежит в его фундаменте. Можно сделать форк, взять build-time, выкинуть все ненужное и получить какой-нить новый *ss :)

  • 0
    Для тех, у кого Windows:
    • +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. Извиняюсь за двойной комментарий, мой косяк.-

      • 0
        Спасибо за дополнение
      • +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

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

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

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


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

              • 0

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


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


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

              • 0
                Веский довод.
          • 0
            Я лично привык сразу все ставить глобально (на локальной машине), потом просто используешь инструменты даже не задумываясь об установке.
      • +1
        Возможно кому то поможет. У меня nodemon, смог правильно подключить server.js только после того как в конце NODE_PATH была добавлена точка с запятой, то есть получилось так:
        set NODE_PATH=./src; && nodemon server.js
        • 0
          Спасибо, сейчас обновлю статью
  • +1
    А добавьте, пожалуйста, к Hello World авторизацию с JWT, компонент авторизации в React.js и проверку на валидность токена. А то TODO-tutorials много, а толковых примеров нет.
    • +1

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


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

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

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

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

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

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

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

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

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

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

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

    Почему?
    • 0
      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%, как когда-то было.


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

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

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

        И насколько сложнее сделать изоморфное приложение с бэкэндом не на Node? То есть понятно, что теоретически можно выполнить JS-код на чём угодно, но есть ли для этого готовые инструменты хотя бы в мейнстримных Java, C#, Python, etc.?
        • +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 может быть использован мобильными приложениями
          • удобно разделить разработку на две команды
          • проще тестировать
      • 0
        Не совсем понял соответствие пунктов ответов вопросам… :)

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

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

          Вы или не понимаете, о чем говорите, или это тролллинг.
          PHP отдает статичный html, который так и останется статичным html, можно лишь вручную его менять. React на сервере отдает статичный html, который на клиенте сразу же подхватывается клиентским реактом и дальше работает так, будто этот реакт его и сгенерировал (то есть при обновлении состояния будет обновится и html).
          • 0
            Я сам уже не помню, что вкладывал в то, что Вы процитировали. :)
            Возможно я что-то недопонял. :)
  • +1
    очень интересная статья — жду продолжения с нетерпением
    • 0

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

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

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

  • 0
    Ремарка по поводу сервера, его тоже надо собирати, иначе при первом запросе будет просидание по скорости. я так понимаю из-за 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 с серверного кода
    • 0

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

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

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


      Спасибо

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

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

    • 0

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

      • 0

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

  • 0

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

    • +1

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


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

  • 0

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

    • +1

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

  • +1
    Для npm run скриптов можно не указывать node_modules/...., соответственно и webpack глобально не ставить:
    In addition to the shell's pre-existing PATH, npm run adds node_modules/.bin to the PATH provided to scripts. Any binaries provided by locally-installed dependencies can be used without the node_modules/.bin prefix.
    • 0

      Спасибо за дополнение, не знал, а руки до прочтения спецификации еще не дошли

  • 0
    После 6 лет на Ruby (но почти без рельсов, чистые sinatra, grape и тд) попытка осознать webpack.config.js немного взрывает мозги
  • 0

    Спасибо за труд. Штудируя статью документации по оптимизации производительности, не втыкаюсь, почему этот код не работает:


      handleClick() {
        // This section is bad style and causes a bug
        const words = this.state.words;
        words.push('marklar');
        this.setState({words: words});
      }

    а этот работает:


      handleClick() {
        this.setState(prevState => ({
          words: prevState.words.concat(['marklar'])
        }));
      }

    Спасите-помогите! :)

    • 0
      Я не уверен конечно, но после setState состояние вроде как не изменится (потому что push уже по ссылке меняет данные в старом state), а значит и перерисовки не будет.
    • +1

      State явно можно менять только в конструкторе.

      • 0

        Непонятно, тогда почему этот код работает?


        src
        • 0

          Так здесь вроде всё, как и должно быть. state явно задается в конструкторе, а потом уже меняется только через setState.

        • +1

          1) Вы инициализируете state в конструкторе, все работает, противоречий нет
          2) Вы в toggleState меняете state через setState. setState — это не просто сеттер JS для аттрибута state, это функция, которая делает много всего. Противоречий нет
          3) В render, вы обращается к state напрямую но на чтение, это никто не запрещает. Противоречий нет.


          Биндить метод в render — плохая практика. Это не критично — оно будет работать, просто создаете оверхед на ровном месте, которого легко избежать.


          В конструкторе пишите
          this.toggleState = this.toggleState.bind(this);


          в render — onClick={this.toggleState}


          Подробнее: https://ryanfunduk.com/articles/never-bind-in-render/

      • 0

        Проблема другая. Но я не понимаю. Цитирую из статьи:


        The problem is that PureComponent will do a simple comparison between the old and new values of this.props.words. Since this code mutates the words array in the handleClick method of WordAdder, the old and new values of this.props.words will compare as equal, even though the actual words in the array have changed. The ListOfWords will thus not update even though it has new words that shoud be rendered.
        • +1

          Ну так var-log про это уже сказал.

          • 0

            Действительно, вот так работает:


              handleClick() {
                this.setState({words: this.state.words.concat(['marklar'])});
              }

            Но тогда зачем нужна вот эта вариация с prevState?


              handleClick() {
                this.setState(prevState => ({
                  words: prevState.words.concat(['marklar'])
                }));
              }
            • 0

              Ох, я понял! Эквивалентная запись с использованием spread:


                handleClick() {
                  this.setState({...this.state, words: this.state.words.concat(['marklar'])});
                }
  • 0

    И ещё. Очень интересно почитать "разжёванную" версию вот этой статьи.

    • +1

      Когда вы рендерите список элементов, вам надо передать каждому из них prop "key", который должен быть уникальным и быть как-то связанным с элементом (передавать значения от 1 до i является антипаттерном, так как не несет никакого смысла с точки зрения фреймворка). Если мы вставляем (или удаляем) элемент из списка, то при ререндеринге мы более эффктивно будем реюзать существующие элементы.


      State A1:


      <li key="11">11</li>
      <li key="22">22</li>
      <li key="33">33</li>
      <li key="44">44</li>

      State A2:


      <li key="1">11</li>
      <li key="2">22</li>
      <li key="3">33</li>
      <li key="4">44</li>

      State B1:


      <li key="11">11</li>
      <li key="22">22</li>
      <li key="44">44</li>

      State B2:


      <li key="1">11</li>
      <li key="2">33</li>
      <li key="3">44</li>

      Если упрощенно, то при переходе от A1->B1 у нас будет 1 обновление DOM, а из A2->B2 — 3, хотя с точки зрения юзера UI идентичен

  • 0
    Объясните, пожалуйста, зачем нужен webpack-dev-server? Ещё вторым окном его запускать и держать в runtime?

    В моём проекте перед запуском сервера webpack собирает bundle.js и всё. Для чего нужен какой-то webpack-dev-server? Для чего это ненужное усложнение?

    Все эти запутанные заморочки в большом количестве просто удручают. Я не могу их понять.
    • 0

      В статье описано, зачем нужен webpack-dev-server. Приведи конкретную цитату из статьи, которая тебе не понятна

      • 0
        Каждый раз пересобирать весь проект может быть весьма накладно: для проекта среднего размера сборка легко может достигать 30 и более секунд. Чтобы решить эту проблему, во время разработки очень удобно использовать webpack-dev-server.

        С чего взято, что время сборки занимает 30 секунд? Занимает 2-3 секунды. Если поставить watch: true, то bundle.js сам пересобирается. А причём тут webpack-dev-server? Вот это и непонятно. Я хочу разобраться. Пока не разберусь, не успокоюсь.
        • +1

          webpack — это сборщик, он конвертирует проект в конечные ассеты: js и css
          webpack-dev-server — это сервер, который запускает webpack для первоначальной сборки и далее для каждого измененного файла. Его задача: сборка + хостинг ассетов в процессе разработок + hot reload, то есть обновление страницы в браузере автоматически в процессе разработки


          С чего взято, что время сборки занимает 30 секунд?

          речь же не о хелло вордах

      • –1
        Тем более webpack-dev-server не запускается. Это что за такая штука, которую невозможности просто взять, установить и запустить! Просто элементарно взять, установить и запустить! Это вообще нормально? Разработка react-redux — это полная лажа!
  • 0
    Конечно, когда просто читаешь статью, всё здорово и замечательно. Можно написать хорошие и хвалебные комментарии в адрес этого стека технологий.

    Но когда лезешь разбираться и пытаешься сделать сам, то тут возникают кучу проблем. Везде бардак. Сплошной бардак. Каждый пишет, как хочет. Нет единых стандартов. Какие-то библиотека работают так, какие-то эдак. Кучу настроек, в которых очень легко запутаться. И в каждую настройку надо вникать, чтобы понимать, что на самом деле происходит. А их туча! Могут работать по-разному. Нет элегантности, простоты и красоты — что всегда должно быть превыше всего в программировании.

Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

Интересные публикации