ZarahioN Presents

Answering why

Webpack

Разбираемся с React(Webpack)-ом [Pt. 1 — Manual React Installation (and pain of doing so)]

Что вообще такое React(.js)?

По уверениям создателей это просто быстрая, декларативная библиотека для создания пользовательских интерфейсов, и технически это сущая правда, однако.. насколько я заметил, реакт (как и многие подобные фреймворки) стал практически синонимом SPA (Single Page Application) — одностраничным приложениям, которые загружаются один раз и дальше работают без фактической перезагрузки страницы.

И это большей частью хорошо, не учитывая какой вес дополнительных библиотек и страшных слов тянет за собой эта аббревиатура. (Да-да, я знаю насколько многие боятся redux, не говоря о redux-saga, которые бок о бок частенько ковыляют на просторах вакансий React.js разработчиков). Впрочем, все не так страшно(, и часто очень даже приятно). Поэтому пристегиваемся и отправляемся в путь, ну а чтобы поездка прошла легко, начнем со слона аккуратно прячущегося на заднем плане.

Начало работы с React.js

Есть два (с половиной) основных способа начать использовать реакт, не учитывая банальную загрузку готовой библиотеки, но этот способ не для нас.

Первым и уже практически ставшим стандартом, является использование утилиты create-react-app, которая создаст базовую установку для практически мгновенного начала работы со стандартным и привычным набором инструментов.

Половиной подхода я бы назвал стареющую альтернативу — поиск шаблонов «по параметрам» на гитхабе или же просторах интернета, но этот способ немного сложнее и заставляет рассчитывать на сознательность индивидуала или команды, а не целой группы разработчиков присматривающих за официальным инструментом (чему тоже не всегда стоит полностью доверять, но это уже на гранях паранойи).

Однако, мы пока не станем пользоваться их помощью, и пройдем упрощенную версию их пути сами, наткнувшись на многочисленные камни и набив шишек по дороге и настругав мозолей. Да вебпак, я помню насколько весело было разбираться с твоей конфигурацией в ранних версиях.

Что же нам нужно?

Внимание, путь дальше очень страшен и труден, придется столкнутся с терминалом или командной строкой!
Существует альтернативный простой путь, но я все же рекомендую пройти базовую подготовку и «боевое крещение».

Node.js и npm, а для любителей котиков и чуть более упорных людей — еще и yarn

Процесс установки обоих достаточно прост и стандартизирован для главных ОС. После установки можно убедиться в успешности введя в терминал или командную строку:

node -v
npm -v

И для вышеназванных любителей котиков:

yarn -v

Если все команды успешно выполнились и вывели ожидаемые версии — успех! Первый шаг сделан.

Для чего нам Node.js?

Во-первых — простота и удобство добавления библиотек.
Во-вторых — сервер разработки на будущее, избавляющий от необходимости держать локальный apache, nginx либо альтернативы (не говоря о прелестях запуска файлов напрямую в браузере).
В-третьих и несерьезно — «это ж модно».

Следующим шагом нам нужно установить сам реакт и пару сопутствующих инструментов:

npm(yarn) init
Круглые скобки подсказывают взаимозаменяемость команд или флагов

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

npm i[nstall] -S(--save) react react-dom либо yarn add react react-dom
Квадратные скобки подсказывают полное название команды или флага

Это должно установить библиотеки react и react-dom, обе необходимые для работы реакта в браузере. И технически мы могли бы начать работу (на некоторых версиях реакта) прямо сейчас, используя src="node_modules/react/dist/..." но это не очень удобно и совсем не то, как мы будем в будущем работать.

Следующим шагом будет установка специального инструмента — «переводчика» (transpiler) или же просто компилятора babel. Он нужен главным образом из-за использованного в реакте языка JSX, который представляет собой XML с вписанным в атрибуты обычным JavaScript-ом. О нем мы еще позже подробно поговорим, но просто знай, что он помогает упростить жизнь, переводя более современный и удобный вариант JS в поддерживаемый повсеместно (и в некоторых областях сильно устаревший) JS.

Мы могли бы установить инструмент babel-cli, который позволяет в реальном времени преобразовывать код, однако, в мире мало кто использует его таким образом для чего-то серьезнее прототипа или приложения на вечер. Поэтому помимо babel мы добавим еще одно некогда страшное слово в список библиотек — webpack.

Webpack позволяет собирать сложные проекты из множества файлов-модулей и используемых библиотек в один большой исполняемый файл, который можно загрузить на страницу.
(При необходимости можно собирать несколько небольших исполняемых файлов и используемые библиотеки выносить отдельно, но это уже вопрос оптимизации процесса сборки)

