как загубить фронтенд-проект
инженер
консультант
тимлид
спикер
Бабочка взмахивает крыльями в Китае и колеблет воздух, и, в конце концов,
на Нью-Йорк обрушивается шторм
Небольшие решения
— большие последствия
shared, common, utils, misc,
stuff, tools,
core, base, main, lib
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+ тестовых случаев |
<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
Если будет меняться — сделай гибче
Не будет меняться — копируй
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!
— Пишем фреймворк на настройке конфигов
— Не меняй корень, непонятно где всё упадёт
allows you to get all of the benefits of Create React App without ejecting
CREATE REACT APP CONFIGURATION OVERRIDE
type LoginData = {
"login": "string",
"password": "string"
}
{
"username": "aleksei.zolotykh",
"password": "123456"
}
const login = ({login, password}) => {
return fetch('/login', {
body: JSON.stringify({username: login , password}
})
}
const users = [
{login: "admin", ...},
{login: "superadmin", ...},
{login: "zolotyh", ...}
]
{
"login": "admin",
"username": "admin,
}
users.map(({login, ...rest}) => {
...rest,
username: login
})
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
A scout leaves no trace… and tries to leave the world a little better than he found it
Надежда, что когда-нибудь это получится
%%{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