Привет, меня зовут Грант, я фронтенд-разработчик в KTS. В этой статье я хочу поделиться опытом заведения проектов от нашей команды.

В отделе рекламных спецпроектов мы запускаем большое количество проектов, поэтому скорость сетапа имеет для нас большое значение. В среднем мы сетапим по три проекта в месяц. Их фронтовый стек основан на React, Styled Components или связке CSS modules с SCSS, MobX и Axios, а если это мини-приложение для ВКонтакте, то к списку добавляются еще VKUI и библиотека VK-Bridge для VK API.

Раньше сборка такого стека занимала у нас довольно много времени, но со временем мы научились настраивать его в два счета. Собственно, в статье пойдет речь о том, как создать инструмент для быстрого сетапа проектов. Сразу приведу ссылку на репозиторий с полным примером этой утилиты, упрощенную версию которой рассмотрим в статье: create-mediaproject-example.

Оглавление

Как мы сетапили проекты раньше

Чаще всего мы пользовались тайной техникой сtrl+c, ctrl+v — просто брали самый свежий из готовых проектов и копировали его в новый. Далее очищали проект-донор от всего ненужного: лишних компонентов, утилит, файлов конфигураций. Добавляли UI Kit нового проекта, настраивали шрифты, подготавливали базовую логику.

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

В то же время при сетапе нового проекта приходилось учитывать наличие структуры из сервисных компонентов, например, привычной логики для навигации на базе React Router. Кроме того, для мини-приложений ВКонтакте нужно было также учесть базовую архитектуру под VKUI и некоторую логику для удобства работы с VK API, особенно неофициальные «секретные» варианты.

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

Как мы искали альтернативу

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

Тестили мы и наиболее популярное решение Create React App, и менее известные варианты вроде шаблонного проекта Create Vite. Они не подошли по двум причинам.

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

  2. Create React App оказался довольно неповоротливым в плане настройки проекта под свои нужды. Например, нам периодически приходится вносить изменения в конфиг сборщика. В CRA конфиг Webpack изначально скрыт, и чтобы что-то настроить, приходится либо выполнять команду eject, либо подключать дополнительные решения вроде react-app-rewired.

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

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

Как мы создали инструмент быстрого сетапа

Общая структура нашего инструмента выглядит следующим образом:

create-mediaproject-example
├── src
│  ├── templates                         папка с шаблонами
│  │  ├── temp1                          один из шаблонов
│  │  │  ├── ...                         
│  │  │  ├── package.template.json
│  │  │  ├── README.template.md
│  │  │  └── webpack.config.template.ts
│  │  └── ...
│  ├── ...
│  ├── createProject.ts                  файлы утилит создания 
│  └── index.ts                          нового проекта
├── ...
├── package.json                         конфиг пакета инструмента быстрого
└── README.md                            сетапа проектов с командами запуска

Рассмотрим на примере проекта для мини-приложений ВКонтакте. Его шаблон (src/templates/vk-mini-app) содержит ряд следующих зависимостей:

  • библиотека для работы с компонентами (React);

  • UI‑библиотека (VKUI);

  • библиотека для работы с VK API (VK‑Bridge);

  • библиотека для роутинга (React‑Router);

  • управление логикой состояний (MobX);

  • работа с запросами (Axios);

  • собственные библиотеки миксинов SCSS, утилит для работы с VK API, набор часто используемых сторов, и т. д.

Также в нем есть свои dev‑зависимости — инструменты разработки:

  • сборщик проекта (Webpack);

  • линтеры (Eslint, Stylelint, Prettier);

  • расширения языков (Typescript, SCSS, Styled‑Components);

  • набор конфигов для этих инструментов;

  • файлы с настройками раскладки проекта.

Для самого же инструмента быстрого сетапа (src/index.ts) мы используем три утилиты.

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

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

  3. Утилита пост‑обработки скопированного проекта. Она будет инициализировать git‑репозиторий и устанавливать зависимости проекта.

Как можно упростить реализацию утилит быстрого сетапа

Стоит сказать, что вам необязательно настраивать свой инструмент точь-в-точь по нашим лекалам. Если вы работаете не над мини-приложениями для ВКонтакте, вы можете сделать сетап несколько проще. Давайте разберемся, каким образом.

Точкой входа, запускающей процесс создания нового проекта по шаблону, будет выступать файл src/index.ts:

src/index.ts
import { parseOptions } from "./parseOptions";
import { createProject } from "./createProject";
import { postProcess } from "./postProcess";
import { printError, printSuccess } from "./utils/print";
import { MESSAGES } from "./config";

