Как стать автором
Обновить
55.12
SimbirSoft
Лидер в разработке современных ИТ-решений на заказ

Развертывание React-приложения

Время на прочтение15 мин
Количество просмотров25K

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

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

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

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

Сборка приложения

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

Для сборки можно взять create-react-app или сборщики модулей, которых также немало.

Вариант с create-react-app подойдет, если мы готовы пожертвовать гибкостью, а иногда и безопасностью. Что это значит? 

Во-первых, create-react-app использует под капотом webpack со всеми необходимыми плагинами. Если в репозитории подключен depend-bot, который следит за актуальностью пакетов, а также их зависимостей, он может выявить устаревший пакет или пакет с уязвимостью, обновление которого лежит на разработчиках CRA. Также минорные версии пакетов обновляют явно не в первую очередь.

Во-вторых, добавление разных плюшек в сборку потребует инжекта webpack конфига, а это практически то же самое, что писать конфиг с нуля самому и подписываться на его поддержку. Поэтому CRA в данном примере использовать не будем.

Если же рассматривать сборщики модулей, то самое гибкое решение на данный момент — webpack 5. Именно его и возьмем за основу, так как основное требование — гибкая и мощная настройка проекта.

Требования к сборке на webpack:

  1. Иметь возможность задавать node_env (development, production, test и т.д.) и влиять на конечный bundle через них.

  2. Иметь возможность задавать любые переменные окружения через конфиги и подключать нужные конфиги в зависимости от node_env.

  3. Настроить свои aliases для экспорта модулей.

  4. Подключить typescript, eslint, prettier.

  5. Подключить все необходимые транспайлеры.

  6. Добавить возможность динамического import react-компонентов.

  7. Оптимизировать все что можно и нужно.

  8. Настроить процессы коммитов по нашему git-flow через конфиг.

  9. Должно работать на всех платформах.

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

Название

Версия

Описание

react, react-dom

^17.*.*

Здесь без комментариев

react-router-dom

^5.*.*

Нам нужен роутинг, для удобства и связки в эпиках будем использовать вместе с connected-react-router ^6.*.*

redux

^4.*.*

Для нашего store

redux-observable

^1.*.*

Для обработки запросов

rxjs

^6.*.*

Для redux-observable и генерации событий

immer

8.*.*

Для иммутабельности store

reselect

^4.*.*

Для мемоизации store

styled-components

^5.*.*

Для генерации динамического css

react-virtualized

^9.*.*

Для виртуализации данных

react-intl

^3.*.*

Для поддержки нескольких локализаций

@loadable/component

^5.*.*

Для динамического импорта

react-final-form

^6.*.*

Для написания правильных пользовательских форм ввода

Константы webpack

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

// Modules
const path = require("path");
​
// Constants from path
const APP_DIR = process.cwd();
const ASSETS_FOLDER = "assets";
const SOURCE_FOLDER = "src";
const PUBLIC = "public";
const BUILD_FOLDER = "build";
const CACHE_FOLDER = ".cache";
const PREFIX_PATH = "@" // Это наш alias для каталога src
const PATH_TO_PROJECT = path.resolve(APP_DIR, SOURCE_FOLDER);
const PATH_TO_PUBLIC = path.resolve(APP_DIR, PUBLIC);
const PATH_TO_ASSETS = path.resolve(APP_DIR, ASSETS_FOLDER);
const PATH_TO_BUILD = path.resolve(APP_DIR, BUILD_FOLDER);
const PATH_TO_CACHE_FOLDER = path.resolve(APP_DIR, CACHE_FOLDER);
​
// Mode
const DEVELOPMENT = "development";
const PRODUCTION = "production";
const { NODE_ENV } = process.env;
const ENV_LIST = [DEVELOPMENT, PRODUCTION];
const VALID_NODE_ENV = NODE_ENV && ENV_LIST.includes(NODE_ENV)
 ? NODE_ENV : PRODUCTION; // Если node_env не валиден, заменяем своим дефолтом.
const IS_DEVELOPMENT = NODE_ENV === DEVELOPMENT;
const IS_PRODUCTION = NODE_ENV === PRODUCTION;
const IS_BUNDLE_ANALYZE = process.env.IS_BUNDLE_ANALYZE || false;   // Это для режима анализа бандла, о нем немного позже.
​
// OS
const OS = {
 IS_WINDOWS: process.platform === "win32",
};

