文件上传是前端开发中的"老朋友",但如何让它既简单又强大,还能无缝对接云端存储?今天,我要带你认识一个超酷的 React 组件 AliUploader ,它不仅支持拖拽上传、批量编辑和文件排序,还直接把文件传到 Aliyun OSS(阿里云对象存储服务),返回云端链接供你随时调用。我们会拆解它的代码,优化它的逻辑,再通过一个"项目文件上传器" Demo 展示它的实力。准备好了吗?让我们一起把文件上传玩出云端新高度吧!
组件的核心功能:上传到云端
AliUploader 的任务是让文件管理变得简单又高效:
- OSS 直传:文件上传直接到 Aliyun OSS,返回云端链接。
- 拖拽上传:支持单文件或批量拖拽,操作丝滑。
- 文件管理:按类型分组(图片、文档、其他),支持排序和批量编辑备注。
- 云端同步:初始化时加载 OSS 文件列表,删除时同步清理云端。
它就像一个"云端快递员",把你的文件快速送上云端,还能随时查收!
代码拆解与优化
原始代码的问题
原始代码已经很强大,但有几处可以改进:
- 上传状态管理混乱:初始文件状态未明确定义,进度更新不完整。
- 错误处理不足:上传和删除的异常捕获不够健壮。
- 性能优化空间:文件列表更新可以更高效。
我们将优化这些点,让代码更优雅、更健壮。
优化后的代码
1. 类型定义与工具函数
javascript
// utils.ts
import OSS from 'ali-oss';
import { message } from 'antd';
import { FileData, Props } from './index';
export function getFileType(fileName: string): FileData['type'] {
if (/\.(png|jpg|jpeg|gif)$/i.test(fileName)) return 'image';
if (/\.(doc|docx|pdf|xls|xlsx|pptx)$/i.test(fileName)) return 'document';
return 'other';
}
export async function uploadToOSS(
file: File,
ossConfig: Props['ossConfig'],
onProgress: (data: Partial<FileData>) => void,
): Promise<FileData> {
if (!ossConfig) throw new Error('OSS 配置未提供');
const client = new OSS({
region: ossConfig.region,
accessKeyId: ossConfig.accessKeyId,
accessKeySecret: ossConfig.accessKeySecret,
bucket: ossConfig.bucket,
});
const fileName = `${Date.now()}-${file.name}`;
const result = await client.put(fileName, file, {
progress: p => onProgress({ percent: Math.round(p * 100) }),
});
if (result.res.status !== 200) throw new Error('OSS 上传失败');
const url = result.url || `https://${ossConfig.bucket}.${ossConfig.region}.aliyuncs.com/${fileName}`;
return {
uid: `${Date.now()}-${Math.random()}`,
fileId: fileName,
name: file.name,
thumbUrl: url,
url,
status: 'done',
percent: 100,
type: getFileType(file.name),
uploadTime: Date.now(),
cloudUrl: url,
};
}
export async function delfileFromOSS(
fileName: string,
ossConfig: Props['ossConfig'],
): Promise<void> {
const client = new OSS({ ...ossConfig! });
const result = await client.delete(fileName);
if (result.res.status !== 204) throw new Error('OSS 删除失败');
message.success(`文件 ${fileName} 已从 OSS 删除`);
}
export async function getOSSList(ossConfig: Props['ossConfig']): Promise<FileData[]> {
const client = new OSS({ ...ossConfig! });
const result = await client.list();
if (result.res.status !== 200) throw new Error('获取 OSS 文件列表失败');
return result.objects.map((r: any) => ({
uid: `${r.name}-${Date.now()}`,
fileId: r.name,
name: r.name,
thumbUrl: r.url,
url: r.url,
status: 'done',
percent: 100,
type: getFileType(r.name),
uploadTime: new Date(r.lastModified).getTime(),
}));
}
export function validateFile(file: File, accept: string, maxBytes: number): boolean {
const types = accept.split(',').map(t => t.trim());
const isValidType = types.some(t => file.name.endsWith(t));
const isValidSize = file.size / 1024 / 1024 < maxBytes;
if (!isValidType) message.warning('上传文件格式不支持');
if (!isValidSize) message.warning(`文件大小不能超过${maxBytes}MB`);
return isValidType && isValidSize;
}
优化亮点:
- uploadToOSS:规范化返回 FileData,支持进度更新。
- delfileFromOSS:移除回调,简化逻辑。
- getOSSList:直接返回文件列表,优化数据处理。
2. 主组件:AliUploader
javascript
import React, { useState, useEffect } from 'react';
import { Upload, Button, Collapse, Select, Modal, Input, message } from 'antd';
import { UploadOutlined } from '@ant-design/icons';
import { FileData, Props } from './index';
import { FileItem } from './FileItem';
import { uploadToOSS, delfileFromOSS, getOSSList, validateFile } from './utils';
import './index.less';
const { Panel } = Collapse;
const { Option } = Select;
const AliUploader: React.FC<Props> = ({
accept = '.doc,.docx,.xls,.xlsx,.pdf,.pptx,.png,.jpg',
uploadName = '上传文件',
listType = 'text',
maxCount = 1,
maxBytes = 20,
multiple = false,
fileList = [],
ossConfig,
showUploadList = true,
disabled = false,
extraTip,
showTips = true,
onChange,
onLoading,
onSuccess,
filedIds,
}) => {
const [uploadFileList, setUploadFileList] = useState<FileData[]>(fileList);
const [loading, setLoading] = useState(false);
const [sortBy, setSortBy] = useState<'time' | 'name'>('time');
const [selectedFiles, setSelectedFiles] = useState<string[]>([]);
const [batchEditVisible, setBatchEditVisible] = useState(false);
const [batchNote, setBatchNote] = useState('');
const handleUpload = async (file: File, files?: File[]) => {
const uploadFiles = multiple && files ? files : [file];
if (uploadFileList.length + uploadFiles.length > maxCount) {
message.warning(`最多上传${maxCount}个文件`);
return;
}
setLoading(true);
onLoading?.(true);
const newFiles = uploadFiles.map(f => ({
uid: `${Date.now()}-${Math.random()}`,
name: f.name,
thumbUrl: '',
status: 'uploading' as const,
percent: 0,
type: getFileType(f.name),
uploadTime: Date.now(),
}));
setUploadFileList(prev => (maxCount === 1 ? newFiles : [...prev, ...newFiles]));
try {
const results = await Promise.all(
uploadFiles.map((f, i) =>
uploadToOSS(f, ossConfig!, data =>
setUploadFileList(prev =>
prev.map(item => (item.uid === newFiles[i].uid ? { ...item, ...data } : item)),
),
),
),
);
const finalList = maxCount === 1 ? results : [...uploadFileList.filter(f => !newFiles.some(n => n.uid === f.uid)), ...results];
setUploadFileList(finalList);
onChange?.(finalList);
onSuccess?.(finalList);
filedIds?.(finalList.map(f => f.fileId));
message.success(`${results.length}个文件上传成功`);
} catch (error) {
setUploadFileList(prev =>
prev.map(f => (newFiles.some(n => n.uid === f.uid) ? { ...f, status: 'error' } : f)),
);
message.error('部分文件上传失败');
} finally {
setLoading(false);
onLoading?.(false);
}
};
const handleRemove = async (file: FileData) => {
try {
await delfileFromOSS(file.fileId, ossConfig!);
const newList = uploadFileList.filter(f => f.uid !== file.uid);
setUploadFileList(newList);
setSelectedFiles(prev => prev.filter(uid => uid !== file.uid));
onChange?.(newList);
filedIds?.(newList.map(f => f.fileId));
} catch (error) {
message.error('删除失败');
}
};
const handleEdit = (editedFile: FileData) => {
const newList = uploadFileList.map(f => (f.uid === editedFile.uid ? editedFile : f));
setUploadFileList(newList);
onChange?.(newList);
};
const handleSelect = (uid: string, selected: boolean) => {
setSelectedFiles(prev => (selected ? [...prev, uid] : prev.filter(id => id !== uid)));
};
const handleBatchEdit = () => {
if (selectedFiles.length === 0) {
message.warning('请先选择文件');
return;
}
setBatchEditVisible(true);
};
const applyBatchEdit = () => {
const newList = uploadFileList.map(f =>
selectedFiles.includes(f.uid) ? { ...f, note: batchNote } : f,
);
setUploadFileList(newList);
onChange?.(newList);
setBatchEditVisible(false);
setSelectedFiles([]);
setBatchNote('');
};
useEffect(() => {
if (ossConfig) {
getOSSList(ossConfig)
.then(files => setUploadFileList(prev => [...prev, ...files]))
.catch(() => message.error('获取 OSS 文件列表失败'));
}
}, [ossConfig]);
const groupedFiles = {
image: uploadFileList.filter(f => f.type === 'image'),
document: uploadFileList.filter(f => f.type === 'document'),
other: uploadFileList.filter(f => f.type === 'other'),
};
const sortFiles = (files: FileData[]) =>
files.sort((a, b) =>
sortBy === 'time' ? b.uploadTime - a.uploadTime : a.name.localeCompare(b.name),
);
return (
<div className="fileUpload">
<Upload.Dragger
accept={accept}
listType={listType as any}
maxCount={maxCount}
multiple={multiple}
beforeUpload={(file, fileList) => {
if (validateFile(file, accept, maxBytes)) {
handleUpload(file, fileList);
}
return false; // 阻止默认上传
}}
fileList={[]}
disabled={disabled || loading}
>
<Button icon={<UploadOutlined />} loading={loading} disabled={disabled}>
{uploadName}
</Button>
{showTips && (
<div className="tip">
{`支持${maxBytes}MB以内的${accept}文件(可拖拽上传,直接存至 OSS)`}
</div>
)}
</Upload.Dragger>
{showUploadList && (
<div>
<div style={{ margin: '10px 0' }}>
<Select value={sortBy} onChange={setSortBy} style={{ width: 120, marginRight: 10 }}>
<Option value="time">按时间排序</Option>
<Option value="name">按名称排序</Option>
</Select>
<Button onClick={handleBatchEdit} disabled={selectedFiles.length === 0}>
批量编辑 ({selectedFiles.length})
</Button>
</div>
<Collapse defaultActiveKey={['image', 'document', 'other']}>
{Object.entries(groupedFiles).map(([type, files]) =>
files.length > 0 ? (
<Panel
header={`${type === 'image' ? '图片' : type === 'document' ? '文档' : '其他'} (${files.length})`}
key={type}
>
{sortFiles(files).map(file => (
<FileItem
key={file.uid}
file={file}
onRemove={() => handleRemove(file)}
onEdit={handleEdit}
onSelect={handleSelect}
selected={selectedFiles.includes(file.uid)}
/>
))}
</Panel>
) : null,
)}
</Collapse>
</div>
)}
{extraTip && <div className="extraTip">{extraTip}</div>}
<Modal
open={batchEditVisible}
title={`批量编辑 (${selectedFiles.length} 个文件)`}
onOk={applyBatchEdit}
onCancel={() => setBatchEditVisible(false)}
>
<Input
value={batchNote}
onChange={e => setBatchNote(e.target.value)}
placeholder="输入统一备注"
/>
</Modal>
</div>
);
};
export default AliUploader;
css
.imageList {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 2px;
.deleteBtn {
color: rgba(0, 0, 0, 0.45);
}
}
.fileList {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 8px;
padding: 8px;
border: 1px solid #d9d9d9;
border-radius: 2px;
.deleteBtn {
color: rgba(0, 0, 0, 0.45);
visibility: hidden;
}
&:hover {
background-color: #f5f5f5;
.deleteBtn {
visibility: visible;
}
}
}
.fileName {
margin: 0 8px;
}
优化亮点:
- 上传逻辑:规范化状态管理,修复初始状态问题。
- 错误处理:增加 try-catch,提升健壮性。
- 性能优化:避免重复添加文件,提升列表更新效率。
Demo:项目文件上传器(OSS 版)
我们用一个"项目文件上传器"展示 AliUploader 的功能。
使用示例
如何使用这个组件?
该组件已集成到 react-nexlif 开源库中。 具体文档可参考详情文档。你可以通过以下方式引入并使用:
pnpm install react-nexlif
javascript
import React, { useState, useRef } from 'react';
import { AliUploader } from 'react-nexlif';
import { ApartmentOutlined } from '@ant-design/icons';
// import {ossConfig} from './utils';
const App: React.FC = () => {
const uploadRef = useRef(null);
const ossConfig = {
region: 'oss-cn-hangzhou',
accessKeyId: '',
accessKeySecret: '',
bucket: '',
};
const handleChange = (list: any[]) => {
// console.log('当前文件列表:', list);
};
const handleSuccess = (list: any[]) => {
// console.log('上传成功:', list);
list.forEach((file) => {
// console.log(`文件 ${file.name} 的 OSS 链接: ${file.url}`),
});
};
const handleIds = (ids: string[]) => {
console.log('文件ID:', ids);
};
return (
<div style={{ padding: '20px', maxWidth: '600px' }}>
<h2>项目文件上传器(OSS 版)</h2>
<AliUploader
accept=".doc,.docx,.pdf,.png,.jpg"
uploadName="上传项目文件到 OSS"
maxCount={5}
maxBytes={10}
multiple={true}
listType="picture"
showUploadList={true}
ossConfig={ossConfig}
onChange={handleChange}
onSuccess={handleSuccess}
filedIds={handleIds}
extraTip={
<p style={{ color: '#999' }}>
支持拖拽批量上传,直接存至 OSS,最多5个文件
</p>
}
/>
</div>
);
};
export default App;

