От мечты к провалу

как загубить фронтенд-проект

Алексей Золотых

инженер

консультант

тимлид

спикер

команда веб-редакторов

Чем губить

  1. Именованием
  2. DRY
  3. Фронтопсом
  4. Лишними маппингами
  5. Типами
  6. Незаконченными миграциями
  7. Сервером

Пролог

Бабочка взмахивает крыльями в Китае и колеблет воздух, и, в конце концов,
на Нью-Йорк обрушивается шторм

Эффект бабочки

Небольшие решения
— большие последствия

I

Про имена пакетов

Файлы

Редакторы

А что есть common!?

  • Авторизация
  • Общие компоненты
  • Веб-клиент
  • Какие-то виджеты

Не называй так

shared, common, utils, misc, stuff, tools, core, base, main, lib

Используй конкретные имена

Сеньорный подход

  • Семантика важна
  • Coupling and Cohesion не только в коде

II

Переиспользуем компоненты

Карточка товара


function ProductCard({ product }) {
  return (
    <div className="product-card">
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      <div className="price">
        <span className="current-price">${product.price}</span>
        {product.oldPrice && (
          <span className="old-price">${product.oldPrice}</span>
        )}
      </div>
      <button onClick={() => addToCart(product.id)}>
        В корзину
      </button>
    </div>
  );
}
20 строк кода
1 параметр (product)
30 секунд на понимание логики
3-5 тестовых случаев для полного покрытия

Компактный блок


function ProductCard({ product, layout = 'default' }) {
  const renderPrice = () => {
    if (layout === 'compact') {
      return <span className="price-compact">${product.price}</span>;
    }
    return (
      <div className="price">
        <span className="current-price">${product.price}</span>
        {product.oldPrice && (
          <span className="old-price">${product.oldPrice}</span>
        )}
      </div>
    );
  };

  const renderButton = () => {
    const buttonText = layout === 'minimal' ? 'Купить' : 'В корзину';
    return (
      <button onClick={() => addToCart(product.id)}>
        {buttonText}
      </button>
    );
  };

  return (
    <div className={`product-card product-card--${layout}`}>
      <img src={product.image} alt={product.name} />
      <h3>{product.name}</h3>
      {renderPrice()}
      {renderButton()}
    </div>
  );
}
50 строк кода (увеличение в 2.5 раза)
4 параметра
3 минуты на понимание
10 тестовых случаев

function ProductCard({
  product,
  layout = 'default',
  theme = 'light',
  showRating = true,
  showBadge = true,
  buttonAction = 'cart',
  priceFormat = 'standard'
}) {
  const getLayoutConfig = () => {
    const configs = {
      default: { imageSize: 'large', titleSize: 'h3', spacing: 'normal' },
      compact: { imageSize: 'medium', titleSize: 'h4', spacing: 'small' },
      minimal: { imageSize: 'small', titleSize: 'h5', spacing: 'tiny' },
      featured: { imageSize: 'xlarge', titleSize: 'h2', spacing: 'large' }
    };
    return configs[layout] || configs.default;
  };

  const renderPrice = () => {
    const formatters = {
      standard: () => renderStandardPrice(),
      compact: () => renderCompactPrice(),
      currency: () => renderCurrencyPrice()
    };
    return formatters[priceFormat]?.() || formatters.standard();
  };

  // ... остальная логика
}
150 строк кода
8 параметров конфигурации
15 минут на понимание логики
30+ тестовых случаев

Композиция и SRP


<Card>
  <CardImage src={product.image} />
  <CardTitle>{product.name}</CardTitle>
  <CardPrice price={product.price} oldPrice={product.oldPrice} />
  <CardButton onClick={() => addToCart(product.id)}>
    В корзину
  </CardButton>
</Card>

Может повторение не так и страшно?


<CompactProductCard product={product} />
<FeaturedProductCard product={product} />
<MinimalProductCard product={product} />

Может есть что-то помимо DRY

WET (Write Everything Twice) принцип

дублировать код до понимания реальных паттернов



AHA (Avoid Hasty Abstractions) принцип

золотая середина между DRY и WET

AHA (Avoid Hasty Abstractions) принцип
  • Дождитесь появления абстракции в трёх или более местах
  • Убедитесь, что паттерн действительно устойчив
  • Создавайте абстракции на основе реального опыта, а не предположений
  • Оптимизируйте код для изменений, а не для элегантности

