从零实现JSON与图片文件上传功能

在前端开发中,文件上传是一个常见但容易出错的功能点。本文将带你从零开始,一步步实现一个支持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>
  );
}

相关推荐
崔庆才丨静觅12 小时前
hCaptcha 验证码图像识别 API 对接教程
前端
passerby606113 小时前
完成前端时间处理的另一块版图
前端·github·web components
掘了13 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
崔庆才丨静觅13 小时前
实用免费的 Short URL 短链接 API 对接说明
前端
崔庆才丨静觅14 小时前
5分钟快速搭建 AI 平台并用它赚钱!
前端
崔庆才丨静觅14 小时前
比官方便宜一半以上!Midjourney API 申请及使用
前端
Moment14 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端
崔庆才丨静觅14 小时前
刷屏全网的“nano-banana”API接入指南!0.1元/张量产高清创意图,开发者必藏
前端
剪刀石头布啊14 小时前
jwt介绍
前端
爱敲代码的小鱼15 小时前
AJAX(异步交互的技术来实现从服务端中获取数据):
前端·javascript·ajax