核心理念:分层增强的组件架构

渐进式组件设计遵循”从基础到增强”的分层原则,确保组件在不同环境下都能提供适当的用户体验。

组件分层架构

第一层:原生 HTML 基础层

// 最基础的、无样式的原生组件
export function NativeButton({ 
  type = 'button', 
  disabled, 
  children, 
  ...props 
}) {
  return (
    <button 
      type={type}
      disabled={disabled}
      {...props}
    >
      {children}
    </button>
  );
}
 
export function NativeInput({ 
  type = 'text',
  required,
  disabled,
  ...props 
}) {
  return (
    <input 
      type={type}
      required={required}
      disabled={disabled}
      {...props}
    />
  );
}

特点

  • 零依赖,零运行时
  • 完全可访问性支持
  • 在任何环境下都能正常工作
  • 提供最基础的功能性

第二层:视觉样式增强层

// 添加设计系统的视觉规范
export function StyledButton({ 
  variant = 'primary',
  size = 'medium',
  children,
  ...props 
}) {
  const baseClasses = 'btn focus:outline-none transition-colors';
  const variantClasses = {
    primary: 'bg-blue-600 text-white hover:bg-blue-700',
    secondary: 'bg-gray-200 text-gray-900 hover:bg-gray-300',
    danger: 'bg-red-600 text-white hover:bg-red-700'
  };
  const sizeClasses = {
    small: 'px-3 py-1 text-sm',
    medium: 'px-4 py-2',
    large: 'px-6 py-3 text-lg'
  };
 
  return (
    <NativeButton
      {...props}
      className={`
        ${baseClasses}
        ${variantClasses[variant]}
        ${sizeClasses[size]}
        ${props.className || ''}
      `}
    >
      {children}
    </NativeButton>
  );
}

第三层:交互行为增强层

// 添加丰富的交互反馈
export function InteractiveButton(props) {
  const [isLoading, setIsLoading] = useState(false);
  const [isHovered, setIsHovered] = useState(false);
  
  const handleClick = async (event) => {
    if (props.disabled || isLoading) return;
    
    setIsLoading(true);
    try {
      await props.onClick?.(event);
    } catch (error) {
      console.error('Button action failed:', error);
    } finally {
      setIsLoading(false);
    }
  };
 
  return (
    <StyledButton
      {...props}
      disabled={props.disabled || isLoading}
      onClick={handleClick}
      onMouseEnter={() => setIsHovered(true)}
      onMouseLeave={() => setIsHovered(false)}
      aria-busy={isLoading}
      style={{
        transform: isHovered && !isLoading ? 'translateY(-1px)' : 'none',
        transition: 'all 0.2s ease',
        ...props.style
      }}
    >
      {isLoading ? (
        <span className="flex items-center">
          <Spinner size="small" />
          <span className="ml-2">处理中...</span>
        </span>
      ) : (
        children
      )}
    </StyledButton>
  );
}

第四层:智能功能增强层

// 添加业务逻辑和高级功能
export function SmartButton({
  analytics,
  confirm,
  undoable,
  successFeedback,
  ...props
}) {
  const [showUndo, setShowUndo] = useState(false);
  const [undoTimer, setUndoTimer] = useState(null);
  
  const handleClick = useCallback(async (event) => {
    // 确认对话框
    if (confirm && !window.confirm(confirm)) {
      return;
    }
    
    // 数据分析
    if (analytics) {
      trackEvent('button_click', {
        button_id: props.id,
        ...analytics
      });
    }
    
    // 可撤销操作
    if (undoable) {
      setShowUndo(true);
      const timer = setTimeout(() => setShowUndo(false), 5000);
      setUndoTimer(timer);
      
      try {
        await props.onClick?.(event);
        if (successFeedback) {
          showToast(successFeedback);
        }
      } catch (error) {
        setShowUndo(false);
        clearTimeout(timer);
        throw error;
      }
      return;
    }
    
    // 普通操作
    try {
      await props.onClick?.(event);
      if (successFeedback) {
        showToast(successFeedback);
      }
    } catch (error) {
      showToast('操作失败,请重试');
      throw error;
    }
  }, [props.onClick, confirm, analytics, undoable, successFeedback]);
  
  const handleUndo = useCallback(() => {
    setShowUndo(false);
    if (undoTimer) clearTimeout(undoTimer);
    // 执行撤销逻辑
    performUndo();
  }, [undoTimer]);
  
  return (
    <div className="flex items-center gap-2">
      <InteractiveButton
        {...props}
        onClick={handleClick}
      />
      {showUndo && (
        <button
          onClick={handleUndo}
          className="text-sm text-blue-600 hover:text-blue-800"
        >
          撤销
        </button>
      )}
    </div>
  );
}

