在前端开发中,文件上传是一个常见但容易出错的功能点。本文将带你从零开始,一步步实现一个支持JSON和图片文件上传、预览的完整案例,帮助你深入理解文件处理的核心机制。
一、功能概述与技术选型
我们要实现的文件上传功能包含以下核心特性:
- 支持拖拽上传和点击上传两种方式
- 限制仅允许上传JSON文件和图片文件
- 实现文件大小限制(最大5MB)
- 提供图片预览功能
- 提供JSON内容解析与格式化显示功能
- 支持文件移除操作
技术选型:
- React + TypeScript 作为基础框架
- Ant Design 组件库提供UI支持,特别是
Upload.Dragger
组件 - FileReader API 处理文件读取操作
二、项目初始化与基础结构搭建
1. 创建基础组件
首先,我们创建一个基础的FileUploadDemo组件,并引入所需要的依赖
typescript
import React, { useState } from 'react';
import { Upload, message } from 'antd';
import type { UploadProps } from 'antd';
import { InboxOutlined } from '@ant-design/icons';
const { Dragger } = Upload;
const FileUploadDemo: React.FC = () => {
// 后续功能将在这里实现
return (
<div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
<h2>文件上传示例</h2>
{/* 后续将添加上传组件 */}
</div>
);
};
export default FileUploadDemo;
三、核心状态管理设计
在组件中,我们需要管理以下状态:
typescript
// 文件列表状态 - 用于Upload组件显示
const [fileList, setFileList] = useState<any[]>([]);
// 当前选中的单个文件
const [selectedFile, setSelectedFile] = useState<File | null>(null);
// 图片预览相关状态
const [previewImage, setPreviewImage] = useState<string>('');
const [previewOpen, setPreviewOpen] = useState<boolean>(false);
// JSON预览相关状态
const [jsonContent, setJsonContent] = useState<any>(null);
const [jsonModalOpen, setJsonModalOpen] = useState<boolean>(false);
这里有个小技巧:虽然我们限制一次只上传一个文件,但 Ant Design 的 Upload 组件要求 fileList
是数组格式,所以我们需要将单个文件转换为数组 [selectedFile]
提供给组件。
四、文件验证与选择功能实现
1. 文件类型与大小验证
我们首先实现 beforeUpload
函数,用于在文件上传前验证文件类型和大小:
ini
const beforeUpload = (file: File) => {
// 验证文件类型
const isJSON = file.type === 'application/json' || file.name.endsWith('.json');
const isImage = file.type.startsWith('image/');
const isAllowedType = isJSON || isImage;
if (!isAllowedType) {
message.error('只能上传JSON文件或图片文件!');
return Upload.LIST_IGNORE;
}
// 验证文件大小 (5MB)
const isLessThan5M = file.size / 1024 / 1024 < 5;
if (!isLessThan5M) {
message.error('文件大小不能超过5MB!');
return Upload.LIST_IGNORE;
}
// 处理选中的文件
handleFile(file);
// 返回false阻止自动上传
return false;
};
技术点说明:
file.type.startsWith('image/')
用于判断文件是否为图片类型- 对于JSON文件,我们同时检查
file.type
和文件扩展名.json
以提高兼容性 Upload.LIST_IGNORE
用于在验证失败时忽略当前文件- 最后返回
false
以阻止组件的自动上传行为,因为我们要实现自定义处理逻辑
2. 文件选择处理函数
接下来实现 handleFile
函数,用于处理用户选择的文件:
go
const handleFile = (file: File) => {
setSelectedFile(file);
setFileList([file]);
// 根据文件类型分别处理
if (file.type.startsWith('image/')) {
// 图片文件处理逻辑将在后续实现
} else if (file.type === 'application/json' || file.name.endsWith('.json')) {
// JSON文件处理逻辑将在后续实现
}
};
五、文件读取与预览功能实现
1. 图片文件预览实现
图片预览需要使用 FileReader API 的 readAsDataURL
方法,这是一个异步操作
ini
const handlePreview = () => {
if (selectedFile && selectedFile.type.startsWith('image/')) {
setPreviewOpen(true);
} else if (selectedFile && (selectedFile.type === 'application/json' || selectedFile.name.endsWith('.json'))) {
setJsonModalOpen(true);
}
};
// 更新handleFile函数,添加图片处理逻辑
const handleFile = (file: File) => {
setSelectedFile(file);
setFileList([file]);
// 根据文件类型分别处理
if (file.type.startsWith('image/')) {
// 创建FileReader实例
const reader = new FileReader();
// 设置onload事件处理函数 - 当文件读取完成后触发
reader.onload = (e) => {
// 将读取到的base64字符串设置到预览图片状态中
setPreviewImage(e.target?.result as string);
};
// 以DataURL形式读取文件 - 这是一个异步操作
reader.readAsDataURL(file);
} else if (file.type === 'application/json' || file.name.endsWith('.json')) {
// JSON文件处理逻辑将在后续实现
}
};
异步操作说明:
FileReader.readAsDataURL()
是一个异步方法,不会立即返回结果- 我们需要通过监听
onload
事件来获取读取完成后的结果 - 读取完成后,
e.target?.result
将包含图片的 Base64 编码字符
2. JSON文件内容解析实现
对于JSON文件,我们使用 FileReader.readAsText
方法读取文件内容,然后使用 JSON.parse
解析:
ini
// 更新handleFile函数,添加JSON处理逻辑
const handleFile = (file: File) => {
setSelectedFile(file);
setFileList([file]);
if (file.type.startsWith('image/')) {
// 图片处理逻辑...
} else if (file.type === 'application/json' || file.name.endsWith('.json')) {
const reader = new FileReader();
reader.onload = (e) => {
try {
// 先将文件内容转换为字符串
const jsonText = e.target?.result as string;
// 然后将JSON字符串解析为JavaScript对象
const parsedJson = JSON.parse(jsonText);
setJsonContent(parsedJson);
} catch (error) {
message.error('JSON文件解析错误');
console.error('JSON解析错误:', error);
}
};
// 以文本形式读取文件
reader.readAsText(file);
}
};
字符串操作说明:
FileReader.readAsText()
将文件内容读取为字符串JSON.parse()
将JSON字符串解析为JavaScript对象- 注意需要使用 try-catch 来捕获可能的JSON解析错误
六、文件移除功能实现
实现文件移除功能,清空相关状态:
ini
const handleRemove = () => {
setFileList([]);
setSelectedFile(null);
setPreviewImage('');
setJsonContent(null);
return true;
};
七、集成Ant Design Upload组件
将以上功能集成到Ant Design的Upload.Dragger组件中:
javascript
// 定义上传配置
const uploadProps: UploadProps = {
name: 'file',
multiple: false,
fileList,
beforeUpload,
onRemove: handleRemove,
customRequest: () => {}, // 自定义上传逻辑,这里为空实现
showUploadList: false, // 隐藏默认的上传列表,我们将自定义显示
};
// 在组件的返回部分添加Dragger组件
return (
<div style={{ maxWidth: 600, margin: '0 auto', padding: 20 }}>
<h2>文件上传示例</h2>
<Dragger {...uploadProps}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">点击或拖拽文件到此区域上传</p>
<p className="ant-upload-hint">
支持单个JSON或图片文件上传,最大5MB
</p>
</Dragger>
{/* 文件信息显示区域将在后续添加 */}
</div>
);
注意事项:
- 设置
multiple: false
确保一次只能上传一个文件 - 设置
showUploadList: false
隐藏默认上传列表,以便我们自定义显示方式 - 添加空的
customRequest
函数覆盖默认上传行为
八、自定义UI显示与预览功能
1. 添加文件信息显示
当用户选择文件后,我们显示文件信息并提供预览按钮:
less
// 在返回的JSX中添加文件信息显示区域
{selectedFile && (
<div style={{ marginTop: 20, padding: 16, border: '1px solid #d9d9d9', borderRadius: 4 }}>
<h3>文件信息</h3>
<p><strong>文件名:</strong> {selectedFile.name}</p>
<p><strong>大小:</strong> {(selectedFile.size / 1024).toFixed(2)} KB</p>
<p><strong>类型:</strong> {selectedFile.type || '未知'}</p>
<div style={{ marginTop: 16 }}>
<button
onClick={handlePreview}
style={{ marginRight: 8, padding: '4px 16px', backgroundColor: '#1890ff', color: 'white', border: 'none', borderRadius: 4 }}>
预览
</button>
<button
onClick={handleRemove}
style={{ padding: '4px 16px', backgroundColor: '#fff', color: '#d9d9d9', border: '1px solid #d9d9d9', borderRadius: 4 }}>
移除
</button>
</div>
</div>
)}
2. 添加预览弹窗
为图片和JSON文件添加预览弹窗:
ini
// 导入Modal组件
import { Upload, message, Modal } from 'antd';
// 在返回的JSX中添加预览弹窗
<Modal
open={previewOpen}
title="图片预览"
footer={null}
onCancel={() => setPreviewOpen(false)}
>
<img alt="预览" style={{ width: '100%' }} src={previewImage} />
</Modal>
<Modal
open={jsonModalOpen}
title="JSON内容"
width={700}
footer={null}
onCancel={() => setJsonModalOpen(false)}
>
<pre style={{ whiteSpace: 'pre-wrap', wordBreak: 'break-all' }}>
{JSON.stringify(jsonContent, null, 2)}
</pre>
</Modal>
字符串格式化说明:
JSON.stringify(jsonContent, null, 2)
用于将JSON对象格式化为易读的字符串- 第二个参数
null
表示不需要过滤任何属性 - 第三个参数
2
表示使用两个空格进行缩进
九、完整代码实现
综合以上所有功能,我们的完整代码如下:
typescript
import React, { useState } from "react";
import { Upload, Button, Typography, message, Modal, Tag, Card } from "antd";
import { CodeOutlined, FileOutlined, InboxOutlined } from "@ant-design/icons";
// typography 排版组件 需要展示标题段落列表文本样式
// Upload上传组件 用于上传文件(图片、json文件等)
const { Text, Title } = Typography;
const { Dragger } = Upload;
// 上传json文件的小案例
/*
文件上传演示组件
支持json和图片文件上传 并且提供预览功能
*/
export default function FileUploadDemo() {
//我们将在这里添加状态和功能
const [fileList, setFileList] = useState<File[]>([]);
const [selectedFile, setSelectedFile] = useState<File | null>(null);
//图片预览相关状态
const [previewOpen, setPreviewOpen] = useState(false);
const [previewImage, setPreviewImage] = useState("");
const [previewTitle, setPreviewTitle] = useState("");
//json文件预览相关状态
const [jsonContent, setJsonContent] = useState<any>("");
const [jsonPreviewOpen, setJsonPreviewOpen] = useState(false);
// 这些状态将帮助我们跟踪用户选择的文件
//文件类型验证
const beforeUpload = (file: File) => {
//获取文件类型
const isJSON =
file.type === "application/json" || file.name.endsWith(".json");
const isImage = file.type.startsWith("image/");
//限制文件格式
if (!isJSON && !isImage) {
message.error("仅支持上传json和图片文件");
return false;
}
//限制文件大小
const isLt5M = file.size / 1024 / 1024 < 5;
if (!isLt5M) {
message.error("文件大小不能超过5MB");
return false;
}
//只允许选择单个文件
if (fileList.length >= 1) {
message.error("只能上传一个文件");
return false;
}
//处理文件
handleFile(file);
return false; //防止自动上传
};
//处理文件
const handleFile = (file: File) => {
const newFileList = [file];
setFileList(newFileList);
setSelectedFile(file);
//如果是图片 设置预览
if (file.type.startsWith("image/")) {
const render = new FileReader();
render.readAsDataURL(file);
//将图片文件转换为base64编码的字符串
render.onload = () => {
setPreviewImage(render.result as string);
};
//读取完成后将结果转化为字符串保存到previewImage状态中
setJsonContent(null);
} else if (
file.type === "application/json" ||
file.name.endsWith(".json")
) {
const reader = new FileReader();
reader.readAsText(file, "utf-8");
//将json文件转换为字符串
reader.onload = (evt) => {
try {
const content = evt.target?.result as string;
const parsedJson = JSON.parse(content);
setJsonContent(parsedJson);
} catch (error) {
message.error("json文件格式错误");
setJsonContent(null);
}
};
//读取完成后将结果转化为字符串保存到jsonContent状态中
setPreviewImage("");
}
};
//预览图片
const handlePreview = () => {
if (selectedFile?.type.startsWith("image/")) {
setPreviewTitle(selectedFile.name);
setPreviewOpen(true);
}
};
//预览json文件
const handleJsonPreview = () => {
if (jsonContent) {
setJsonPreviewOpen(true);
}
};
//格式化JSON显示
const formatJson = (json: any): string => {
return JSON.stringify(json, null, 2);
};
//移除文件
const handleRemove = () => {
setFileList([]);
setSelectedFile(null);
setPreviewImage("");
};
//上传配置
const uploadProps = {
name: "file",
//像后端表单上传时的字段名
multiple: false,
//是否允许 一个选或者拖拽多个文件 false只允许一个
fileList: fileList.map((file) => ({
uid: Date.now().toString(),
name: file.name,
status: "done",
url: file.type.startsWith("image/") ? previewImage : undefined,
})),
beforeUpload,
onRemove: handleRemove,
onDrop: (e) => {
if (e.dataTransfer.files && e.dataTransfer.files.length > 0) {
handleFile(e.dataTransfer.files[0]);
}
},
onPreview: handlePreview,
//拖拽的话也执行处理文件
showUploadList: true,
customRequest: () => {}, //阻止自动上传
};
return (
<div style={{ maxWidth: "800px", margin: "0 auto" }}>
<Title level={2}>文件上传演示</Title>
<Text type="secondary">支持上传json和图片文件</Text>
<div style={{ marginTop: "24px" }}>
<Dragger {...uploadProps}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">点击或拖拽文件到这里上传</p>
<p className="ant-upload-hint">仅支持上传json和图片文件</p>
</Dragger>
</div>
{/* 显示文件信息和操作按钮 */}
{selectedFile && (
<Card
title="文件信息"
style={{ marginTop: 16 }}
extra={
selectedFile.type.startsWith("image/") ? (
<Button
type="primary"
icon={<FileOutlined />}
onClick={handlePreview}
>
预览图片
</Button>
) : jsonContent !== null ? (
<Button
type="primary"
icon={<CodeOutlined />}
onClick={handleJsonPreview}
>
查看JSON内容
</Button>
) : null
}
>
<p>
<strong>文件名:</strong>
{selectedFile.name}
</p>
<p>
<strong>文件大小:</strong>
{(selectedFile.size / 1024).toFixed(2)} KB
</p>
<p>
<strong>文件类型:</strong>
{selectedFile.type.startsWith("image/") ? (
<Tag color="blue">图片</Tag>
) : (
<Tag color="green">JSON</Tag>
)}
</p>
</Card>
)}
<Modal
open={previewOpen}
onCancel={() => setPreviewOpen(false)}
title={previewTitle}
footer={null}
>
<img src={previewImage} alt="预览" style={{ width: "100%" }} />
</Modal>
<Modal
open={jsonPreviewOpen}
onCancel={() => setJsonPreviewOpen(false)}
title="JSON文件预览"
footer={null}
>
<pre>{formatJson(jsonContent)}</pre>
</Modal>
</div>
);
}