const main = async (): Promise<void> => {
  // получает от юзера название нового проекта и выбранный шаблон
  const options = await parseOptions();

  try {
    // создает папку нового проекта и копирует файлы выбранного шаблона
    createProject(options);
    // инициализирует гит-репозиторий и устанавливает зависимости
    postProcess(options);

    printSuccess(
      MESSAGES.projectCreateSuccess(
	      options.templateName, 
	      options.buildDir
	    )
    );
  } catch {
    printError(MESSAGES.projectCreateError);
  }
};

main();

Первым шагом при создании нового проекта необходимо определиться с его названием и выбрать шаблон. Утилита src/parseOptions.ts будет последовательно запрашивать у пользователя эти данные. Здесь используется библиотека Inquirer.js, которая предоставляет удобный набор компонентов для ввода с помощью интерфейса командной строки:

src/parseOptions.ts
import * as fs from 'fs';
import * as path from 'path';

import { prompt } from 'inquirer';

const TEMPLATES_DIR_NAME = 'templates';

// тип результата с выбором пользователя
export type OptionsType = {
  // имя шаблона
  templateName: string;
  // путь к его папке
  templatePath: string;
  // название создаваемого проекта (и его папки)
  buildDir: string;
};

// утилита ввода из командной строки данных от пользователя
export const parseOptions = async (): Promise<OptionsType> => {
  // получаем путь к папке с шаблонами
  const templatesPath = path.join(__dirname, TEMPLATES_DIR_NAME);
  // получаем список имен шаблонов внутри этой папки
  const choices = fs.readdirSync(templatesPath);

  // подготавливаем конфиги с учетом списка шаблонов
  const questions = [
    promptQuestions.templateName(choices),
    promptQuestions.buildDir,
  ];

  // запросы в командную строку и получение ответов от юзера
  const { templateName, buildDir } = await prompt(questions);

  // результат в виде выбранных юзером опций
  return {
    buildDir,
    templateName,
    templatePath: path.join(templatesPath, templateName),
  };
};

// валидатор имени папки нового проекта
const validateDirectoryName = (dirName: string): true | string => {
 return /^([A-Za-z\\-_\\d])+$/.test(dirName) || 'Incorrect directory name';
};

