company_banner

Отказ от create-react-app и создание собственного шаблона для React-приложений

Автор оригинала: Nikhil Kumaran S
  • Перевод
Автор статьи, перевод которой мы сегодня публикуем, предлагает React-разработчикам отойти от использования create-react-app (CRA) и создать собственный шаблон для React-приложений. Здесь речь пойдёт о преимуществах и недостатках CRA, а так же будет предложено решение, которое способно заменить create-react-app.



Что такое CRA?


Create React App — это набор инструментов, созданный и поддерживаемый разработчиками из Facebook. CRA предназначен для быстрого создания шаблонных проектов React-приложений. При использовании CRA база React-проекта создаётся с помощью одной команды.

Сильные стороны CRA


  • CRA позволяет создать базу для React-проекта одной командой:

    npx create-react-app my-app
    
  • Использование CRA избавляет разработчика от необходимости глубокого изучения вспомогательных инструментов. Разработчик может сосредоточиться на React и не беспокоиться о настройке Webpack, Babel и других служебных механизмов.
  • При применении CRA разработчику требуется лишь одна зависимость, имеющая отношение к сборке проекта. Это — react-scripts. Эта зависимость включает в себя все остальные зависимости сборки, в результате оказывается, что, например, для установки и обновления зависимостей достаточно одной команды:

    npm install react-scripts@latest
    

Недостатки CRA


  • При использовании CRA усложняется использование собственных конфигураций сборки проектов. Один из способов обхода этого ограничения заключается в использовании команды eject, но это лишает CRA-проекты преимущества в виде единственной зависимости сборки. Ещё один способ использования собственных конфигураций заключается в применении пакетов наподобие customize-cra или react-app-rewired, но подобные конфигурации отличаются ограниченными возможностями.
  • CRA скрывает от разработчика внутренние механизмы вспомогательных подсистем проектов. React-разработчик должен знать о том, что именно происходит в ходе подготовки React-приложения к реальной работе. Но так как в CRA используется уже не раз упомянутая «политика одной зависимости», у начинающего разработчика может возникнуть впечатление того, что react-scripts — это единственная зависимость, которая нужна для запуска React-приложения. Начинающий может попросту не знать о том, что, на самом деле, react-scripts — это, по сути, лишь «упаковка» для транспилятора (Babel) и бандлера (Webpack), которые играют ведущую роль в подготовке React-приложений к реальной работе. Я, признаться, и сам этого не знал до тех пор, пока не прочитал этот замечательный материал.
  • CRA, как мне кажется, перегружен возможностями, которые, в каком-то проекте, вполне могут оказаться невостребованными. Например, заготовки приложений, создаваемые с помощью CRA, поддерживают SASS. То есть, если в проекте используется обычный CSS или Less, поддержка SASS окажется совершенно ненужной. Вот, если интересно, файл package.json CRA-приложения после команды eject. В этом файле «развёрнуты» зависимости, раньше представленные react-scripts.

Альтернативой CRA может стать разработка собственного шаблона для быстрого создания базовых React-проектов.

Альтернатива CRA


Разрабатывая альтернативу CRA, мы собираемся оснастить её возможностью быстрого, с использованием лишь одной команды, создания базовых React-проектов. Это повторяет одну из полезных возможностей create-react-app. А недостатки CRA мы, конечно, в нашу систему переносить не будем, самостоятельно устанавливая зависимости и настраивая проект. В наш проект не попадут две других полезных возможности CRA (избавление разработчика от необходимости изучения вспомогательных механизмов и схема «одной зависимости»), так как они несут с собой и недостатки (сокрытие внутренних механизмов вспомогательных подсистем от разработчика и сложность настройки собственных конфигураций сборки проектов).

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

Начнём работу с инициализации проекта средствами npm и с инициализации его git-репозитория:

npm init
git init

Создадим файл .gitignore следующего содержания:

node_modules
build

Это позволит нам не включать в состав репозитория папки, имена которых присутствуют в файле.

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

Библиотеки react и react-dom