Отдельно стоит выделить объект OS. Он нам понадобится при написании eslint конфига, к нему мы еще вернемся.

Конфиги и переменные окружения

Будем иметь в виду, что для каждой среды в package.json своя команда, в которой пробрасывается нужный env_variable.

"scripts": {
    "start": "cross-env NODE_ENV=development webpack serve --config ./webpack.config.js",
    "build": "cross-env NODE_ENV=production webpack --config ./webpack.config.js",
    "analyze-dev": "cross-env NODE_ENV=development cross-env webpack serve --config ./webpack.config.js",
  }

Также создадим конфиг в корне проекта для каждой из сред со следующим форматом имени:

.env.${node_env}

где node_env любая среда.

Если нужно создать шаблон, тогда добавляем еще нейминг, например:

.env.template.${node_env}

Прописываем его в .gitignore, и любой разработчик копипастит и переименовывает его по своему усмотрению.

Пример конфига:

NODE_ENV = development
HOST = localhost
PORT = 3000
VERSION = 1.0.0
CLIENT_ID = xxxx-xxxx-xxxx-xxxx

В него можно сразу добавить полезные переменные, например, HOST и PORT — для среды develop, чтобы webpack четко знал, где поднимать свой webpack-dev-server, а также API, чтобы знать, на какой адрес отправлять запросы клиента.

Webpack необходимо разбить архитектурно так, чтобы не испытывать муки во время поддержки. Нам должно быть только приятно или, как минимум, не больно. =)

Поэтому в корне проекта создаем каталог webpack и заводим в нем следующие каталоги:

  1. loaders — наши loaders (babel, sass, less и т.д.) для загрузки в корень конфига.

Пример loader:

const getBabelLoader = () => {
  const loader = {
    loader: "babel-loader",
    options: {
      configFile: path.join(APP_DIR, ".babelrc"),
   },
 };
​
  return loader;
};
​
module.exports = { getBabelLoader };

2. optimization — сюда будут входить модули оптимизации, которые не попадают под другие категории. Например, terser плагин указывает webpack, как сжимать код.

const TerserPlugin = require("terser-webpack-plugin");
​
const terser = () => {
  const plugin = new TerserPlugin({
    terserOptions: {
      parse: {
        ecma: 10,
     },
      compress: {
        ecma: 5,
        warnings: false,
        comparisons: false,
        booleans: true,
        collapse_vars: false,
        if_return: true,
        sequences: true,
        unused: true,
        conditionals: true,
        dead_code: true,
        evaluate: true
     },
      mangle: {
        safari10: true,
     },
      output: {
        beautify: false,
        ecma: 5,
        comments: false,
        ascii_only: true,
     },
   },
    parallel: true,
 });
​
  return plugin;
};
​
module.exports = { terser };

Основные параметры конфига:

  • ecma — стандарт JavaScript, по умолчанию 5 (ES5)

  • booleans — оптимизация условных выражений: !!a ? b : c → a ? b : c

  • comparisons — оптимизация логических выражений: a = !b && !c && !d && !e → a=!(b||c||d||e)

  • collapse_vars — выборочное удаление переменных var и const, которые не переиспользуются

  • if_return — оптимизация условий за счет return

  • sequences — объединение операторов через запятую

  • unused — удаление переменных и операторов, на которые отсутствуют ссылки (неиспользуемый код)

  • conditionals — применение оптимизации к условным выражениям

  • dead_code — удаление кода, который никогда не выполнится

  • evaluate — замена константных выражений результатом вычисления (если возможно)

С более полным описанием всех параметров можно ознакомиться тут.

  1. optimization-presets — здесь храним готовые наборы пресетов, полученные из пункта 2.

const getOptimizationMainPreset = () => {
  const optimization = {
    minimize: IS_PRODUCTION,
    minimizer: [
      terser(),
      cssAssets(),
   ],
    splitChunks: getSplitChunks(),
    runtimeChunk: 'single'
 };
​
  return optimization;
};
​
module.exports = { getOptimizationMainPreset };
  1. plugins — наборы плагинов для webpack.