npm i -D(--save-dev) webpack@3.11.0 babel-loader babel-core babel-preset-react-app
либо
yarn add -D(--dev) webpack@3.11.0 babel-loader babel-core babel-preset-react-app
-D или --dev это флаг для записи библиотек в условный раздел devDependencies нашего package.json. Условный он потому, что для запуска приложения нам не нужны и зависимости из dependencies — они будут собраны в готовый файл в будущем. Однако это полезное разделение на относящиеся непосредственно к работе приложения модули и просто необходимые для сборки (и желательно тестирования) инструменты.

Это загрузит непосредственно webpack и babel. Важный момент — проверь, что вебпак установился нужной версии. На протяжении его разработки было несколько критических изменений в структуре файла конфигурации, которые отказывались от обратной совместимости и нужно было обновлять файл конфигурации. Если у тебя установится версия, которая внесла подобные изменения в отношении к указанной, предложенный далее файл конфигурации может не работать. Плюс конфигурация вебпака была в некоторой мере «неодназначной» и в связи с обилием примеров на просторах интернета под прошлые версии — сломать что-то и гадать, почему оно не работает, было очень легко.
Сейчас ситуация стала куда лучше, но избежать возможных проблем на первых шагах все же не помешает.

Что же заглянем на страницу документации настройки вебпака и.. да…

Но, к счастью, большая часть настроек нам не понадобиться в ближайшее (и, скорее всего, дальнейшее) время, однако знать, куда попробовать заглянуть в случае чего — весьма полезно. Полный процесс настройки я, возможно, распишу как-нибудь в другой раз, сейчас же хватит простенького конфига для сборки базового приложения на реакте. (Мы почти готовы писать код спустя пол часа подготовки!)

@webpack.config.js
var path = require('path');
var webpack = require('webpack');

module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                loader: 'babel-loader',
                options: {
                    presets: ['react-app']
                }
            }
        ]
    },
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')],
        extensions: ['.js', '.jsx']
    },
};

Для начала что такое require и module.exports — это функционал по соответственно импорту и экспорту модулей и библиотек. Он начал свой путь с библиотек вроде RequireJS, потом перекочевал в Node.js и JS как расширенная часть языка и наконец был принят в стандарте ES6. (Или как-то так, я не слишком хорошо осведомлен о истории модулей).

Далее непосредственно конфиг:
entry — файл, с которого начинать сборку, с него по сути начинает работу приложение — загружает при необходимости библиотеки (реакт, к примеру) и что-то делает со страницей.
output — путь и название конечного файла (или файлов) в которые соберется весь необходимый код приложения.
module — «логика» (rules) вебпака — какие файлы обрабатывать, через какие загрузчики (loader) их пропускать, какие параметры передать этим загрузчикам и так далее.
— — test — регулярное выражение (как правило) по которому проверять, какие файлы должен обрабатывать этот загрузчик
— — loader — непосредственно название библиотеки загрузчика
— — options — набор параметров, который нужно передать данному загрузчику, каждый загрузчик принимает свой собственный набор параметров
resolve — какие пути (modules) проверять при импорте модулей в коде и какие расширения файлов считать импортируемыми (extensions) таким образом. Это немного своеобразный элемент, так как он (должен) работать только для импорта библиотек, но можно настроить подгрузку своих файлов, чтобы не нужно было прописывать глубоких путей наверх в некоторых случаях. Это своеобразный вопрос конфигурации и я, возможно, разберу его при необходимости в будущем.

И.. на этом все с настройкой. Теперь мы наконец можем начать создавать наше приложение!
Но, отставив шутки, если ты высидел и прошел весь этот путь, постаравшись разобраться что и зачем нужно — ты герой. Многих пугает современная разработка на JS большой частью из-за всего этого скопища полезных и порой необходимых инструментов, которая позволила удобно преобразовать простенький скриптовый язык в что-то чуть более серьезное и уверенное. (Ну или демонического монстра, разрушающего будущее веб-разработки, кому что больше по душе)

Но, мы еще не закончили, последний шаг.

Hello World with React.js (and bunch of not so fancy stuff)

Создадим два пустых файла index.html и index.js (который мы указали в entry конфига вебпака). В body html файла нужно добавить две строки:

<div id="root"></div>
<script src="bundle.js"></script>