Это — единственные зависимости времени выполнения, которые нам нужны:

npm install react react-dom --save

Транспилятор (Babel)


Транспилятор Babel преобразует код, соответствующий стандартам ECMAScript 2015+, в код, который будет работать и в новых, и в устаревших браузерах. Babel, благодаря применению пресетов, используется и для обработки JSX-кода:

npm install @babel/core @babel/preset-env @babel/preset-react --save-dev

Вот как выглядит простая конфигурация Babel, предназначенная для подготовки к работе React-приложений. Эту конфигурацию можно добавить в файл .babelrc или в package.json:

{
    "presets": [
        "@babel/preset-env",
        "@babel/preset-react"
    ]
}

Babel поддерживает множество пресетов и плагинов. Их можно добавлять в проект по мере возникновения необходимости в них.

Бандлер (Webpack)


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

npm install webpack webpack-cli webpack-dev-server babel-loader css-loader style-loader html-webpack-plugin --save-dev

Простая конфигурация Webpack, предназначенная для сборки пакетов React-приложений, выглядит так, как показано ниже:

const path = require('path');
const HtmlWebPackPlugin = require('html-webpack-plugin');

module.exports = {
  output: {
    path: path.resolve(__dirname, 'build'),
    filename: 'bundle.js',
  },
  resolve: {
    modules: [path.join(__dirname, 'src'), 'node_modules'],
    alias: {
      react: path.join(__dirname, 'node_modules', 'react'),
    },
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
        },
      },
      {
        test: /\.css$/,
        use: [
          {
            loader: 'style-loader',
          },
          {
            loader: 'css-loader',
          },
        ],
      },
    ],
  },
  plugins: [
    new HtmlWebPackPlugin({
      template: './src/index.html',
    }),
  ],
};

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

Это — все необходимые нам зависимости. Теперь давайте добавим в проект шаблонный HTML-файл и заготовку React-компонента.

Создадим в директории проекта папку src и добавим в неё файл index.html:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>React Boilerplate</title>
</head>
<body>
  <div id="root"></div>
</body>
</html>

В той же папке создадим React-компонент HelloWorld:

import React from 'react';

const HelloWorld = () => {
  return (
      <h3>Hello World</h3>
  );
};

export default HelloWorld;

В ту же папку добавим файл index.js:

import React from 'react';
import { render } from 'react-dom';

import HelloWorld from './HelloWorld';

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

И наконец — добавим в package.json описания скриптов для запуска (start) и сборки (build) проекта:

"scripts": {
    "start": "webpack-dev-server --mode=development --open --hot",
    "build": "webpack --mode=production"
  }

Вот и всё. Теперь в нашем распоряжении имеется работоспособная заготовка React-приложения. Убедиться в этом можно, выполнив команды npm start и npm run build.

Теперь давайте оснастим нашу систему возможностью подготовки шаблона проекта с помощью единственной команды. То есть — воссоздадим одну из сильных сторон CRA. Мы собираемся использовать исполняемый JS-файл, который будет вызываться при вводе соответствующей команды в командной строке. Например, подобная команда может выглядеть так:

reactjs-boilerplate new-project

Для реализации этой идеи мы собираемся воспользоваться разделом bin файла package.json.

Сначала установим пакет fs-extra:

npm i fs-extra

Теперь создадим исполняемый JS-файл start.js, который будет расположен в папке bin нашего проекта. Поместим в этот файл следующий код:

#!/usr/bin/env node
const fs = require("fs-extra");
const path = require("path");
const https = require("https");
const { exec } = require("child_process");

const packageJson = require("../package.json");

const scripts = `"start": "webpack-dev-server --mode=development --open --hot",
"build": "webpack --mode=production"`;

const babel = `"babel": ${JSON.stringify(packageJson.babel)}`;

const getDeps = (deps) =>
  Object.entries(deps)
    .map((dep) => `${dep[0]}@${dep[1]}`)
    .toString()
    .replace(/,/g, " ")
    .replace(/^/g, "")
    // исключим зависимость, используемую только в этом файле, не относящуюся к шаблону
    .replace(/fs-extra[^\s]+/g, "");

