从零实现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>
  );
}

相关推荐
WebGirl5 小时前
动态生成多层表头表格算法
前端·javascript
hywel6 小时前
一开始只是想整理下书签,结果做成了一个 AI 插件 😂
前端
傅里叶6 小时前
SchedulerBinding 的三个Frame回调
前端·flutter
小小前端_我自坚强6 小时前
React Hooks 使用详解
前端·react.js·redux
java水泥工6 小时前
基于Echarts+HTML5可视化数据大屏展示-车辆综合管控平台
前端·echarts·html5·大屏模版
aklry6 小时前
elpis之学习总结
前端·vue.js
笔尖的记忆6 小时前
【前端架构和框架】react中Scheduler调度原理
前端·面试
_advance6 小时前
我是怎么把 JavaScript 的 this 和箭头函数彻底搞明白的——个人学习心得
前端
右子6 小时前
React 编程的优雅艺术:从设计到实现
前端·react.js·mobx