Элемент div нужен как контейнер (или обертка) для указания реакту, куда вставлять (render) разметку его компонентов. Скрипт же очевидным образом загружает bundle.js, который мы сейчас соберем вебпаком из index.js. Переходя к которому:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

const StateLessComponent = ({text}) =>
<div>
    <span>{text}</span>
</div>;

class StateFullComponent extends Component {
    state = {
        text: 'Heya Earthlings!'
    }
    render(){
        return (
            <StateLessComponent text={this.state.text}/>
        );
    }
}

ReactDOM.render(<StateFullComponent/>, document.getElementById('root'));

И примерно так выглядит весь React-код. Конечно же, разметки, логики и финтифлюшек в реальной жизни куда больше, но база остатется той же. Смотрим снизу-вверх:

ReactDOM.render(JSXComponent, DOMNode) — вставляет (монтирует) собранный реакт(jsx)-компонент в указанный элемент DOM-а страницы. То есть, мы находим какой-то элемент (контейнер) на реальной странице и вставляем в него наш компонент (или дерево вложенных компонентов, как в нашем скромном случае).

class StateFullComponent extends Component — это ES6 синтаксис-обертка для создания функции-конструктора и по совместительству в реалиях реакта — полноценный компонент.
Полноценный означает что он может, помимо простого отображения разметки (метод render), иметь некую логику (свои методы\функции), иметь состояние (state) и использовать некоторые «события» реакта. (lifecycle hooks, технически это не события, но тем не менее, практическая суть достаточно близка).
О всех этих несомненно интересных словах мы поговорим, уже разбирая непосредственно внутренности реакта.

Как можно догадаться, есть и «неполноценные» или же как их чаще называют — функциональные компоненты — они могут только отображать разметку и переданные данные (включая возможность привязывать обработчики событий). Чем, собственно и является:

const StateLessComponent = ({text}) => <div>...

(arguments) => (returnedValue) это небольшое но приятное дополнение ES6 имеющее, помимо некоторых очень нужных и важных свойств, возможность записывать функции в коротком виде.

Разделение на statefull и stateless, «умные» и «глупые», контейнеры и компоненты мелькает очень часто, однако простая теоретическая суть — умные имеют логику, данные, и всякое и раскидывают это все по глупым, которые занимаются только отображением — очень часто не суть реальная. Подходов и способов работы со всей этой кутерьмой множество и основную часть мы рассмотрим, как выше было сказано, ковыряя сам реакт.

Последние строки:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

Это уже названный выше функционал модулей в ES6 версии JS — он немного отличается от использованного в конфиге вебпака из-за того, что даже Node.js еще не начал полноценное использование этого стандарта.
(Насколько я знаю*. Достаточно скоро или уже Node.js может полностью перейти к этому стандарту, но до тех пор там используется CommonJS версия импорта и экспорта — require() и module.)

Помимо синтаксического отличия я использовал именованный импорт — он позволяет запросить только нужные части из модуля, без всего модуля.
И опять же требуется пометка — насколько я знаю, на данный момент для такого (реально) частичного импорта нужно использовать «обрезание» кода и дерева модулей — «dead code elimination» и «tree shaking», по этим словам ты скорее всего найдешь информацию, если тебя интересует вопрос оптимизации размеров твоего приложения. Я же, возможно, в будущем сам расскажу подробнее про эти возможности, но учитывая какой уровень дополнительной сложности вносит только базовый вебпак, оптимизация — это разговор на другой раз.