console.log("Initializing project..");

// создадим папку и инициализируем npm-проект
exec(
  `mkdir ${process.argv[2]} && cd ${process.argv[2]} && npm init -f`,
  (initErr, initStdout, initStderr) => {
    if (initErr) {
      console.error(`Everything was fine, then it wasn't:
    ${initErr}`);
      return;
    }
    const packageJSON = `${process.argv[2]}/package.json`;
    // заменим скрипты, задаваемые по умолчанию
    fs.readFile(packageJSON, (err, file) => {
      if (err) throw err;
      const data = file
        .toString()
        .replace(
          '"test": "echo \\"Error: no test specified\\" && exit 1"',
          scripts
        )
        .replace('"keywords": []', babel);
      fs.writeFile(packageJSON, data, (err2) => err2 || true);
    });

    const filesToCopy = ["webpack.config.js"];

    for (let i = 0; i < filesToCopy.length; i += 1) {
      fs.createReadStream(path.join(__dirname, `../${filesToCopy[i]}`)).pipe(
        fs.createWriteStream(`${process.argv[2]}/${filesToCopy[i]}`)
      );
    }
    // npm, при установке пакета, удалит файл .gitignore, поэтому его нельзя скопировать из локальной папки шаблона; этот файл нужно загрузить. После отправки кода в GitHub-репозиторий пользуйтесь raw-файлом .gitignore
    https.get(
      "https://raw.githubusercontent.com/Nikhil-Kumaran/reactjs-boilerplate/master/.gitignore",
      (res) => {
        res.setEncoding("utf8");
        let body = "";
        res.on("data", (data) => {
          body += data;
        });
        res.on("end", () => {
          fs.writeFile(
            `${process.argv[2]}/.gitignore`,
            body,
            { encoding: "utf-8" },
            (err) => {
              if (err) throw err;
            }
          );
        });
      }
    );

    console.log("npm init -- done\n");

    // установка зависимостей
    console.log("Installing deps -- it might take a few minutes..");
    const devDeps = getDeps(packageJson.devDependencies);
    const deps = getDeps(packageJson.dependencies);
    exec(
      `cd ${process.argv[2]} && git init && node -v && npm -v && npm i -D ${devDeps} && npm i -S ${deps}`,
      (npmErr, npmStdout, npmStderr) => {
        if (npmErr) {
          console.error(`Some error while installing dependencies
      ${npmErr}`);
          return;
        }
        console.log(npmStdout);
        console.log("Dependencies installed");

        console.log("Copying additional files..");
        // копирование дополнительных файлов с кодом
        fs.copy(path.join(__dirname, "../src"), `${process.argv[2]}/src`)
          .then(() =>
            console.log(
              `All done!\n\nYour project is now ready\n\nUse the below command to run the app.\n\ncd ${process.argv[2]}\nnpm start`
            )
          )
          .catch((err) => console.error(err));
      }
    );
  }
);

Теперь давайте свяжем этот исполняемый JS-файл с командой из package.json:

"bin": {
    "your-boilerplate-name": "./bin/start.js"
  }

Создадим локальную связь для пакета:

npm link

Теперь, после выполнения этой команды, если в терминале выполнить команду вида your-boilerplate-name my-app, будет вызван наш исполняемый файл start.js. Он создаст новую папку с именем my-app, скопирует в неё файлы package.json, webpack.config.js и .gitignore, а так же папку src, и установит зависимости нового проекта my-app.

Замечательно. Теперь всё это работает на вашем компьютере и позволяет вам, пользуясь единственной командой, создавать базовые React-проекты, обладающие собственной конфигурацией сборки.

Вы можете пойти дальше и опубликовать свой шаблон в реестре npm. Для того чтобы это сделать, сначала надо отправить проект в GitHub-репозиторий. А дальше — следуйте этим инструкциям.

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

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