表单组件的渐进式实现

基础表单结构

export function ProgressiveForm({ onSubmit, children, ...props }) {
  const [isSubmitting, setIsSubmitting] = useState(false);
  
  const handleSubmit = async (event) => {
    event.preventDefault();
    setIsSubmitting(true);
    
    try {
      await onSubmit?.(event);
    } finally {
      setIsSubmitting(false);
    }
  };
  
  return (
    <form 
      {...props}
      onSubmit={handleSubmit}
      noValidate // 允许自定义验证
    >
      <fieldset disabled={isSubmitting}>
        {children}
      </fieldset>
    </form>
  );
}

渐进式输入组件

export function ProgressiveInput({
  validation,
  realtimeValidation,
  suggestions,
  ...props
}) {
  const [errors, setErrors] = useState([]);
  const [showSuggestions, setShowSuggestions] = useState(false);
  
  // 基础验证
  const validate = useCallback((value) => {
    if (!validation) return [];
    
    const newErrors = [];
    if (validation.required && !value.trim()) {
      newErrors.push('此字段为必填项');
    }
    if (validation.minLength && value.length < validation.minLength) {
      newErrors.push(`至少需要${validation.minLength}个字符`);
    }
    if (validation.pattern && !validation.pattern.test(value)) {
      newErrors.push(validation.message || '格式不正确');
    }
    
    return newErrors;
  }, [validation]);
  
  const handleChange = (event) => {
    const value = event.target.value;
    props.onChange?.(event);
    
    // 实时验证
    if (realtimeValidation) {
      setErrors(validate(value));
    }
    
    // 显示建议
    if (suggestions && value.length > 2) {
      setShowSuggestions(true);
    } else {
      setShowSuggestions(false);
    }
  };
  
  const handleBlur = (event) => {
    // 失焦时验证
    setErrors(validate(event.target.value));
    setShowSuggestions(false);
    props.onBlur?.(event);
  };
  
  return (
    <div className="relative">
      <NativeInput
        {...props}
        onChange={handleChange}
        onBlur={handleBlur}
        aria-invalid={errors.length > 0}
        className={`
          ${props.className || ''}
          ${errors.length > 0 ? 'border-red-500' : 'border-gray-300'}
        `}
      />
      
      {/* 错误提示 */}
      {errors.length > 0 && (
        <div className="mt-1 text-sm text-red-600">
          {errors.map((error, index) => (
            <div key={index}>{error}</div>
          ))}
        </div>
      )}
      
      {/* 智能建议 */}
      {showSuggestions && suggestions && (
        <div className="absolute z-10 w-full mt-1 bg-white border rounded shadow-lg">
          {suggestions
            .filter(suggestion => 
              suggestion.toLowerCase().includes(props.value.toLowerCase())
            )
            .map((suggestion, index) => (
              <div
                key={index}
                className="px-3 py-2 cursor-pointer hover:bg-gray-100"
                onClick={() => {
                  props.onChange?.({
                    target: { value: suggestion }
                  });
                  setShowSuggestions(false);
                }}
              >
                {suggestion}
              </div>
            ))
          }
        </div>
      )}
    </div>
  );
}

组件能力检测与渐进加载

运行时能力检测