'use-strict';
// Modules
const LoadablePlugin = require('@loadable/webpack-plugin');
// Constants
const {
  IS_PRODUCTION,
} = require('../utils/constants');
​
const getLoadableWebpackPlugin = () => {
​
  const plugin = new LoadablePlugin({
    filename: 'stats-loadable.json',
    writeToDisk: IS_PRODUCTION,
 });
​
  return plugin;
};
​
module.exports = { getLoadableWebpackPlugin };
  1. plugins-loader — лоадеры плагинов из пункта 4.

const getMainPluginsLoader = () => {
  return [
    getProgressBarPlugin(),
    getRefreshPlugin(),
    IS_PRODUCTION && getCleanWebpackPlugin(),
    getForkTsCheckerWebpackPlugin(),
    getDefinePlugin(),
 ].filter(Boolean);
};
​
module.exports = { getMainPluginsLoader };
  1. presets — готовые пресеты.

// Loaders
const { getStyleLoader } = require("../loaders/style-loader");
const { getCssLoader } = require("../loaders/css-loader");
const { getPostCssLoader } = require("../loaders/postcss-loader");
​
const getCssPreset = () => {
  const preset = {
    test: /\.css$/i,
    use: [
      getStyleLoader(),
      getCssLoader(),
      getPostCssLoader(),
   ],
 };
​
  return preset;
};
​
module.exports = { getCssPreset}
  1. utils — константы, дополнительные фичи для webpack.

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

Особое внимание уделяем aliases.

// Modules
const fs = require("fs");
const path = require("path");
// Constants
const { PATH_TO_PROJECT, PREFIX_PATH } = require("./constants");
​
const DEFAULT_ALIASES = {
 redux: "redux/src",
[PREFIX_PATH]: path.join(PATH_TO_PROJECT),
};
​
const getDirectories = () =>
 fs
  .readdirSync(PATH_TO_PROJECT, { withFileTypes: true })
  .filter(dirent => dirent.isDirectory())
  .map(dirent => dirent.name);
​
const reducer = (accumulator, currentValue) => {
 const key = `${PREFIX_PATH}${currentValue.toLowerCase()}`;
 const pathDirectory = path.join(PATH_TO_PROJECT, currentValue);
​
 accumulator[key] = pathDirectory;
 return accumulator;
};
​
const ALIASES_OBJECT = Object.assign(DEFAULT_ALIASES, getDirectories().reduce(reducer, {}));
​
module.exports = { ALIASES_OBJECT };

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

resolve: {
   alias: ALIASES_OBJECT,
   extensions: [".js", ".jsx", ".ts", ".tsx", ".json", ".mjs"],
   modules: [PATH_TO_PROJECT, "node_modules"],
   plugins: [PnpWebpackPlugin],
},

Или в eslint конфиг с расширением .js:

settings: {
   'import/extensions': ['.js', '.jsx', '.ts', '.tsx', '.json'],
   'import/resolver': {
     alias: {
       map: Object.entries(ALIASES_OBJECT),
       extensions: ['.js', '.jsx', '.ts', '.tsx', '.json'],
     },
   },
   react: {
     version: 'detect',
   },
 },

Далее стараемся привести наш проект внутри src к следующему виду:

// Redux

actions — каталог с redux-экшенами для импорта в react-компоненты и epics.

constants — каталог с redux-константами для импорта в actions, epics и reducers.

modeles — модели redux-store, это начальные состояния reducers, для импорта в reducers.

epics — эпики redux для обработки запросов и сохранения данных в store.

reducers — все reducers проекта, разбитые на каталоги в зависимости, например, от роутинга или раздела приложения.

selectors — каталоги selectors и structured-selectors для импорта в качестве state в react-компоненты.

api — rxjs.ajax объекты для импорта в epics с целью инициализации запросов.

redux — каталог для создания redux store, полного initial-store, root-reducer, и других middleware, например, root-epic.​

// React

app — каталог инициализации корня приложения, сюда прописываем все react providers, роутинг и т.д.

components — каталог с react-компонентами, которые часто переиспользуются в других проектах.

enhancers — сюда пишем все декораторы, которые мы можем комбинировать, например, с помощью compose из redux. Декоратор локали, прав доступа к разделам сайта и т.д.

hooks — для кастомных хуков react.

i18n — здесь храним json локалей.

