habrahabr

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

  • пятница, 14 августа 2020 г. в 00:27:41
https://habr.com/ru/company/ruvds/blog/515008/
  • Блог компании RUVDS.com
  • Разработка веб-сайтов
  • JavaScript
  • ReactJS


Автор статьи, перевод которой мы сегодня публикуем, предлагает 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-проектов?