Я подготовил шаблон reactjs-boilerplate, позволяющий создавать проекты, готовые к работе в продакшне. Тут используется соответствующая конфигурация сборки, линтинг и хуки, ответственные за проверку проекта перед созданием коммитов. Испытайте этот шаблон. Если у вас появятся какие-то идеи по его совершенствованию, или если вы решите сделать вклад в его разработку — присоединяйтесь к работе над ним.

Итоги


Вот о чём мы говорили в этом материале:

  • Мы разобрали плюсы и минусы create-react-app.
  • Мы реализовали в своём проекте полезную возможность CRA по созданию заготовок React-приложений одной командой. А от недостатков CRA мы избавились.
  • Мы оснастили свой проект минимальными конфигурациями Webpack и Babel, необходимыми для сборки и запуска React-приложений.
  • Мы создали React-компонент HelloWorld.js, предусмотрели возможность сборки проекта и его запуска с использованием сервера разработки.
  • Мы создали исполняемый JS-файл и связали его с соответствующей командой, воспользовавшись разделом bin файла package.json.
  • Мы воспользовались командой npm link с целью создания локальной связи для нашего проекта и получения возможности создавать новые базовые проекты на его основе с помощью единственной команды.

Надеюсь, этот материал поможет вам в деле разработки проектов, основанных на React.

Пользуетесь ли вы create-react-app при создании новых React-проектов?

RUVDS.com
RUVDS – хостинг VDS/VPS серверов