layouts — декораторы-react-компоненты для других react-компонентов. Например, мы хотим, чтобы определенные разделы сайта имели меню. Описываем эти компоненты там и оборачиваем в них разделы.

lib (иногда helpers или utils) — методы, которые часто переиспользуются внутри react-компонентов. Например, генерация градиентов, обрезка текста и т.д.

modules — это каталог react-компонентов, которые переиспользуются только в layouts. Например, сам компонент menu.

routes — здесь храним все разделы, которые прописываем в роутинге приложения. Чуть позже познакомимся с тем, как создавать для них конфиги с динамическим импортом.​

// Other

theme — каталог тем для ui-kits или правил для styled-components (палитра, отступы, шрифты и т.д). Опишем его чуть позже.

utils — различные фичи, которые не нашли себе место в одном из каталогов.

Имея данную конфигурацию, можем обратиться, например, к каталогу redux:

import @redux/**/*'

или

import '@/redux/**/*'

При добавлении нового каталога в корень src просто перезапускаем webpack.

Получение данных из конфигов переменных окружения на этапе компиляции webpack

Выше мы уже создавали конфиги на примере .env.development. Проблема в том, что на этапе компиляции объект process.env из среды node о них ничего не знает. Он знает только те переменные, которые были переданы в качестве env в скрипты package.json. Например, здесь мы передаем NODE_ENV:

"start": "cross-env NODE_ENV=development webpack serve --config ./webpack.config.js",

Чтобы записать их в process.env, сначала считаем их из нужного нам конфига:

"use-strict";
// Utils
const {
 VALID_NODE_ENV,
 IS_DEVELOPMENT,
 IS_PRODUCTION,
} = require("./constants");
const { getConfig } = require("./get-config-file");
​
const DEFAULT_ENV = {
 NODE_ENV: VALID_NODE_ENV,
 IS_DEVELOPMENT: IS_DEVELOPMENT,
 IS_PRODUCTION: IS_PRODUCTION,
};
​
const getConfigEnv = (objectEnv = {}) => {
 const conf = getConfig();
​
 if (conf) {
   const resultConf = Object.keys(conf).reduce(
    (acc, key) => {
       const value = JSON.stringify(conf[key]);
​
       acc[key] = value;
       return Object.assign({}, acc);
    },
     Object.assign({}, objectEnv, DEFAULT_ENV),
  );
​
   return resultConf;
}
​
 return {};
};
​
module.exports = { getConfigEnv };

Теперь метод getConfigEnv возвращает все переменные окружения development конфига.

NODE_ENV = development
HOST = localhost
PORT = 3000
VERSION = 1.0.0
CLIENT_ID = xxxx-xxxx-xxxx-xxxx

Теперь их нужно записать в process.env. Создадим в разделе plugins такой метод:

// Modules
const webpack = require("webpack");
// Utils
const { getConfigEnv } = require("../utils/get-config-env");
​
const getDefinePlugin = () => {
 const plugin = new webpack.DefinePlugin({
   "process.env": getConfigEnv(),
});
​
 return plugin;
};
​
module.exports = { getDefinePlugin };

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

Дублирование пакетов:

При их установке наш пакетный менеджер добавит в проект не только сами пакеты, но и их зависимости. Часто бывает, что разные пакеты используют одни и те же зависимости различных версий. Чтобы знать о таких дублированиях, установим webpack-plugin:

// Modules
const DuplicatePackageCheckerPlugin =
  require('duplicate-package-checker-webpack-plugin');


const getDuplicatePackageCheckerPlugin = () => {
const plugin = new DuplicatePackageCheckerPlugin({
  verbose: true,
  emitError: true,
});
​
return plugin;
};
​
module.exports = { getDuplicatePackageCheckerPlugin };

Чтобы исправить конфликты, добавим нужный пакет и его версию в package.json в раздел resolutions, чтобы везде применялась одна и та же версия пакета.

"resolutions": {
 "get-intrinsic": "1.1.1",
}

Для того чтобы узнать, сколько версий пакета используется в приложении вручную, есть команда в терминале (на примере react):

npm ls react

Она выдаст дерево зависимостей, если версий больше одной.

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

"peerDependencies": {
   "react": ">=16.9.0",
   "react-dom": ">=16.9.0"
},