export function AdaptiveComponent({ 
  fallback,
  children,
  requiredFeatures = [] 
}) {
  const [canRender, setCanRender] = useState(true);
  const [supportedFeatures, setSupportedFeatures] = useState({});
  
  useEffect(() => {
    // 检测浏览器支持的特性
    const features = {
      intersectionObserver: 'IntersectionObserver' in window,
      webGL: detectWebGLSupport(),
      touchEvents: 'ontouchstart' in window,
      serviceWorker: 'serviceWorker' in navigator,
    };
    
    setSupportedFeatures(features);
    
    // 检查是否满足渲染条件
    const hasRequiredFeatures = requiredFeatures.every(
      feature => features[feature]
    );
    
    setCanRender(hasRequiredFeatures);
  }, [requiredFeatures]);
  
  if (!canRender) {
    return fallback ? fallback(supportedFeatures) : null;
  }
  
  return children(supportedFeatures);
}

使用示例

function App() {
  return (
    <div>
      {/* 基础组件 - 始终渲染 */}
      <StyledButton variant="primary">
        基础按钮
      </StyledButton>
      
      {/* 增强组件 - 条件渲染 */}
      <AdaptiveComponent
        requiredFeatures={['webGL', 'intersectionObserver']}
        fallback={(features) => (
          <div>
            <p>您的设备不支持 3D 预览功能</p>
            <img src="/static-preview.jpg" alt="产品预览" />
          </div>
        )}
      >
        {(features) => (
          <WebGLViewer 
            model="/model.glb"
            enableIntersection={features.intersectionObserver}
          />
        )}
      </AdaptiveComponent>
    </div>
  );
}

设计系统主题的渐进式应用

主题分层系统

// 1. 基础主题变量
export const baseTheme = {
  colors: {
    text: '#000000',
    background: '#ffffff',
    primary: '#007acc',
    error: '#d32f2f',
    warning: '#ff9800',
    success: '#4caf50'
  },
  spacing: {
    xs: '0.25rem',
    sm: '0.5rem',
    md: '1rem',
    lg: '1.5rem',
    xl: '2rem'
  }
};
 
// 2. 组件特定主题
export const componentTheme = {
  button: {
    borderRadius: '0.375rem',
    borderWidth: '1px',
    fontSize: {
      small: '0.875rem',
      medium: '1rem',
      large: '1.125rem'
    }
  }
};
 
// 3. 品牌定制主题
export const createBrandTheme = (brandColors) => ({
  ...baseTheme,
  colors: {
    ...baseTheme.colors,
    ...brandColors
  }
});

测试策略

分层测试架构

// 1. 基础层测试 - 功能性和可访问性
describe('NativeButton', () => {
  it('renders with proper accessibility attributes', () => {
    render(<NativeButton disabled>Click me</NativeButton>);
    expect(screen.getByRole('button')).toBeDisabled();
  });
});
 
// 2. 样式层测试 - 视觉一致性
describe('StyledButton', () => {
  it('applies correct variant styles', () => {
    render(<StyledButton variant="danger" />);
    expect(screen.getByRole('button')).toHaveClass('bg-red-600');
  });
});
 
// 3. 交互层测试 - 用户交互行为
describe('InteractiveButton', () => {
  it('shows loading state during async operation', async () => {
    render(<InteractiveButton onClick={() => new Promise(r => setTimeout(r, 100))} />);
    fireEvent.click(screen.getByRole('button'));
    expect(await screen.findByText('处理中...')).toBeInTheDocument();
  });
});
 
// 4. 智能层测试 - 业务逻辑
describe('SmartButton', () => {
  it('shows confirmation dialog when confirm prop is set', () => {
    window.confirm = jest.fn(() => false);
    render(<SmartButton confirm="Are you sure?" />);
    fireEvent.click(screen.getByRole('button'));
    expect(window.confirm).toHaveBeenCalledWith('Are you sure?');
  });
});

最佳实践总结

  1. 分层设计:从原生 HTML 开始,逐层增强样式、交互和智能功能
  2. 向后兼容:确保每一层增强都不会破坏基础层功能
  3. 能力检测:根据运行时环境决定渲染哪些增强功能
  4. 优雅降级:当增强功能不可用时,自动回退到基础版本
  5. 性能优先:按需加载增强层,避免不必要的运行时开销
  6. 测试覆盖:为每一层组件编写相应的测试用例

这种渐进式组件设计方法确保了设计系统在不同环境下的鲁棒性,同时为现代化浏览器提供了丰富的用户体验。