Самый гибкий фреймворк — язык программирования

Сеньорный подход

  • Нужно уметь предсказывать
    • Понимать бизнес
    • Уметь жить в режиме неопределённости
  • Принципы переоценены

Если будет меняться — сделай гибче

Не будет меняться — копируй

И пусть весь мир подождёт...

III

Про настройки

Webpack


const path = require('path');

module.exports = {
  entry: './src/index.js',
  output: {
    path: path.resolve(__dirname, 'dist'),
    filename: 'bundle.js',
    clean: true,
  },
  mode: 'development',
  module: {
    rules: [
      {
        test: /\.(js|jsx)$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            presets: [
              '@babel/preset-env', '@babel/preset-react'
            ],
          },
        },
      },
      {
        test: /\.css$/,
        use: [ 'style-loader', 'css-loader' ],
      },
    ],
  },
  resolve: {
    extensions: ['.js', '.jsx'],
  },
  devServer: {
    static: path.join(__dirname, 'dist'),
    compress: true,
    port: 3000,
  },
};

— Сейчас у каждого свой конфиг! Давайте переиспользовать!

— Зачем?

— Вот нужно будет поменять...

Всё просто


module.exports = require('@my-company/configs/webpack.config');

— Мне нужно добавить плагин!

Всё просто


const commonConfig = require('@my-company/configs/webpack.config');

// свой плагин
module.exports = {
  ...commonConfig,
  plugins : [mySuperPlugin]
}

Всё просто


const commonConfig = require('@my-company/configs/webpack.config');

module.exports = {
  ...commonConfig,
  plugins : [
    ...commonConfig.plugins,
    mySuperPlugin
  ]
}

Добро пожаловать в ад!

— Некоторые плагины нужно перенастраивать

— У всех разные правила

— У всех разные серверы разработки

Что происходит дальше

— Нам нужен frontOPS!

— Пишем фреймворк на настройке конфигов

— Не меняй корень, непонятно где всё упадёт

Помните react-scripts?

Craco

allows you to get all of the benefits of Create React App without ejecting
CREATE
          REACT
          APP
          CONFIGURATION
          OVERRIDE

Вывод: выбери одно!

  • Переиспользовать и запрещать менять
  • Не переиспользовать

Сеньорный подход

  • Ownership ( делали бы вы это всё за свои деньги?)
  • «Одинаковость» не окупается
  • Умение договориться

IV

Про имена


type LoginData = {
  "login": "string",
  "password": "string"
}

На сервере


{
  "username": "aleksei.zolotykh",
  "password": "123456"
}