Это крайне важно для react, так как все пакеты проекта и сам проект должны использовать не только одну версию react, но и одну копию пакета. В противном случае будет ошибка.

В качестве сборщика вместо webpack можно использовать rollup. Например, на нем написано множество популярных пакетов, например, redux.

Конфиг babel.rc

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

  "plugins": [
    [
      "babel-plugin-styled-components",
      { "ssr": false, "pure": true }
    ],
    "@babel/plugin-syntax-dynamic-import",
    ["@babel/plugin-proposal-class-properties", { "loose": true }],
    "@babel/plugin-proposal-export-default-from",
    "@babel/plugin-proposal-optional-chaining",
    "@babel/plugin-proposal-unicode-property-regex",
    "@babel/plugin-transform-runtime"
  ],

  "env": {
    "development": {
      "plugins": [
        "react-refresh/babel"
      ]
    }
  }
}

Здесь перечислены все необходимые плагины для удобной работы с JavaScript.

Для обновления страницы при разработке мы применяем react-refresh.

Eslint конфиг

const path = require('path');
// Utils
const CONSTANTS = require('./webpack/utils/constants');
const { ALIASES_OBJECT } = require('./webpack/utils/aliases');
const { APP_DIR, OS } = CONSTANTS;
const { IS_WINDOWS } = OS;

​
module.exports = {
  parser: '@typescript-eslint/parser',
  parserOptions: {
    tsconfigRootDir: path.resolve(APP_DIR, 'src'),
    project: path.resolve(APP_DIR, './tsconfig.json'),
    createDefaultProgram: true,
    ecmaVersion: 2018,
    sourceType: 'module',
    ecmaFeatures: {
      jsx: true,
    },
  },
  env: {
    browser: true,
    es2020: true,
    node: true,
  },
  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:react/recommended',
    'plugin:react-hooks/recommended',
    'airbnb-typescript',
    'plugin:jsx-a11y/recommended',
    'plugin:import/typescript',
  ],
  plugins: [
    '@typescript-eslint',
    'import',
    'react',
    'optimize-regex',
    'prettier',
    'promise',
  ],
  rules: {
    'arrow-parens': 'off',
    'no-alert': 'error',
    '@typescript-eslint/explicit-function-return-type': 'off',
    '@typescript-eslint/ban-ts-comment': 'off',
    '@typescript-eslint/no-var-requires': 'off',
    '@typescript-eslint/lines-between-class-members': 'off',
    'linebreak-style': 0,
    'eslint linebreak-style': [0, 'error', IS_WINDOWS ? 'windows' : 'unix'],
    'import/prefer-default-export': 'off',
    'import/no-extraneous-dependencies': 'off',
    'no-console': 'off',
    'react/prop-types': 'off',
    'implicit-arrow-linebreak': ['error', 'below'],
    'no-param-reassign': 'off',
  },
  settings: {
    'import/extensions': ['.ts', '.tsx', '.json'],
    'import/resolver': {
      alias: {
        map: Object.entries(ALIASES_OBJECT),
        extensions: ['.ts', '.tsx', '.json'],
      },
    },
  },
  globals: {
    document: true,
    window: true,
    parent: true,
  },
};

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

Строка:

'eslint linebreak-style': [0, 'error', IS_WINDOWS ? 'windows' : 'unix'],

отвечает за кроссплатформенные решения переноса строк.

HTTPS webpack-dev-server

Иногда для имитации рабочей среды нужно локально открывать https-соединение. Чтобы такое осуществить, создадим в корне проекта каталог cert. Внутрь него положим 2 файла:

localhost.crt

localhost.key

Это самоподписанный сертификат и его публичный ключ.

Затем прописываем в webpack конфиге webpack-dev-server свойство https:

"use-strict";
// Modules
const fs = require("fs");
const path = require("path");
const dotenv = require("dotenv");
// Utils
const {
 APP_DIR,
 PATH_TO_ASSETS,
} = require("./constants");
​
const devConstants = dotenv.config({ path: "./.env.development" }).parsed;
​
const getDevServer = () => {
 const serverConf = {
   compress: true,
   static: [
     PATH_TO_ASSETS,
  ],
   firewall: false,
   historyApiFallback: true,
   hot: true,
   https: {
     key: fs.readFileSync(path.join(APP_DIR, '/cert/localhost.key')),
     cert: fs.readFileSync(path.join(APP_DIR, '/cert/localhost.crt')),
  },
   host: devConstants.HOST,
   open: true,
   port: devConstants.PORT,
};
​
 return serverConf;
};
​
module.exports = { getDevServer };

