React 中使用策略模式的案例 → 来源:⚛️ Applying Strategy Pattern in React (Part 2) - DEV Community 组件设计 在组件设计系统中,组件具有各种变体、颜色和尺寸,例如Button组件, props 类型如下 export type ButtonVariant = 'contained' | 'outlined' | 'text'; export type ButtonColor = 'primary' | 'secondary' | 'error' | 'success' | 'warning' | 'info'; export type ButtonSize = 'xs' | 'sm' | 'md'; export interface ButtonProps { color?: ButtonColor; variant?: ButtonVariant; size?: ButtonSize; children: React.ReactNode; } 在使用策略模式之前,你可能会这样写 import { memo } from 'react'; import { Button as ThemeUIButton } from 'theme-ui'; import { ButtonProps } from './Button.types'; const ButtonBase = ({ color = 'primary', variant = 'contained', size = 'sm', children }: ButtonProps) => { return ( <ThemeUIButton sx={{ outline: 'none', borderRadius: 4, transition: '0.1s all', cursor: 'pointer', ...(variant === 'contained' ? { backgroundColor: `${color}.main`, color: 'white', '&:hover': { backgroundColor: `${color}.dark`, }, } : {}), ...(variant === 'outlined' ? { backgroundColor: 'transparent', color: `${color}.main`, border: '1px solid', borderColor: `${color}.main`, '&:hover': { backgroundColor: `${color}.light`, }, } : {}), ...(variant === 'text' ? { backgroundColor: 'transparent', color: `${color}.main`, '&:hover': { backgroundColor: `${color}.light`, }, } : {}), ...(size === 'xs' ? { fontSize: '0.75rem', padding: '8px 12px', } : {}), ...(size === 'sm' ? { fontSize: '0.875rem', padding: '12px 16px', } : {}), ...(size === 'md' ? { fontSize: '1rem', padding: '16px 24px', } : {}), }} > {children} </ThemeUIButton> ); }; export const Button = memo(ButtonBase); 简单但是混乱。 应用策略模式 button/utils.ts import { ButtonColor } from './Button.types'; export const getButtonVariantMapping = (color: ButtonColor = 'primary') => { return { contained: { backgroundColor: `${color}.main`, color: 'white', '&:hover': { backgroundColor: `${color}.dark`, }, }, outlined: { backgroundColor: 'transparent', color: `${color}.main`, border: '1px solid', borderColor: `${color}.main`, '&:hover': { backgroundColor: `${color}.light`, }, }, text: { backgroundColor: 'transparent', color: `${color}.main`, '&:hover': { backgroundColor: `${color}.light`, }, }, }; }; export const BUTTON_SIZE_STYLE_MAPPING = { xs: { fontSize: '0.75rem', padding: '8px 12px', }, sm: { fontSize: '0.875rem', padding: '12px 16px', }, md: { fontSize: '1rem', padding: '16px 24px', }, }; button/index.tsx import { memo } from 'react'; import { Button as ThemeUIButton } from 'theme-ui'; import { ButtonProps } from './Button.types'; import { getButtonVariantMapping, BUTTON_SIZE_STYLE_MAPPING } from './Button.utils'; const ButtonBase = ({ color = 'primary', variant = 'contained', size = 'sm', children }: ButtonProps) => { const buttonVariantStyle = getButtonVariantMapping(color)[variant]; return ( <ThemeUIButton sx={{ outline: 'none', borderRadius: 4, transition: '0.1s all', cursor: 'pointer', ...buttonVariantStyle, ...BUTTON_SIZE_STYLE_MAPPING[size], }} > {children} </ThemeUIButton> ); }; export const Button = memo(ButtonBase); 使用 TailwindCSS 与 CVA 的写法 'use client'; import React from 'react'; import { cva } from 'class-variance-authority'; import { cn } from '@/utils'; const _button = cva('cursor-pointer rounded text-xs transition flex items-center justify-center', { variants: { type: { default: ['bg-transparent', 'text-white/70', 'hover:bg-control-hover'], primary: ['bg-accent', 'text-white', 'hover:brightness-130'], ghost: ['bg-transparent', 'text-white/70', 'hover:text-white'], warn: ['bg-error', 'text-white', 'hover:brightness-130'], link: ['bg-transparent', 'text-white/70', 'hover:text-white underline'], normal: ['bg-white/5', 'text-caption', 'hover:bg-white/10'], outline: [ 'bg-transparent', 'text-accent', 'border border-accent', 'hover:border-accent-hover hover:text-accent-hover', ], }, size: { medium: ['h-9', 'px-6'], small: ['h-6', 'px-6'], flat: ['h-10', 'px-11'], superFlat: ['h-10', 'px-16'], modal: ['h-8', 'px-4'], }, disable: { true: ['bg-gray-100', 'bg-control-disable', 'cursor-not-allowed', 'text-white/20'], }, }, compoundVariants: [ // 当 type 为 default 时,size 为 medium,额外应用 className { type: 'primary', size: 'medium', className: '' }, { type: 'primary', disable: true, className: 'bg-accent-disable hover:brightness-100' }, ], defaultVariants: { type: 'default', size: 'medium', }, }); /** * Button component. * * @param {Object} props * @param {'primary' | 'default' | 'ghost' | 'link' | 'warn' | 'outline'} props.type 三种组件类型 * @param {'medium' | 'small'} props.size * @param {string} props.className * @param {boolean} props.disable * @param {boolean} props.asChild - 是否使用 children 作为按钮内容(替换底层元素但保持样式不变) * @param {Function} props.onClick * @param {React.ReactNod} props.children * @returns {React.ReactNode} The rendered button component. */ export default function Button({ children, type, disable, size, className, onClick, asChild, ...restProps }) { const _className = cn(_button({ type, size, className, disable })); const handleClick = (e) => { if (disable) { return; } onClick?.(e); }; const _props = { className: _className, onClick: handleClick, ...restProps }; if (asChild) { if (React.isValidElement(children) === false) { throw new Error('children必须是一个React element'); } return React.cloneElement(children, { ..._props }); } return <button {..._props}>{children}</button>; }