Base UI文件上传:Upload组件的设计

Base UI文件上传:Upload组件的设计

【免费下载链接】base-ui Base UI is a library of headless ("unstyled") React components and low-level hooks. You gain complete control over your app's CSS and accessibility features. 【免费下载链接】base-ui 项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui

痛点:为什么需要专业的文件上传组件?

在Web应用开发中,文件上传功能看似简单,实则暗藏诸多挑战:

  • 样式定制困难:原生<input type="file">样式受限,难以与设计系统统一
  • 用户体验不佳:缺乏拖拽上传、多文件选择、进度显示等现代功能
  • 可访问性缺失:键盘导航、屏幕阅读器支持不足
  • 状态管理复杂:文件列表、上传进度、错误处理等状态难以维护

Base UI的Upload组件正是为解决这些痛点而生,提供无样式(headless)的React组件,让你完全掌控样式和交互逻辑。

Upload组件的核心设计理念

1. 无样式设计哲学

Base UI采用无样式(headless)设计,组件只提供核心逻辑和可访问性,样式完全由开发者控制:

// 基础Upload组件结构
import { Upload } from '@base-ui/react/upload';

function FileUpload() {
  return (
    <Upload.Root>
      <Upload.Trigger as="button">
        选择文件
      </Upload.Trigger>
      <Upload.Dropzone>
        拖拽文件到此处
      </Upload.Dropzone>
      <Upload.FileList>
        {(files) => files.map(file => (
          <Upload.FileItem key={file.name}>
            {file.name} - {file.size} bytes
          </Upload.FileItem>
        ))}
      </Upload.FileList>
    </Upload.Root>
  );
}

2. 组件架构设计

mermaid

核心功能实现详解

1. 多文件选择与验证

import { Upload } from '@base-ui/react/upload';

function ValidatedUpload() {
  const [errors, setErrors] = useState([]);

  const validateFiles = (files) => {
    const newErrors = [];
    files.forEach(file => {
      if (file.size > 10 * 1024 * 1024) {
        newErrors.push(`${file.name}:文件大小不能超过10MB`);
      }
      if (!file.type.startsWith('image/')) {
        newErrors.push(`${file.name}:仅支持图片文件`);
      }
    });
    setErrors(newErrors);
    return newErrors.length === 0;
  };

  return (
    <Upload.Root 
      multiple 
      accept="image/*"
      onFilesChange={(files) => {
        if (validateFiles(files)) {
          // 处理有效文件
        }
      }}
    >
      <Upload.Trigger as="button">
        选择图片
      </Upload.Trigger>
      {errors.length > 0 && (
        <div className="error-list">
          {errors.map(error => <div key={error}>{error}</div>)}
        </div>
      )}
    </Upload.Root>
  );
}

2. 拖拽上传实现

function DragAndDropUpload() {
  const [isDragging, setIsDragging] = useState(false);

  return (
    <Upload.Root>
      <Upload.Dropzone
        onDragOver={() => setIsDragging(true)}
        onDragLeave={() => setIsDragging(false)}
        onDrop={() => setIsDragging(false)}
      >
        <div className={`dropzone ${isDragging ? 'dragging' : ''}`}>
          {isDragging ? '释放文件以上传' : '拖拽文件到此处'}
        </div>
      </Upload.Dropzone>
    </Upload.Root>
  );
}

3. 文件列表与状态管理

function FileUploadWithProgress() {
  const [uploadProgress, setUploadProgress] = useState({});

  const uploadFile = async (file) => {
    const formData = new FormData();
    formData.append('file', file);

    const xhr = new XMLHttpRequest();
    
    xhr.upload.addEventListener('progress', (event) => {
      if (event.lengthComputable) {
        const progress = Math.round((event.loaded * 100) / event.total);
        setUploadProgress(prev => ({ ...prev, [file.name]: progress }));
      }
    });

    xhr.open('POST', '/api/upload');
    xhr.send(formData);
  };

  return (
    <Upload.Root onFilesChange={(files) => {
      files.forEach(uploadFile);
    }}>
      <Upload.FileList>
        {(files) => files.map(file => (
          <Upload.FileItem key={file.name}>
            <div className="file-item">
              <span>{file.name}</span>
              <div className="progress-bar">
                <div 
                  className="progress-fill" 
                  style={{ width: `${uploadProgress[file.name] || 0}%` }}
                />
              </div>
              <span>{uploadProgress[file.name] || 0}%</span>
            </div>
          </Upload.FileItem>
        ))}
      </Upload.FileList>
    </Upload.Root>
  );
}

可访问性设计最佳实践

1. 键盘导航支持

function AccessibleUpload() {
  return (
    <Upload.Root>
      <Upload.Trigger as="button" aria-label="选择文件">
        📁 选择文件
      </Upload.Trigger>
      
      <Upload.Dropzone 
        role="region" 
        aria-label="文件拖拽区域"
      >
        <div tabIndex={0} role="button" aria-label="拖拽上传">
          或者拖拽文件到此处
        </div>
      </Upload.Dropzone>

      <Upload.FileList aria-label="已选择文件列表">
        {(files) => files.map((file, index) => (
          <Upload.FileItem 
            key={file.name}
            aria-label={`文件 ${index + 1}: ${file.name}, 大小: ${file.size} 字节`}
          >
            {file.name}
          </Upload.FileItem>
        ))}
      </Upload.FileList>
    </Upload.Root>
  );
}

2. 屏幕阅读器优化