使用效果
- 初始化:组件加载时从 OSS 获取已有文件列表。
- 上传:拖入 doc.pdf 和 image.png,文件上传至 OSS,显示进度。
- 分组:分为"图片"和"文档",可按时间或名称排序。
- 批量编辑:选中文件,添加备注"会议资料"。
- 删除:移除文件,同时清理 OSS。
- 链接返回:控制台打印 OSS 链接,如 https://web-xiaoyao.oss-cn-hangzhou.aliyuncs.com/xxx.png。
组件解析:云端快递的魔法
- OSS 集成 :
- uploadToOSS 上传文件,返回 OSS 链接。
- getOSSList 初始化云端文件。
- delfileFromOSS 删除云端文件。
- 上传体验 :
- 多线程上传,进度实时更新。
- 拖拽支持,操作直观。
- 文件管理 :
- 分组、排序、批量编辑,井然有序。
- 预览和删除,功能齐全。
使用场景与扩展
场景:
- 项目协作:上传文件到 OSS,共享链接。
- 内容管理:批量上传图片或文档。
- 云备份:自动同步到云端。
总结:你的云端"快递员"
AliUploader 就像一个"云端快递员",把文件快速送上 Aliyun OSS,返回链接随时取用。通过优化,我们让它更健壮、更高效,用"项目文件上传器"展示了它的实力。试着拖几个文件进去跑跑看,或者丢进你的项目玩一玩吧!有其他需求或创意?欢迎留言一起聊聊!
关键词:React 文件上传组件、AliUploader OSS、云存储链接、前端文件管理。