// конфиг с вопросами (в поле message) для выбора опций при создании проекта
const promptQuestions = {
  templateName: (choices: string[]) => ({
    name: 'templateName',
    type: 'list',
    message: 'Select template',
    choices,
  }),
  buildDir: {
    name: 'buildDir',
    type: 'input',
    message: 'Project directory (also name)',
    validate: validateDirectoryName,
};

После выбора названия и шаблона для будущего проекта в дело вступит утилита src/createProject.ts. Она скопирует структуру и контент шаблона в новый проект и трансформирует определенные файлы с помощью шаблонизатора (в нашем случае это EJS):

src/createProject.ts
import * as fs from "fs";
import * as path from "path";

import * as ejs from "ejs";

import { MESSAGES } from "./config";
import { printError } from "./utils/print";
import { OptionsType } from "./parseOptions";

// файлы из списка не будут копироваться в новый проект из шаблонов
export const SKIP_FILENAMES = [".DS_Store", ".git", "node_modules"];

// при наличии в имени файла такого расширения нужно обработать его шаблонизатором
const TEMPLATE_REGEXP = /\\.template/;
const isTemplate = (name: string) => Boolean(name.match(TEMPLATE_REGEXP));

// утилита принимает опции, выбранные юзером, и собирает новый проект
export const createProject = (options: OptionsType): void => {
  // если папка с тем же именем уже существует, сообщаем юзеру и завершаем выполнение
  if (fs.existsSync(options.buildDir)) {
    printError(MESSAGES.buildDirExists);
    throw new Error(MESSAGES.buildDirExists);
  }

  // создаем папку с именем нового проекта
  fs.mkdirSync(options.buildDir);
  // наполняем проект по выбранному шаблону
  createDirectoryContents(options);
};

// утилита, принимающая выбранные юзером опции и собирающая новый проект
const createDirectoryContents = (
  options: OptionsType,
  relativePath = ""
): void => {
  const buildRelativePath = (parentPath: string) =>
    path.join(parentPath, relativePath);

  // получаем полный путь до текущей папки в новом проекте
  const fullProjectPath = buildRelativePath(options.buildDir);
  // получаем полный путь до текущей папки в шаблоне
  const fullTemplatePath = buildRelativePath(options.templatePath);
  // получаем список файлов в текущей папке шаблона для переноса в проект
  const entriesToCreate = fs.readdirSync(fullTemplatePath);

  // обрабатываем список файлов в текущей папке шаблона
  entriesToCreate.forEach((entryName) => {
    // пропускаем ненужные файлы
    if (SKIP_FILENAMES.includes(entryName)) {
      return;
    }

    // получаем полный путь до этого файла в шаблоне
    const entryPath = path.join(fullTemplatePath, entryName);

    // если файл с этим именем - это не папка, то сохраняем его содержимое в проект
    if (fs.statSync(entryPath).isFile()) {
      return saveFile({
        options,
        entryPath,
        entryName,
        projectBuildPath: fullProjectPath,
      });
    }

    // иначе создаем папку с таким именем в проекте
    fs.mkdirSync(path.join(fullProjectPath, entryName));
    // и рекурсивно обрабатываем ее содержимое
    createDirectoryContents(options, path.join(relativePath, entryName));
  });
};

type SaveFileProps = {
  // опции, выбранные юзером
  options: OptionsType,
  // полный путь до файла в шаблоне проекта
  entryPath: string,
  // название файла
  entryName: string,
  // путь, по которому файл лежит в папке с проектом
  projectBuildPath: string,
};

// функция обработки содержимого файла шаблона и копирования его в проект
const saveFile = ({
  options,
  entryName,
  entryPath,
  projectBuildPath,
}: SaveFileProps) => {
  // считываем данные файла
  let dataToWrite = fs.readFileSync(entryPath, "utf-8");

  // если файлу нужна обработка шаблонизатором, то пропускаем его через EJS
  if (isTemplate(entryName)) {
    dataToWrite = ejs.render(dataToWrite, {
      // так EJS заменит в строке dataToWrite
      // все включения <%= PROJECT_NAME %>
      // на значение в опции buildDir (т.е. на выбранное юзером имя проекта)
      PROJECT_NAME: options.buildDir,
    });
  }

  // очищаем имя файла от возможной подстроки .template для финального вида
  const writeFileName = entryName.replace(TEMPLATE_REGEXP, "");
  // собираем путь до этого файла в новом проекте
  const fullFilePath = path.join(projectBuildPath, writeFileName);
  // получаем параметр файла в папке шаблона
  const { mode } = fs.statSync(entryPath);
  // сохраняем файл в новый проект с тем же mode,
  // чтобы выполняемые файлы остались выполняемыми
  fs.writeFileSync(fullFilePath, dataToWrite, { mode });
};

Завершать создание нового проекта будет утилита src/postProcess.ts. Она инициирует git-репозиторий и устанавливает зависимости проекта. Здесь использована библиотека shelljs для удобства запуска внешних утилит:

src/postProcess.ts
import * as fs from "fs";

import { cd, exec, ShellString } from "shelljs";

import { MESSAGES } from "./config";
import { OptionsType } from "./parseOptions";

// утилита принимает опции, выбранные юзером, и завершает создание проекта
export const postProcess = (options: OptionsType): void => {
  // переходим в папку проекта
  cd(options.buildDir);

  // инициализируем гит-репозиторий и устанавливаем зависимости
  initGit();
  createGitIgnoreFile();
  installDeps();
};

export const INITIAL_GIT_BRANCH = "main";
const FILES_GIT_SHOULD_IGNORE = [
  "*.log",
  "node_modules/",
  "public/",
  "dist/",
  ".idea/",
  ".vscode/",
  ".DS_Store",
];

// инициализация гит-репозитория
const initGit = (branch = INITIAL_GIT_BRANCH): ShellString =>
  exec(`git init --initial-branch ${branch}`);

// создание файла .gitignore
const createGitIgnoreFile = (): void => {
  // для каждой строки выполняется отдельная команда echo
  // такая реализация нужна для совместимости со всеми ОС
  const commands = FILES_GIT_SHOULD_IGNORE.map((name) => `echo ${name}`);

  exec(`(${commands.join(" && ")}) > '.gitignore'`);
};

// установка зависимостей с помощью утилиты yarn
export const installDeps = (): void => {
  if (!fs.existsSync("package.json")) {
    return;
  }

  const result = exec("yarn install");

  if (result.code !== 0) {
    throw new Error(MESSAGES.yarnInstallFail);
  }
};

Как работает инструмент быстрого сетапа

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

  • выбор шаблона для нового проекта;

  • создание папки для нового проекта и копирование файлов шаблона в нее;

  • инициализация системы контроля версий (git);

  • установка зависимостей проекта (yarn).

Для демонстрации создадим новый проект для мини-аппа ВКонтакте:

С какими проблемами мы столкнулись

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

После создания инструмента быстрого сетапа мы начали добавлять в него новые шаблоны. В частности, среди них был шаблон SPA‑приложения для Web‑проектов. В первом шаблоне для сборки приложения использовался Webpack, но чтобы идти в ногу со временем, мы решили заодно попробовать Vite — он несколько проще в настройке. Да, его возможностей может не хватать, но с ним значительно ускоряется процесс сборки.

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

Еще при создании новых шаблонов или доработках существующих у нас возникали неудобства в процессе разработки. Дело в том, что шаблоны не содержат установленных библиотек для своих внешних зависимостей. Если открыть такой шаблон в IDE, то работать с Typescript будет попросту неудобно: типы сущностей этих библиотек окажутся неопределены, что может породить кучу ошибок.

Чтобы сделать разработку удобнее, нужно сначала создать из шаблона новый проект, и уже его открывать в IDE. Кроме того, такой проект можно запустить в dev‑режиме его сборщика, и определенные изменения станут видны в браузере в реальном времени. После доработок нужно не забыть аккуратно перенести все изменения обратно в шаблон, что тоже отнимает некоторое время, особенно если весь процесс приходится повторять несколько раз.

Для решения этой проблемы мы разработали свой «dev‑режим» для утилит инструмента быстрого сетапа. При запуске через него шаблон собирается во временную папку тем же способом, что и в обычном режиме: копируются файлы, инициализируется git и устанавливаются зависимости. Далее папка с внешними библиотеками из этого проекта (node_modules) линкуется в папку этого шаблона. Это позволяет IDE в открытом шаблоне видеть типы всех зависимостей.

Следующим шагом запускается специальная утилита на базе библиотеки chokidar. Она следит за изменениями в файлах шаблона и транслирует их в разложенный проект, создавая, сохраняя или удаляя редактируемые файлы и папки. Параллельно созданный временный проект также автоматически запускается в dev‑режиме своего сборщика, что позволяет сразу открыть его в браузере и видеть правки в реальном времени. Таким образом можно редактировать файлы прямо в шаблоне, и IDE корректно помогает в работе, а в конце временный проект просто удаляется.

Чего мы добились в итоге

С помощью разработанного инструмента мы закрыли задачи по сетапу примерно для 70% проектов отдела. Среднее время сетапа нового проекта сократилось в 4 раза — до 30 минут. Мы получили быстрый способ добавления новых шаблонов и избавились от необходимости искать по старым проектам артефакты и «секретную логику» работы с VKUI.

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

А если вам интересно, как еще можно упростить работу с фронтендом, читайте другие наши статьи на эту тему:

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


  1. i360u
    17.07.2024 13:17
    +1

    Мы для этих же целей используем template repository. А для разных шаблонов - ветки. Таким образом, развертывание нового проекта, со всеми необходимыми настройками, занимает одну минуту и нет необходимости ни в каких дополнительных специальных тулзах.


  1. SUNsung
    17.07.2024 13:17

    Более 10 лет писательства на разных языках и в проектах разного уровня сложности. Все эти скрипты - это точка отказа. У кого то не так стало, у кого то другая версия и тд. Самое главное оно может кливо отработать.
    Самое оптимальное это Shell-скрипты. Для быстрой развертки или "клонирования" из готового примера такое идеально.
    Как показала практика даже с питоном могут быть проблемы на сотню разных пользователей. А вот Shell-скрипт работает абсолютно на всах линуксах и маках, не говоря за контейнеры. И самое гланое не нужно ничего ставить предварительно или проверять "версию" чего либо у пользователя.


    1. vvzvlad
      17.07.2024 13:17

      А вот Shell-скрипт работает абсолютно на всах линуксах и маках, не говоря за контейнеры

      Ха-ха-хахах. Да-да.


    1. ahdenchik
      17.07.2024 13:17

      А вот Shell-скрипт работает абсолютно на всах линуксах и маках

      Если речь про bash то точно нет - Apple что-то не понравилось в новой лицензии bash (GPL v3) поэтому они оставили у себя старую некроту, под которую отдельно нужно затачивать скрипты. Сходу: .SHELLSTATUS и let она не поддерживает