function ScreenReaderFriendlyUpload() {
  const [announcement, setAnnouncement] = useState('');

  return (
    <>
      <div 
        aria-live="polite" 
        aria-atomic="true"
        className="sr-only"
      >
        {announcement}
      </div>
      
      <Upload.Root onFilesChange={(files) => {
        if (files.length > 0) {
          setAnnouncement(`已选择 ${files.length} 个文件`);
        }
      }}>
        {/* 组件内容 */}
      </Upload.Root>
    </>
  );
}

高级功能扩展

1. 自定义预览组件

function ImagePreviewUpload() {
  return (
    <Upload.Root accept="image/*">
      <Upload.FileList>
        {(files) => files.map(file => (
          <Upload.FileItem key={file.name}>
            <div className="image-preview">
              <img 
                src={URL.createObjectURL(file)} 
                alt={file.name}
                onLoad={() => URL.revokeObjectURL(this.src)}
              />
              <div className="file-info">
                <span>{file.name}</span>
                <span>{(file.size / 1024).toFixed(1)} KB</span>
              </div>
            </div>
          </Upload.FileItem>
        ))}
      </Upload.FileList>
    </Upload.Root>
  );
}

2. 分块上传实现

function ChunkedUpload() {
  const chunkSize = 1 * 1024 * 1024; // 1MB chunks

  const uploadInChunks = async (file) => {
    const totalChunks = Math.ceil(file.size / chunkSize);
    const fileId = Date.now().toString();

    for (let chunkIndex = 0; chunkIndex < totalChunks; chunkIndex++) {
      const start = chunkIndex * chunkSize;
      const end = Math.min(start + chunkSize, file.size);
      const chunk = file.slice(start, end);

      const formData = new FormData();
      formData.append('file', chunk);
      formData.append('fileId', fileId);
      formData.append('chunkIndex', chunkIndex.toString());
      formData.append('totalChunks', totalChunks.toString());

      await fetch('/api/upload-chunk', {
        method: 'POST',
        body: formData
      });
    }

    // 通知服务器合并文件
    await fetch('/api/merge-chunks', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ fileId, fileName: file.name })
    });
  };

  return (
    <Upload.Root onFilesChange={(files) => {
      files.forEach(uploadInChunks);
    }}>
      <Upload.Trigger as="button">
        上传大文件
      </Upload.Trigger>
    </Upload.Root>
  );
}

性能优化策略

1. 虚拟滚动文件列表

import { FixedSizeList as List } from 'react-window';

function VirtualizedFileList({ files }) {
  const Row = ({ index, style }) => (
    <div style={style}>
      <Upload.FileItem file={files[index]}>
        {files[index].name}
      </Upload.FileItem>
    </div>
  );

  return (
    <List
      height={400}
      itemCount={files.length}
      itemSize={50}
      width="100%"
    >
      {Row}
    </List>
  );
}

2. 内存管理优化

function MemoryOptimizedUpload() {
  const [filePreviews, setFilePreviews] = useState({});

  const createPreview = (file) => {
    const previewUrl = URL.createObjectURL(file);
    setFilePreviews(prev => ({ ...prev, [file.name]: previewUrl }));
    
    // 自动清理不再需要的预览
    return () => URL.revokeObjectURL(previewUrl);
  };

  useEffect(() => {
    return () => {
      // 组件卸载时清理所有预览URL
      Object.values(filePreviews).forEach(URL.revokeObjectURL);
    };
  }, []);

  return (
    <Upload.Root onFilesChange={(files) => {
      files.forEach(file => {
        const cleanup = createPreview(file);
        // 存储清理函数以备后用
      });
    }}>
      {/* 组件内容 */}
    </Upload.Root>
  );
}

错误处理与用户体验

1. 完整的错误处理机制

function RobustUpload() {
  const [uploadState, setUploadState] = useState('idle');
  const [error, setError] = useState(null);

  const handleUpload = async (files) => {
    setUploadState('uploading');
    setError(null);

    try {
      for (const file of files) {
        const formData = new FormData();
        formData.append('file', file);

        const response = await fetch('/api/upload', {
          method: 'POST',
          body: formData
        });

        if (!response.ok) {
          throw new Error(`上传失败: ${response.statusText}`);
        }
      }
      setUploadState('success');
    } catch (err) {
      setUploadState('error');
      setError(err.message);
    }
  };

  return (
    <Upload.Root onFilesChange={handleUpload}>
      <div className={`upload-container ${uploadState}`}>
        {uploadState === 'uploading' && <div>上传中...</div>}
        {uploadState === 'error' && (
          <div className="error-message">
            {error}
            <button onClick={() => setUploadState('idle')}>重试</button>
          </div>
        )}
        {uploadState === 'success' && <div>上传成功!</div>}
      </div>
    </Upload.Root>
  );
}

总结:为什么选择Base UI Upload组件?

Base UI的Upload组件提供了:

  1. 完全的控制权:无样式设计让你可以完全自定义外观和交互
  2. 卓越的可访问性:内置ARIA支持和键盘导航
  3. 灵活的扩展性:支持各种高级功能如分块上传、拖拽操作
  4. 优秀的性能:优化的内存管理和渲染性能
  5. 强大的错误处理:完善的错误处理和用户反馈机制

通过Base UI Upload组件,你可以构建出既美观又功能强大的文件上传体验,同时保持代码的简洁和可维护性。

【免费下载链接】base-ui Base UI is a library of headless ("unstyled") React components and low-level hooks. You gain complete control over your app's CSS and accessibility features. 【免费下载链接】base-ui 项目地址: https://gitcode.com/GitHub_Trending/ba/base-ui

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值