Поэтому мы наконец завершаем наше затянувшееся путешествие, во время которого мы успели
1. [Поставить Node.js, npm и, опционально, но мягко рекомендуемо, yarn.](#Что же нам нужно?)
2. Создать пустой проект через yarn init и добавить в него реакт через yarn add
3. Немного разобрать что есть babel и webpack и зачем они нужны
4. [И даже написать первые строки приложения на реакте](#Hello World with React.js (and bunch of not so fancy stuff))

На этом я ненадолго оставляю тебя с, надеюсь, немного подкипевшей головой и списком материала на вечернее чтение:
npm, yarn, webpack, babel и сам реакт
Это сайты документации или помощи инструментов, которыми мы будем очень часто пользоваться на пути создания реакт приложений и я, как бы не хотел, не смогу ответить на все вопросы — иногда придется расчехлить навыки поиска и покопаться в официальных источниках (ну или заглянуть на stackoverflow\подобные) в поисках нужного ответа.

Благость под названием create-react-app [Pt.2 – Automatic React Installation (and how to break it)]

Что же, в прошлый раз мы исследовали прелестное искусство мазохизма и настраивали своеобразный синоним среды разработки самостоятельно — с вебпаком, бейблом и ручной настройкой всего и вся. Но еще в самом начале прошлого рассказа я упомянул, о куда более простой альтернативе:

create-react-app

Что же это? По факту cli — command line interface для создания базового шаблона стандартного приложения на реакте.

В начале своего пути реакт не имел подобных инструментов (да и вообще разрабатывался «по приколу» и «для себя» фейсбуком). Однако, со временем и появлением других подобных библиотек на базе или доработке идей виртуального DOM-а и наличием у них подобных инструментов упрощения жизни разработчиков, сообщество реакта и фейсбук (вроде) наконец созрели для создания своего инструмента.

Почему не сразу? Все просто — чтобы научиться что-то чинить, нужно или это что-то сломать, или построить свой вариант. Ломать это, конечно, весело, но без хоть какого-либо понимания, процесс обратной починки может затянуться, а то и вовсе по факту не начаться. Если у тебя что-то сломается при использовании шаблона созданного c-r-a — ты хотя бы примерно сможешь ориентироваться в каком из узлов всей цепочки сборки и компиляции это произошло и где искать помощи.

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

Впрочем, завершим затянувшееся лирическое отступление и приступим к делу:

Работа с create-react-app

Есть два пути: старый-добрый-глобальный, если у тебя npm ниже версии 5.2, и новый-модный npx, если выше. Отличаются они в одну строчку и кучку деталей (про которые можно почитать здесь(англ) или здесь(рус)), поэтому посмотрим оба (, а я таки обновлю свою стареющую ноду 8.5.0 версии (хотя это делать оказалось необязательно)).

Классический способ это установка cli в глобальную директорию npm:

npm i -g create-react-app

После чего можно использовать: (если это первая глобальная установка, возможно придется добавить путь в PATH на Win или в /bin (или куда нужно) на *nix системах)

create-react-app app-folder-name

Это создаст проект в «app-folder-name» по текущему пути консоли.

Модный способ это использование вышеназванного npx, который поставляется с 5.2 версий npm:

npm install npm@latest -g

Это обновит npm, на случай, если ты хочешь использовать npx, но npm уже старенький. Напоминаю, что проверить версии можно с помощью npm(node) -v

npx create-react-app app-folder-name

Это опять же создаст проект в «app-folder-name» по текущему пути консоли.

Удобство npx состоит в том, что он позволяет как запускать локально установленные версии инструментов (модулей), так и использовать «одноразовые» инструменты вроде create-react-app без их установки в глобальную директорию. Что в последнем случае очень удобно — не приходится следить за версией названных инструментов.

И.. собственно все, в этот мы справились куда быстрее, не так ли? Мы можем запустить вебпак через yarn start и начать создавать!

Но это скучно.

Поэтому, приготовься покинуть кабину, мы катапультируемся!

(Я знаю, что eject это не совсем катапультироваться, но тем не менее)

create-react-app это очень базовый, стандартизированный и проверенный способ начинать работу с реактом, поэтому в нем нет всех тех прикольных штук, которые тебе возможно захочется использовать. К примеру последний писк моды и ES8 с компиляцией от бейбла, или немногословный и немножко страшный, но такой удобный stylus, или тебе просто, как и мне, интересно поковыряться и узнать как они все это настроили.

Поэтому без промедления (и находясь в папке с созданным проектом):

yarn eject

Ах, сколько новых папочек и файлов

К слову, если ты (как любой адекватный человек, да-да) держишь проект в гит-репо, c-r-a не даст тебе катапультироваться без сохранения последних изменений, что очень мило с их стороны, на случай если тебе не понравится опыт досрочного кхем выпуска.

Что же, разберемся по-подробнее с появившимся разнообразием конфигурационных файлов. В особенности меня сейчас интересует configs/webpack.config.dev.js — он содержит конфигурацию для — yarn start — процесса разработки, а ближайшее время мы будем заниматься только им.
К слову, в нем достаточно много подробных комментариев, так что приглашаю заглянуть в него вместе, не дожидаясь моих пояснений.
Также помимо самого файла конфигурации, может быть полезно заглянуть в configs/paths.js — он содержит распознаватель путей и непосредственно их самих — полезно примерно прикидывать что куда указывает.

module.exports = {
  entry: [
    require.resolve('react-dev-utils/webpackHotDevClient'),
    paths.appIndexJs,
  ],
  output: {
    pathinfo: true,
    filename: 'static/js/bundle.js',
    chunkFilename: 'static/js/[name].chunk.js',
    publicPath: '/',
  },
  resolve: {
    modules: ['node_modules', paths.appNodeModules].concat(
      process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
    ),
    extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        test: /\.(js|jsx|mjs)$/,
        enforce: 'pre',
        use: [
          {
            options: {
              formatter: eslintFormatter,
              eslintPath: require.resolve('eslint'),
            },
            loader: require.resolve('eslint-loader'),
          },
        ],
        include: paths.appSrc,
      },
      {
        oneOf: [
          {
            test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            loader: require.resolve('url-loader'),
            options: {
              limit: 10000,
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
          {
            test: /\.(js|jsx|mjs)$/,
            include: paths.appSrc,
            loader: require.resolve('babel-loader'),
            options: {
              cacheDirectory: true,
            },
          },
          {
            test: /\.css$/,
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  importLoaders: 1,
                },
              },
              {
                loader: require.resolve('postcss-loader'),
                options: {
                  ident: 'postcss',
                  plugins: () => [
                    require('postcss-flexbugs-fixes'),
                    autoprefixer({ ... }),
                  ],
                },
              },
            ],
          },
          {
            exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
            loader: require.resolve('file-loader'),
            options: {
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
        ],
      },
    ],
  },
};

Я, как можно заметить, обрезал комментарии и некоторые слишком уж специфичные элементы оптимизации или работы вебпака, с которыми шансы столкнуться приближается к скромному нулю (плюс плагины, опции загрузчиков и бонусы для react-native).

Как можно сразу заметить, entry и output слегка отличаются от нашего самопала, однако большей частью это лишь косметические изменения:

entry может принимать не единственную строку, как в нашем случае, а массив или даже объект строк (и массивов, вложенность, ура), в данном случае помимо нашего приложения вебпак просто обрабатывает сервер для разработки и набор заполнителей выбранный c-r-a; output же использует несколько доп опций:
pathinfo — включает в конечную сборку данные о путях, модулях и прочем,
publicPath — позволяет указать корень выхода — папку, в которую помещаются все сгенерированные файлы, мы не использовали его, так как наш проект содержал всего один файл
chunkFilename — указывает как называть чанки — куски кода оптимизированные для отдельной загрузки и кеширования.

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

И наконец module. Помимо самих загрузчиков, присутствует один параметр strictExportPresence — он указывает вебпаку расценивать не найденный модуль (импорт) как ошибку и сразу же прекращать дальнейшую сборку, что очень полезно, так как сразу же кричит о проблеме, не дожидаясь перезагрузки пака и порой, если используется роутер или компонент загружается не сразу, попадания на пропущенный импорт.

С загрузчиками, впрочем, все достаточно просто и знакомо (спасибо вебпаку за стандартизацию конфигурации):
Первым идет eslint — линтер для JS. Линтер — это «инструмент статического анализа» — или говоря по-человечьи, подсветка ошибок и несоответствий выбранному набору правил написания кода. Очень полезный инструмент, который, что особенно приятно, как правило доступен в виде расширения для редактора.
При этом загрузчик линтера использует специальный модификатор enforce — он нужен, чтобы линтер всегда обрабатывал исходный код до его преобразования бейблом. Модификатор (параметр) enforce задает категорию порядка применения загрузчика, всего доступно 4 опции (pre, inline, normal, post), о которых можно почитать здесь.

После линтера идет основной набор, объединенный через модификатор oneOf — он позволяет обработать файл только одним (первым подходящим) загрузчиком. В конце находится загрузчик просто предоставляющий доступ к файлу, не затрагивая при этом исходный код и стандартные медиа-форматы — png, jpg, т.д — за счет их расположения выше по списку и соответственно порядку загрузки.

На чем наш расковыряный файл конфигурации заканчивается. Я, опять же, мягко указываю тебе покопаться в конфигурации самостоятельно и, к примеру, добавить поддержку любимого css препроцессора или пресета для бейбла. Процесс достаточно простой, и я буду разбирать его в будущем, когда приложению понадобится стилизация (а делать ее на css или тем более встроенных стилях я отказываюсь), но всегда полезно попробовать самостоятельно (имея бекап), прежде чем идти читать руководство.
Также должен заметить, что учитывая базовость и частое нежелание катапультироваться из c-r-a, создали кучку способов изменения или расширения конфигурации, оставаясь технически с c-r-a. Но я сам больше склоняюсь к eject-у, нежели использованию таких инструментов.

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