const login = ({login, password}) => {
  return fetch('/login', {
    body: JSON.stringify({username: login , password}
  })
}
login => username

const users = [
  {login: "admin", ...},
  {login: "superadmin", ...},
  {login: "zolotyh", ...}
]

Маппинг в массивах


{
  "login": "admin",
  "username": "admin,
}

users.map(({login, ...rest}) => {
  ...rest,
  username: login
})
Эта проблема может быть масштабнее
Проект может до 80% состоять из маппингов
Этой проблемы бы не было, если бы контракт был прописан вначале!

Сеньорный подход

  • Договориться о стандартах
  • Понимать все части приложения
  • DDD и Единый язык (Ubiquitous language)

openapi: 3.0.0
info:
  title: API Documentation
  version: 1.0.0
paths:
  /auth/login:
    post:
      summary: User login
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/LoginCredentials'
      responses:
        '200':
          description: Successful login
components:
  schemas:
    LoginCredentials:
      type: object
      required:
        - username
        - password
      properties:
        username:
          type: string
          description: User's username or email
          example: johndoe
        password:
          type: string
          description: User's password
          format: password
          example: securePassword123!
          writeOnly: true

V

Про долгие миграции

Разработчик

  • Я хочу уйти от Redux к Redux Saga
  • Вот прототип
  • Вот первая фича на Saga
  • Остальное по правилу туриста

Большая миграция через правило туриста

A scout leaves no trace… and tries to leave the world a little better than he found it
Когда будем переписывать — переведём на новый стейт-менеджер

Теперь у нас есть

  • Redux
  • Redux-saga
  • Redux-toolkit
  • RxJS

Надежда, что когда-нибудь это получится

Что нужно было сделать

  • Будет ли миграция оправдана?
  • Можете ли вы позволить себе миграцию?
  • Кто будет отвечать за миграцию?
  • Критерий окончания миграции

Сеньорный подход

  • Взять технический долг проще, чем в микрозаймах
  • Уметь доводить дела до конца
  • Ownership

VI

Про BFF и лишний сервер

Что такое BFF

            %%{init: {'theme':'neutral'}}%%
            graph TB
            subgraph "Традиционный подход (БЕЗ BFF)"
            Web1[Web приложение]
            Mobile1[Mobile приложение]
            Desktop1[Desktop приложение]

            Web1 --> API1[Общий API Gateway]
            Mobile1 --> API1
            Desktop1 --> API1

            API1 --> MS1[Микросервис Юзеры]
            API1 --> MS2[Микросервис Заказы]
            API1 --> MS3[Микросервис Продукты]
            API1 --> MS4[Микросервис Оплата]

            style API1 fill:#ffcccc
            end

            subgraph "BFF паттерн (С BFF)"
            Web2[Web приложение]
            Mobile2[Mobile приложение]
            Desktop2[Desktop приложение]

            Web2 --> BFF1[BFF для Web]
            Mobile2 --> BFF2[BFF для Mobile]
            Desktop2 --> BFF3[BFF для Desktop]

            BFF1 --> CoreMS1[Микросервис Юзеры]
            BFF1 --> CoreMS2[Микросервис Заказы]
            BFF1 --> CoreMS3[Микросервис Продукты]

            BFF2 --> CoreMS1
            BFF2 --> CoreMS2
            BFF2 --> CoreMS4[Микросервис Оплата]

            BFF3 --> CoreMS1
            BFF3 --> CoreMS2
            BFF3 --> CoreMS3
            BFF3 --> CoreMS4

            style BFF1 fill:#ccffcc
            style BFF2 fill:#ccffcc
            style BFF3 fill:#ccffcc
            end
          

SSR — это BFF

Мешает FCP и LCP

            %%{init: {'theme':'neutral'}}%%
            graph TB

            subgraph "400мс до старта загрузки"
            Web2[Web приложение]
            Mobile2[Mobile приложение]
            Desktop2[Desktop приложение]

            Web2 --> |100мс|BFF1[BFF для Web]
            Mobile2 --> BFF2[BFF для Mobile]
            Desktop2 --> BFF3[BFF для Desktop]

            BFF1 --> |300мс|CoreMS1[Микросервис Юзеры]
            BFF1 --> CoreMS2[Микросервис Заказы]
            BFF1 --> |100мс|CoreMS3[Микросервис Продукты]

            BFF2 --> CoreMS1
            BFF2 --> CoreMS2
            BFF2 --> CoreMS4[Микросервис Оплата]

            BFF3 --> CoreMS1
            BFF3 --> CoreMS2
            BFF3 --> CoreMS3
            BFF3 --> CoreMS4

            style BFF1 fill:#ccffcc
            style BFF2 fill:#ccffcc
            style BFF3 fill:#ccffcc
            linkStyle 0 stroke:#ff0000,stroke-width:2px
            linkStyle 3 stroke:#ff0000,stroke-width:2px
            linkStyle 5 stroke:#ff0000,stroke-width:2px
            end
          

Сервер требует поддержки

  • Мониторинг
  • Алертинг
  • Логирование
  • Деплой
  • Трейсинг
  • Иногда замедляет

Оптимизация в разработке

Оптимизация в разработке

Оптимизация в разработке

Оптимизация в разработке

Оптимизация в разработке

Оптимизация в разработке

Оптимизация в разработке

Оптимизация в продакшене

Оптимизация в продакшене

Выводы

  • Если можно без сервера, то лучше без сервера
  • Мониторим в проде
  • Считаем

Сеньорный подход

  • Узнаём НФТ, считаем ресурсы
  • Данные — наше всё
  • Скептическое мышление

Эпилог

  • Все хотели сделать хорошо
  • Весь сеньорный раздел был про софтскиллы
    • Понимание бизнеса
    • Работа с неопределённостью
    • Ownership и ответственность
    • Умение доводить дела до конца
    • Навыки переговоров и согласования
    • Скептическое мышление

Спасибо!

Вопросы

@zolotyh