Комментарии 10

    +1

    Зачем мучиться с webpack, когда есть parcel.js?

      0

      А вторая версия уже вышла? В первой немало проблем было для не совсем стандартных случаев, вроде тех, когда нужно поддерживать несколько index.html страниц.

      +5

      Как всегда, автор натягивает сову на глобус ради того, чтобы просто статью написать. По сути, единственная разумная причина — это первая, которая решается средствами, которые автор сам же и упоминает — customize-cra или craco (react-app-rewired вроде считается устаревшим со второй версии CRA).


      Вторая причина — откровенно ниочём. Ну да, начинающий так будет думать. Но если начинающего ткнуть в настройку вебпака — он с криками убежит и больше никогда не вернётся. Так что надо будет — пойдёт и изучит, из чего CRA состоит и как его правильно настраивать. Рано или поздно это всегда придётся сделать, но я бы сказал, что лучше это делать после того, как ты поигрался со своим первым сайтом и решил углубиться.


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


      А ещё можно нехило поджечь кресло, дебажа bin/start.js автора, куда он напихал просто неведомую хрень. Особенно уровень неведомого будет зашкаливать для начинающего, о котором автор, вроде бы, заботится. И эту хрень поддерживать он, в отличие от авторов CRA, конечно же, не будет, так что любые изменения и хотелки придётся вносить самостоятельно. За это автора, кстати, неплохо пропесочили в комментах исходной статьи.


      Треш, короче. Конечно, разбираться в том, из чего CRA состоит — полезно и даже необходимо. Но отказываться от инструмента при этом — совершенно необязательно.

        0
        Давно ищё ответ на вопрос, почему мне нужно бояться команды eject? Что в этом плохого?
        Я воспринимаю cra — как отличный отдебаженный инструмент, который разрабатывают много хороших разработчиков. И это как стартовая настройка для проектов (типа prettier-а, чтоб не было разногласий в команде.). И если вдруг нужно что-то добавить/кастомизировать — делаем eject. Так вот в чём проблема eject-а? почему я не должен его делать?
          0

          Всё дело в обновлениях. Поддерживать приложение, основанное на CRA без eject очень легко — по сути, за весь билд отвечает команда CRA, а всё, что нужно девелоперу — просто обновиться.


          Если же ты сделал eject, то проще держать все зависимости залоченными, потому что постоянно обновлять их (а зависимости, используемые CRA, выходят часто) — очень тяжело. Зато легко что-нибудь сломать в билде.


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


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

            0
            спасибо.
              +1

              У меня лично был опыт проекта, который стартовали именно извлечением из CRA + TypeScript. Кастомизаций было совсем немного, но уже к моему приходу в компанию часть зависимостей устарела (примерно на год).


              Суммарно ушло около 8 месяцев, чтобы обновить всё до актуальной версии и "запихнуть обратно" (параллельно с поддержкой и разработкой высоко-нагруженного проекта).


              В обновлённую версию мы уже добавили кастомизацию, которая корректировала некоторые хотелки, но больше никакого eject.


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


              P.S. Для себя – это очень полезная и интересная практика посмотреть, что там под капотом.

            0
            как отличный отдебаженный инструмент, который разрабатывают много хороших разработчиков

            Правда хороших? Я когда eject делал, плевался на чём свет стоит. Огромные файлы состоящие преимущественно из лапши. Никакой внятной декомпозиции. Просто хаос творится. Единственное что их спасает — тонны комментариев с прикреплёнными ссылками на issues. Постороннему человеку такое поддерживать будет очень непросто.


            Когда я переписал всё это с декомпозицией, внезапно, окалось, что всё можно сделать и просто, и аккуратно, и сохравнив всё необходимое (и разумеется добавив свои хотелки). Да, для этого нужно много времени, да требуется опытный разработчик, да придётся продираться через клубок сложной конфигурации. Но сделать это по уму более чем реально.


            В чём проблема eject-а? Вот сделала команда eject, т.к. не хватило ей чего-то. А потом пытались поддерживать эту лапшу со своими примесями. С каждым изменением это становилось всё более и более тяжелой задачей. Любые изменения касающиеся SSR сервера и webpack-конфига превращались в агонию. При всём при этом за год таких мучений CRA ушёл вперёд и стал работать во многом иначе. Так что просто подсмотреть как у них — было часто бесперспективным вариантом. Менялись либы, конфиг опции, в целом подходы.


            По итогу полный отказ от CRA и создание своего велосипеда. Взяли только то, что сами используем. Сконфигурировали так что можно миддла это править посадить. Разбили всё на десятка два мелких функций, с внятными названиями, где отдельно взятая задача решается цельно, а не разбросана по всему 700+ строчному файлу. Все костыли тоже аккуратно прокомментировали. Ну и добавили свои хотелки, разумеется. Из того что я бы изменил повтори я это снова — я бы написал конфигурацию на TS, добавив руками типы там, где их нет.


            Для меня вывод простой — если у вас большой и сложный проект, много своих хотелок, и достаточно бюджета времени на то чтобы сшить конфиг под свои нужды, тогда не заигрывайтесь с CRA. А сделав eject, просто выпиливайте его целиком. Сразу. Сэкономите себе кучу времени и нервов. Ну и денег :)


            Ну и к изначальной цитате. Почему-то многие люди думают, что если проект имеет много звёздочек, то там внутри не говнокод. Бывает это и так. Но в случае, скажем, с npm, cra, enzyme, telegram — это совсем не так. Мне просто больно было копаться в их кодовой базе.

              0
              спасибо, что поделились.

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

              Хотелок не сильно много. Там декораты для mobx-а добавить, там svgo какой-нибудь прикрутить. Впринципе, полностью понимаем, что после eject-а мы сами по себе, и постепенно «заменяем» cra-шный конфиг своим по мере развития проекта.
              Но фишка в том, что в самом начале мы-то секономили кучу времени за счёт того, что другие (команда facebook или кто там разрабатывает cra) его потратили.
              Мы ведь не знаем на старте как дальше проект пойдёт, может и cra достаточно будет, а если не будет — пожалуйста eject. Я именно поэтому недоумевал — что плохого, чтоб сначала взять cra-конфиг секономив время, а потом допиливать его по мере развития проекта (после eject-а под свою ответственность понятное дело).
            0
            Главное что узнал со стати делать исполняемый файл. Только не понял почему он сделал alias для react вроде с этим кодом ничего не изменится или я не прав?

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

            Самое читаемое