Коммиты, husky, конфиг

Наша задача автоматизировать коммиты. То есть привести их, например, к виду feature(my-project): [TASK-ID] my description, и чтобы это не занимало много времени у разработчиков.

Также все, что eslint и prettier могут автоматически исправить, мы перезапишем. Если исправить нельзя, выведем сообщение об ошибке и запретим делать коммит.

Для автоматизации процесса коммитов мы будем использовать:

husky: не ниже 6 версии

lint-staged, commitizen

Создаем в корне .czrc и прописываем путь к конфигу:

{
  "path": "./.commitizen/cz-config.js"
}

Создаем каталог .commitizen в корне проекта и в нем наш конфиг cz-config.js:

'use-strict';
​
const { configLoader } = require('commitizen');
const longest = require('longest');
const map = require('lodash/map');
const config = configLoader.load() || {};
const regExpJiraTicket = /^(ux|devops)-[1-9][0-9]{0,5}$/gi;
const options = {
 scope: 'courier-admin',
 maxCommitWidth: 50,
};

const choicesList = {
  feature: {
    title: 'feature',
    description: 'Новая задача',
  },
  'bug-fix': {
    title: 'bug-fix',
    description: 'Исправить баг',
  },
};

const length = longest(Object.keys(choicesList)).length + 1;

const choices = map(choicesList, (type, key) => ({
  name: `${`${key}:`.padEnd(length)} ${type.description}`,
  value: key,
}));


​
module.exports = {
  prompter(cz, setCommit) {
    cz.prompt([
      {
        type: 'list',
        name: 'type',
        message: 'Выберите тип коммита:',
        choices,
        default: config.defaultType,
      },
      {
        type: 'input',
        name: 'jiraTicket',
        message: 'Введите тег задачи в JIRA (например UX-123):',
        validate: value => {
          const trimValue = value.trim();

          if (!trimValue.length > 0) {
            return 'Обязательное поле.';
          }

          if (!regExpJiraTicket.test(trimValue)) {
            return 'Неверный формат.';
          }

          return true;
        },
        transformer: subject => subject.toUpperCase(),
        filter: subject => subject.trim().toUpperCase(),
      },
      {
        type: 'input',
        name: 'commit',
        message: `Краткое наименование коммита (не более ${options.maxCommitWidth}):\n`,
        validate: value => {
          const trimValue = value.trim();

          if (!trimValue.length > 0) {
            return 'Обязательное поле.';
          }

          if (trimValue.length > options.maxCommitWidth) {
            return 'Сократите коммит.';
          }

          return true;
        },
        filter: subject => subject.trim().toLowerCase(),
      },
    ])
      .then(answers => {
        const { type, jiraTicket, commit } = answers;
        const str = `${type} (${options.scope}): [${jiraTicket}] ${commit}`;

        setCommit(str);
      })
      .catch(error => {
        console.log(error);
      });
  },
};

Здесь все описываем согласно вашему gitflow. Пример выше демонстрирует основные возможности commitizen. Также мы можем брать уже готовые конфиги из npm.

Создаем каталог .husky в нем файл pre-commit

#!/bin/sh
. "$(dirname "$0")/_/husky.sh"
​
yarn lint-staged

В package.json в раздел scripts добавляем скрипт предустановки:

"prepare": "husky install",
"commit": "cz"

После этого на unix системах возможна ошибка в работе husky. Для её исправления необходимо дать рекурсивно права каталогу .husky на перезапись.

chmod -R +w ./.husky

При вводе в терминал команды

yarn cz

нас ждет короткий опрос, после которого будет запущена проверка линтером, и в случае успеха — коммит.

Заключение

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

Спасибо за внимание! Полезные материалы для разработчиков мы также публикуем в наших соцсетях – ВК и Telegram.

Теги:
Хабы:
Всего голосов 3: ↑2 и ↓1+1
Комментарии5

Публикации

Информация

Сайт
www.simbirsoft.com
Дата регистрации
Дата основания
Численность
1 001–5 000 человек
Местоположение
Россия