Created
October 7, 2025 15:45
-
-
Save zolotyh/2b47c78eda3ef3626e33aef458b4e1ae to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| <!doctype html> | |
| <html lang="en"> | |
| <head> | |
| <meta charset="utf-8" /> | |
| <meta | |
| name="viewport" | |
| content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" | |
| /> | |
| <title>reveal.js</title> | |
| <link rel="stylesheet" href="dist/reset.css" /> | |
| <link rel="stylesheet" href="dist/reveal.css" /> | |
| <link rel="stylesheet" href="dist/theme/black.css" /> | |
| <!-- Theme used for syntax highlighted code --> | |
| <link rel="stylesheet" href="plugin/highlight/monokai.css" /> | |
| </head> | |
| <body> | |
| <div class="reveal"> | |
| <div class="slides"> | |
| <section> | |
| Эффект бабочки | |
| <aside class="notes"> | |
| <ul> | |
| <li> | |
| Фильм эффект бабочки операется на вполне себе научное | |
| размышление | |
| </li> | |
| <li> | |
| Мы принимаем вроде бы незначительные решения, решения влияют на | |
| проект | |
| </li> | |
| <li>Иногда непринятие решение, это тоже решение</li> | |
| </ul> | |
| </aside> | |
| </section> | |
| <section></section> | |
| <section>Старые слайды</section> | |
| <section> | |
| Про что доклад | |
| <aside class="notes"> | |
| <ul> | |
| <li> | |
| Почему нельзя слепо доверять принципам написания хорошего кода | |
| </li> | |
| </ul> | |
| </aside> | |
| </section> | |
| <section> | |
| <h2 class="r-fit-text">Переиспользуй это</h2> | |
| </section> | |
| <section> | |
| Создаём универсальную карточку для отображения клиентов, заказов и | |
| продуктов | |
| </section> | |
| <section> | |
| <pre><code data-trim class="typescript"> | |
| interface DataCardProps { | |
| title: string; | |
| description: string; | |
| type: 'client' | 'order' | 'product'; | |
| onClick?: () => void; | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code class="typescript" data-trim> | |
| function DataCard({ title, description, type, onClick }: DataCardProps) { | |
| const typeStyles = { | |
| client: 'border-l-4 border-blue-500 bg-blue-50', | |
| order: 'border-l-4 border-green-500 bg-green-50', | |
| product: 'border-l-4 border-purple-500 bg-purple-50', | |
| }; | |
| return ( | |
| <div | |
| className={`p-4 rounded shadow-md cursor-pointer ${typeStyles[type]}`} | |
| onClick={onClick} | |
| > | |
| <h3 className="text-lg font-semibold">{title}</h3> | |
| <p className="text-gray-600">{description}</p> | |
| </div> | |
| ); | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <p> | |
| Бизнес требует добавить действия, статусы и поддержку кастомных | |
| иконок | |
| </p> | |
| </section> | |
| <section> | |
| <pre><code data-trim class="typescript"> | |
| interface DataCardPropsV2 extends DataCardProps { | |
| icon?: React.ReactNode; | |
| status?: 'active' | 'inactive' | 'pending'; | |
| actions?: { label: string; action: () => void }[]; | |
| className?: string; | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code data-trim class="typescript" data-line-numbers> | |
| function DataCardV2({ | |
| title, | |
| description, | |
| type, | |
| onClick, | |
| icon, | |
| status, | |
| actions, | |
| className, | |
| }: DataCardPropsV2) { | |
| const typeStyles = { | |
| client: 'border-l-4 border-blue-500 bg-blue-50', | |
| order: 'border-l-4 border-green-500 bg-green-50', | |
| product: 'border-l-4 border-purple-500 bg-purple-50', | |
| }; | |
| const statusStyles = { | |
| active: 'text-green-600', | |
| inactive: 'text-red-600', | |
| pending: 'text-yellow-600', | |
| }; | |
| return ( | |
| <div | |
| className={`p-4 rounded shadow-md ${typeStyles[type]} ${className || ''}`} | |
| onClick={onClick} | |
| > | |
| <div className="flex items-center"> | |
| {icon && <span className="mr-2">{icon}</span>} | |
| <h3 className="text-lg font-semibold">{title}</h3> | |
| </div> | |
| <p className={`text-gray-600 ${status ? statusStyles[status] : ''}`}> | |
| {description} | |
| </p> | |
| {actions && ( | |
| <div className="mt-2 flex gap-2"> | |
| {actions.map((action, index) => ( | |
| <button | |
| key={index} | |
| className="text-sm text-blue-500 hover:underline" | |
| onClick={(e) => { | |
| e.stopPropagation(); // Предотвращаем вызов onClick карточки | |
| action.action(); | |
| }} | |
| > | |
| {action.label} | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| </code></pre> | |
| </section> | |
| <section>Проблемы начинаются</section> | |
| <section> | |
| Добавляются поддержка метаданных, аналитики и кастомизации для разных | |
| отделов | |
| </section> | |
| <section> | |
| <pre><code data-trim data-line-numbers> | |
| interface DataCardPropsV3 extends DataCardPropsV2 { | |
| metadata?: Record<string, string>; | |
| trackEvent?: string; | |
| layout?: 'compact' | 'detailed'; | |
| isDraggable?: boolean; | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code data-trim data-line-numbers> | |
| function DataCardV3({ | |
| title, | |
| description, | |
| type, | |
| onClick, | |
| icon, | |
| status, | |
| actions, | |
| className, | |
| metadata, | |
| trackEvent, | |
| layout = 'detailed', | |
| isDraggable, | |
| }: DataCardPropsV3) { | |
| const typeStyles = { | |
| client: 'border-l-4 border-blue-500 bg-blue-50', | |
| order: 'border-l-4 border-green-500 bg-green-50', | |
| product: 'border-l-4 border-purple-500 bg-purple-50', | |
| }; | |
| const statusStyles = { | |
| active: 'text-green-600', | |
| inactive: 'text-red-600', | |
| pending: 'text-yellow-600', | |
| }; | |
| const layoutStyles = { | |
| compact: 'p-2 text-sm', | |
| detailed: 'p-4 text-base', | |
| }; | |
| const handleClick = () => { | |
| if (trackEvent) { | |
| // Отправка события в аналитику (имитация) | |
| console.log(`Track event: ${trackEvent}`); | |
| } | |
| onClick?.(); | |
| }; | |
| return ( | |
| <div | |
| className={`rounded shadow-md ${typeStyles[type]} ${layoutStyles[layout]} ${className || ''} ${ | |
| isDraggable ? 'cursor-move' : 'cursor-pointer' | |
| }`} | |
| onClick={handleClick} | |
| draggable={isDraggable} | |
| onDragStart={isDraggable ? (e) => e.dataTransfer.setData('text/plain', title) : undefined} | |
| > | |
| <div className="flex items-center"> | |
| {icon && <span className="mr-2">{icon}</span>} | |
| <h3 className="text-lg font-semibold">{title}</h3> | |
| </div> | |
| <p className={`text-gray-600 ${status ? statusStyles[status] : ''}`}> | |
| {description} | |
| </p> | |
| {metadata && ( | |
| <div className="mt-2 text-sm text-gray-500"> | |
| {Object.entries(metadata).map(([key, value]) => ( | |
| <div key={key}> | |
| <span className="font-medium">{key}:</span> {value} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {actions && ( | |
| <div className="mt-2 flex gap-2"> | |
| {actions.map((action, index) => ( | |
| <button | |
| key={index} | |
| className="text-sm text-blue-500 hover:underline" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| action.action(); | |
| }} | |
| > | |
| {action.label} | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| </code></pre> | |
| </section> | |
| <section>Хаос и технический долг</section> | |
| <section> | |
| Компонент обрастает новыми фичами: поддержка тем, мультимедиа, | |
| локализация и асинхронные действия | |
| </section> | |
| <section> | |
| <pre><code data-trim data-line-numbers> | |
| interface DataCardPropsV4 extends DataCardPropsV3 { | |
| theme?: 'light' | 'dark'; | |
| media?: { type: 'image' | 'video'; url: string }; | |
| locale?: 'en' | 'ru' | 'es'; | |
| asyncAction?: () => Promise<void>; | |
| isSelectable?: boolean; | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code data-trim data-line-numbers> | |
| function DataCardV4({ | |
| title, | |
| description, | |
| type, | |
| onClick, | |
| icon, | |
| status, | |
| actions, | |
| className, | |
| metadata, | |
| trackEvent, | |
| layout = 'detailed', | |
| isDraggable, | |
| theme = 'light', | |
| media, | |
| locale = 'en', | |
| asyncAction, | |
| isSelectable, | |
| }: DataCardPropsV4) { | |
| const typeStyles = { | |
| client: 'border-l-4 border-blue-500 bg-blue-50', | |
| order: 'border-l-4 border-green-500 bg-green-50', | |
| product: 'border-l-4 border-purple-500 bg-purple-50', | |
| }; | |
| const statusStyles = { | |
| active: 'text-green-600', | |
| inactive: 'text-red-600', | |
| pending: 'text-yellow-600', | |
| }; | |
| const layoutStyles = { | |
| compact: 'p-2 text-sm', | |
| detailed: 'p-4 text-base', | |
| }; | |
| const themeStyles = { | |
| light: 'bg-opacity-90', | |
| dark: 'bg-gray-800 text-white bg-opacity-80', | |
| }; | |
| const localeContent = { | |
| en: { view: 'View', edit: 'Edit' }, | |
| ru: { view: 'Просмотр', edit: 'Редактировать' }, | |
| es: { view: 'Ver', edit: 'Editar' }, | |
| }; | |
| const handleClick = async () => { | |
| if (trackEvent) { | |
| console.log(`Track event: ${trackEvent}`); | |
| } | |
| if (asyncAction) { | |
| try { | |
| await asyncAction(); | |
| } catch (error) { | |
| console.error('Async action failed:', error); | |
| } | |
| } | |
| onClick?.(); | |
| }; | |
| return ( | |
| <div | |
| className={`rounded shadow-md ${typeStyles[type]} ${layoutStyles[layout]} ${themeStyles[theme]} ${ | |
| isSelectable ? 'ring-2 ring-blue-300' : '' | |
| } ${className || ''} ${isDraggable ? 'cursor-move' : 'cursor-pointer'}`} | |
| onClick={handleClick} | |
| draggable={isDraggable} | |
| onDragStart={isDraggable ? (e) => e.dataTransfer.setData('text/plain', title) : undefined} | |
| > | |
| {media && ( | |
| <div className="mb-2"> | |
| {media.type === 'image' ? ( | |
| <img src={media.url} alt={title} className="w-full h-32 object-cover rounded" /> | |
| ) : ( | |
| <video src={media.url} controls className="w-full h-32 rounded" /> | |
| )} | |
| </div> | |
| )} | |
| <div className="flex items-center"> | |
| {icon && <span className="mr-2">{icon}</span>} | |
| <h3 className="text-lg font-semibold">{title}</h3> | |
| </div> | |
| <p className={`text-gray-600 ${status ? statusStyles[status] : ''}`}> | |
| {description} | |
| </p> | |
| {metadata && ( | |
| <div className="mt-2 text-sm text-gray-500"> | |
| {Object.entries(metadata).map(([key, value]) => ( | |
| <div key={key}> | |
| <span className="font-medium">{key}:</span> {value} | |
| </div> | |
| ))} | |
| </div> | |
| )} | |
| {actions && ( | |
| <div className="mt-2 flex gap-2"> | |
| {actions.map((action, index) => ( | |
| <button | |
| key={index} | |
| className="text-sm text-blue-500 hover:underline" | |
| onClick={(e) => { | |
| e.stopPropagation(); | |
| action.action(); | |
| }} | |
| > | |
| {localeContent[locale][action.label.toLowerCase()] || action.label} | |
| </button> | |
| ))} | |
| </div> | |
| )} | |
| </div> | |
| ); | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code data-line-number data-trim> | |
| function CRMPage() { | |
| return ( | |
| <div> | |
| <DataCardV4 | |
| title="John Doe" | |
| description="VIP Client since 2022" | |
| type="client" | |
| status="active" | |
| layout="compact" | |
| theme="dark" | |
| isSelectable={true} | |
| media={{ type: 'image', url: '/client-photo.jpg' }} | |
| metadata={{ email: '[email protected]', phone: '+1234567890' }} | |
| trackEvent="client_card_click" | |
| locale="ru" | |
| asyncAction={async () => { | |
| await new Promise((resolve) => setTimeout(resolve, 1000)); | |
| console.log('Client data fetched'); | |
| }} | |
| actions={[ | |
| { label: 'view', action: () => console.log('View client') }, | |
| { label: 'edit', action: () => console.log('Edit client') }, | |
| ]} | |
| /> | |
| <DataCardV4 | |
| title="Order #1234" | |
| description="Pending delivery" | |
| type="order" | |
| status="pending" | |
| isDraggable={true} | |
| className="mt-4" | |
| /> | |
| </div> | |
| ); | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| Специализация вместо универсальности | |
| <aside class="notes"> | |
| Вместо одного DataCard создать отдельные компоненты: ClientCard, | |
| OrderCard, ProductCard. Каждый компонент должен решать конкретную | |
| задачу с минимальным набором пропсов. Пример: ClientCard для | |
| отображения email и телефона, OrderCard с поддержкой drag-and-drop, | |
| ProductCard с мультимедиа. | |
| </aside> | |
| </section> | |
| <section> | |
| Использование композиции | |
| <aside class="notes"> | |
| <ul> | |
| <li> | |
| Разделить общую логику и стили на хуки и утилиты. Например, | |
| использовать хук useCardStyles для стилей и useAnalytics для | |
| трекинга событий. | |
| </li> | |
| <li> | |
| Пример: Общие стили (границы, отступы) можно вынести в Tailwind | |
| CSS или CSS-модули, а специфические — оставить в компонентах. | |
| </li> | |
| </ul> | |
| </aside> | |
| </section> | |
| <section> | |
| Чёткое разделение ответственности | |
| <aside class="notes"> | |
| <ul> | |
| <li> | |
| UI-компоненты должны отвечать только за отображение, а | |
| бизнес-логика (аналитика, асинхронные действия) — выноситься в | |
| хуки или сервисы. | |
| </li> | |
| <li> | |
| Пример: Логика аналитики могла быть реализована через хук | |
| useAnalyticsTrack, а локализация — через библиотеку i18next. | |
| </li> | |
| </ul> | |
| </aside> | |
| </section> | |
| <section> | |
| <pre><code data-trim data-line-numbers="1-200|1-10|15-23|25-37|39-47|49-70"> | |
| // Утилита для стилей (заменяем объектные маппинги на Tailwind CSS) | |
| const getCardStyles = (type: 'client' | 'order' | 'product', theme: 'light' | 'dark' = 'light') => { | |
| const typeStyles = { | |
| client: 'border-l-4 border-blue-500 bg-blue-50', | |
| order: 'border-l-4 border-green-500 bg-green-50', | |
| product: 'border-l-4 border-purple-500 bg-purple-50', | |
| }; | |
| const themeStyles = { | |
| light: 'bg-opacity-90', | |
| dark: 'bg-gray-800 text-white bg-opacity-80', | |
| }; | |
| return `p-4 rounded shadow-md ${typeStyles[type]} ${themeStyles[theme]}`; | |
| }; | |
| // Хук для аналитики | |
| const useAnalyticsTrack = (trackEvent?: string) => { | |
| const track = () => { | |
| if (trackEvent) { | |
| console.log(`Track event: ${trackEvent}`); | |
| } | |
| }; | |
| return track; | |
| }; | |
| // Хук для асинхронных действий | |
| const useAsyncAction = (asyncAction?: () => Promise<void>) => { | |
| const execute = async () => { | |
| if (asyncAction) { | |
| try { | |
| await asyncAction(); | |
| } catch (error) { | |
| console.error('Async action failed:', error); | |
| } | |
| } | |
| }; | |
| return execute; | |
| }; | |
| // Специализированный компонент для клиентов | |
| interface ClientCardProps { | |
| title: string; | |
| description: string; | |
| metadata: Record<string, string>; | |
| theme?: 'light' | 'dark'; | |
| onClick?: () => void; | |
| trackEvent?: string; | |
| } | |
| function ClientCard({ title, description, metadata, theme = 'light', onClick, trackEvent }: ClientCardProps) { | |
| const track = useAnalyticsTrack(trackEvent); | |
| const handleClick = () => { | |
| track(); | |
| onClick?.(); | |
| }; | |
| return ( | |
| <div className={getCardStyles('client', theme)} onClick={handleClick}> | |
| <h3 className="text-lg font-semibold">{title}</h3> | |
| <p className="text-gray-600">{description}</p> | |
| <div className="mt-2 text-sm text-gray-500"> | |
| {Object.entries(metadata).map(([key, value]) => ( | |
| <div key={key}> | |
| <span className="font-medium">{key}:</span> {value} | |
| </div> | |
| ))} | |
| </div> | |
| </div> | |
| ); | |
| } | |
| // Специализированный компонент для заказов с поддержкой drag-and-drop | |
| interface OrderCardProps { | |
| title: string; | |
| description: string; | |
| status: 'active' | 'inactive' | 'pending'; | |
| isDraggable?: boolean; | |
| theme?: 'light' | 'dark'; | |
| onClick?: () => void; | |
| trackEvent?: string; | |
| } | |
| function OrderCard({ title, description, status, isDraggable, theme = 'light', onClick, trackEvent }: OrderCardProps) { | |
| const track = useAnalyticsTrack(trackEvent); | |
| const statusStyles = { | |
| active: 'text-green-600', | |
| inactive: 'text-red-600', | |
| pending: 'text-yellow-600', | |
| }; | |
| const handleClick = () => { | |
| track(); | |
| onClick?.(); | |
| }; | |
| return ( | |
| <div | |
| className={`${getCardStyles('order', theme)} ${isDraggable ? 'cursor-move' : 'cursor-pointer'}`} | |
| draggable={isDraggable} | |
| onDragStart={isDraggable ? (e) => e.dataTransfer.setData('text/plain', title) : undefined} | |
| onClick={handleClick} | |
| > | |
| <h3 className="text-lg font-semibold">{title}</h3> | |
| <p className={statusStyles[status]}>{description}</p> | |
| </div> | |
| ); | |
| } | |
| // Специализированный компонент для продуктов с мультимедиа | |
| interface ProductCardProps { | |
| title: string; | |
| description: string; | |
| media?: { type: 'image' | 'video'; url: string }; | |
| theme?: 'light' | 'dark'; | |
| onClick?: () => void; | |
| trackEvent?: string; | |
| } | |
| function ProductCard({ title, description, media, theme = 'light', onClick, trackEvent }: ProductCardProps) { | |
| const track = useAnalyticsTrack(trackEvent); | |
| const handleClick = () => { | |
| track(); | |
| onClick?.(); | |
| }; | |
| return ( | |
| <div className={getCardStyles('product', theme)} onClick={handleClick}> | |
| {media && ( | |
| <div className="mb-2"> | |
| {media.type === 'image' ? ( | |
| <img src={media.url} alt={title} className="w-full h-32 object-cover rounded" /> | |
| ) : ( | |
| <video src={media.url} controls className="w-full h-32 rounded" /> | |
| )} | |
| </div> | |
| )} | |
| <h3 className="text-lg font-semibold">{title}</h3> | |
| <p className="text-gray-600">{description}</p> | |
| </div> | |
| ); | |
| } | |
| // Пример использования рефакторинга | |
| function CRMPage() { | |
| return ( | |
| <div> | |
| <ClientCard | |
| title="John Doe" | |
| description="VIP Client since 2022" | |
| metadata={{ email: '[email protected]', phone: '+1234567890' }} | |
| theme="dark" | |
| trackEvent="client_card_click" | |
| onClick={() => console.log('Client clicked')} | |
| /> | |
| <OrderCard | |
| title="Order #1234" | |
| description="Pending delivery" | |
| status="pending" | |
| isDraggable={true} | |
| theme="light" | |
| trackEvent="order_card_click" | |
| onClick={() => console.log('Order clicked')} | |
| /> | |
| <ProductCard | |
| title="Product XYZ" | |
| description="New product" | |
| media={{ type: 'image', url: '/product-photo.jpg' }} | |
| theme="light" | |
| trackEvent="product_card_click" | |
| onClick={() => console.log('Product clicked')} | |
| /> | |
| </div> | |
| ); | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code data-line-number data-trim> | |
| function CRMPage() { | |
| return ( | |
| <div> | |
| <ClientCard | |
| title="John Doe" | |
| description="VIP Client since 2022" | |
| metadata={{ email: '[email protected]', phone: '+1234567890' }} | |
| theme="dark" | |
| trackEvent="client_card_click" | |
| onClick={() => console.log('Client clicked')} | |
| /> | |
| <OrderCard | |
| title="Order #1234" | |
| description="Pending delivery" | |
| status="pending" | |
| isDraggable={true} | |
| theme="light" | |
| trackEvent="order_card_click" | |
| onClick={() => console.log('Order clicked')} | |
| /> | |
| <ProductCard | |
| title="Product XYZ" | |
| description="New product" | |
| media={{ type: 'image', url: '/product-photo.jpg' }} | |
| theme="light" | |
| trackEvent="product_card_click" | |
| onClick={() => console.log('Product clicked')} | |
| /> | |
| </div> | |
| ); | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| Вывод: | |
| <p>Стремление к универсальности может привести к хаосу</p> | |
| </section> | |
| <section> | |
| <p>Мета выводы:</p> | |
| <ul> | |
| <li>Cемантика</li> | |
| <li>Разделение ответсвенности</li> | |
| <li>Эмпатия</li> | |
| <li>Будущее</li> | |
| </ul> | |
| </section> | |
| <section> | |
| <h2 class="r-fit-text">Еще раз про DRY</h2> | |
| </section> | |
| <section> | |
| <h2>Webpack</h2> | |
| <pre><code class="javascript" data-trim> | |
| 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, | |
| }, | |
| }; | |
| </code></pre> | |
| </section> | |
| <section> | |
| У нас много команд, давайте переиспользуем этот замечательный config | |
| </section> | |
| <section> | |
| <h2>Все просто</h2> | |
| <pre><code data-trim class="javascript"> | |
| module.exports = require('@my-company/configs/webpack.config'); | |
| </code></pre> | |
| </section> | |
| <section> | |
| <h2>— Мне нужно добавить плагин!</h2> | |
| </section> | |
| <section data-transition="slide none"> | |
| <h2>Все просто</h2> | |
| <pre><code data-trim class="javascript"> | |
| const commonConfig = require('@my-company/configs/webpack.config'); | |
| // свой плагин | |
| module.exports = { | |
| ...commonConfig, | |
| plugins : [mySuperPlugin] | |
| } | |
| </code></pre> | |
| </section> | |
| <section data-transition="none slide"> | |
| <h2>Все просто</h2> | |
| <pre><code data-trim class="javascript" data-line-numbers> | |
| const commonConfig = require('@my-company/configs/webpack.config'); | |
| module.exports = { | |
| ...commonConfig, | |
| plugins : [ | |
| ...commonConfig.plugins, | |
| mySuperPlugin | |
| ] | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| Добро пожаловать в ад! | |
| <ul> | |
| <li class="fragment">Некоторые плагины нужно перенастраивать</li> | |
| <li class="fragment">У всех разные правила</li> | |
| <li class="fragment">У всех разные сервера разработки</li> | |
| </ul> | |
| </section> | |
| <section> | |
| <h2>Что происходит дальше</h2> | |
| <ul> | |
| <li>Нам нужен frontops!</li> | |
| <li>Пишем фреймворк на настройке конфигов</li> | |
| <li>Не меняй корневой пакет, не понятно где все упадет</li> | |
| </ul> | |
| </section> | |
| <section>Помните react-scripts?</section> | |
| <section> | |
| <h2>Craco</h2> | |
| <blockquote> | |
| allows you to get all of the benefits of Create React App without | |
| ejecting | |
| </blockquote> | |
| <pre class="fragment"> | |
| CREATE | |
| REACT | |
| APP | |
| CONFIGURATION | |
| OVERRIDE | |
| </pre | |
| > | |
| </section> | |
| <section style="text-align: left"> | |
| <p>— Зачем мы это делали?</p> | |
| <p>— Хотели единый конфиг!</p> | |
| </section> | |
| <section style="text-align: left"> | |
| <p>— Зачем нам единый конфиг?</p> | |
| <p> | |
| — Чтобы не настраивать webpack каждый раз, чтобы менять все | |
| настройки в одном месте | |
| </p> | |
| </section> | |
| <section style="text-align: left"> | |
| <p> | |
| — Сколько стоит настроить вебпак для каждого проекта и менять | |
| каждый конфиг отдельно? | |
| </p> | |
| <p>— Да фиг его знает!</p> | |
| </section> | |
| <!-- <section>Про инженерную интуицию и тягу к прекрасоному</section> --> | |
| <section> | |
| <h2 class="r-fit-text">Имена и типы</h2> | |
| </section> | |
| <section> | |
| <img src="img/login.png" alt="" /> | |
| </section> | |
| <section> | |
| <pre><code> | |
| type LoginData = { | |
| "login": "string", | |
| "password": "string" | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <h2>JSON</h2> | |
| <pre><code data-trim> | |
| { | |
| "username": "aleksei.zolotykh", | |
| "password": "123456" | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code data-trim> | |
| const login = ({login, password}) => { | |
| return fetch('/login', { | |
| body: JSON.stringify({username: login , password} | |
| }) | |
| } | |
| </code></pre> | |
| </section> | |
| <section>login => username</section> | |
| <section> | |
| <pre><code class="js" data-trim> | |
| const users = [ | |
| {login: "admin", ...}, | |
| {login: "superadmin", ...}, | |
| {login: "zolotyh", ...} | |
| ] | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code class="javascript" data-trim> | |
| users.map( | |
| (user) => { | |
| ...user, | |
| username: user.login | |
| } | |
| ) | |
| </code></pre> | |
| <pre class="fragment"><code class="json" data-trim> | |
| { | |
| "login": "admin", | |
| "username": "admin, | |
| } | |
| </code></pre> | |
| <pre class="fragment"><code class="javascript" data-trim> | |
| users.map( | |
| ({login, ...rest}) => { | |
| ...rest, | |
| username: login | |
| } | |
| ) | |
| </code></pre> | |
| </section> | |
| <section>Эта проблема может быть масштабней</section> | |
| <section>Проект может до 80% состоять из маппингов</section> | |
| <section> | |
| Этой проблемы бы не было, если бы контракт был бы прописан вначале! | |
| </section> | |
| <section> | |
| <pre><code class="yaml" style="font-size: 0.5em; line-height: 1.1" data-line-numbers="1-31|5|13|17-32"> | |
| openapi: 3.0.0 | |
| ... | |
| paths: | |
| /auth/login: | |
| post: | |
| ... | |
| requestBody: | |
| required: true | |
| content: | |
| application/json: | |
| schema: | |
| $ref: '#/components/schemas/LoginCredentials' | |
| ... | |
| 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 | |
| </code></pre> | |
| </section> | |
| <section>Типы</section> | |
| <section> | |
| <pre><code class="typescript" data-trim> | |
| interface User { | |
| id?: string; // теоретически может быть undefined | |
| name: string; | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code class="typescript" data-trim> | |
| function processUser(user: User) { | |
| // Ошибка TS: 'id' возможно undefined | |
| console.log(user.id.toUpperCase()); | |
| } | |
| </code></pre> | |
| </section> | |
| <section> | |
| <pre><code data-trim> | |
| interface User { | |
| id: string; | |
| name: string; | |
| } | |
| type UnsavedUser = Omit<User, "id"> | |
| </code></pre> | |
| </section> | |
| <section> | |
| <h2>Знай cвои данные!</h2> | |
| </section> | |
| <section> | |
| <h2 class="r-fit-text">3. Про незаконченные миграции</h2> | |
| </section> | |
| <section> | |
| <h2>Большая миграция через правило туриста</h2> | |
| </section> | |
| <section> | |
| <ul> | |
| <li>Я хочу уйти от Redux к Redux Saga</li> | |
| <li class="fragment">Вот прототип</li> | |
| <li class="fragment">Вот первая фича на Saga</li> | |
| <li class="fragment">Остальное по правилу туриста</li> | |
| </ul> | |
| </section> | |
| <section> | |
| <blockquote> | |
| A scout leaves no trace… and tries to leave the world a little | |
| better than he found it | |
| </blockquote> | |
| </section> | |
| <section> | |
| Когда будем переписывать — переведем на новый стейт менеджер | |
| </section> | |
| <section> | |
| <h2>Теперь у нас есть</h2> | |
| <ul> | |
| <li>Redux</li> | |
| <li>Redux-saga</li> | |
| <li>Redux-toolkit</li> | |
| <li>RxJS</li> | |
| </ul> | |
| <p class="fragment">Надежда, что когда-нибудь это получится</p> | |
| </section> | |
| <section> | |
| <h2>Что нужно было сделать</h2> | |
| <ul> | |
| <li>Будет ли миграция оправдана?</li> | |
| <li>Можете ли вы позволить себе миграцию?</li> | |
| <li>Кто будет отвечать за миграцию?</li> | |
| </ul> | |
| </section> | |
| <section> | |
| <h2 class="r-fit-text">4. Про оптимизации и overengeneering</h2> | |
| </section> | |
| <section>TBD</section> | |
| <section> | |
| <h2 class="r-fit-text">Послесловие</h2> | |
| </section> | |
| </div> | |
| </div> | |
| <script src="dist/reveal.js"></script> | |
| <script src="plugin/notes/notes.js"></script> | |
| <script src="plugin/markdown/markdown.js"></script> | |
| <script src="plugin/highlight/highlight.js"></script> | |
| <script> | |
| // More info about initialization & config: | |
| // - https://revealjs.com/initialization/ | |
| // - https://revealjs.com/config/ | |
| Reveal.initialize({ | |
| hash: true, | |
| // Learn about plugins: https://revealjs.com/plugins/ | |
| plugins: [RevealMarkdown, RevealHighlight, RevealNotes], | |
| }); | |
| </script> | |
| </body> | |
| </html> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment