• Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • Babel 插件通关秘籍
  • Git 原理详解及实用指南
  • Nest 通关秘籍
  • React 通关秘籍
  • TypeScript 全面进阶指南
  • TypeScript 类型体操通关秘籍
  • 现代CSS
  • React 通关秘籍

    • 1.关于本小册
    • 2.一网打尽组件常用Hook
    • 3.Hook的闭包陷阱的成因和解决方案
    • 4.React组件如何写TypeScript类型
    • 5.React组件如何调试
    • 6.受控模式VS非受控模式
    • 7.组件实战:迷你Calendar
    • 8.组件实战:Calendar日历组件(上)
    • 9.组件实战:Calendar日历组件(下)
    • 10.快速掌握Storybook
    • 11.React组件如何写单测
    • 12.深入理解Suspense和ErrorBoundary
    • 13.组件实战:Icon图标组件
    • 14.组件实战:Space间距组件
    • 15.React.Children和它的两种替代方案
    • 16.三个简单组件的封装
    • 17.浏览器的5种Observer
    • 18.组件实战:Watermark防删除水印组件
    • 19.手写react-lazyload
    • 20.图解网页的各种距离
    • 21.自定义hook练习
    • 22.自定义hook练习(二)
    • 23.用react-spring做弹簧动画
    • 24.react-spring结合use-gesture手势库实现交互动画
    • 25.用react-transition-group和react-spring做过渡动画
    • 26.快速掌握Tailwind:最流行的原子化CSS框架
    • 27.用CSSModules避免样式冲突
    • 28.CSSInJS:快速掌握styled-components
    • 29.react-spring实现滑入滑出的转场动画
    • 30.组件实战:Message全局提示组件
    • 31.组件实战:Popover气泡卡片组件
    • 32.项目里如何快速定位组件源码
    • 33.一次超爽的React调试体验
    • 34.组件实战:ColorPicker颜色选择器(一)
    • 35.组件实战:ColorPicker颜色选择器(二)
    • 36.组件实战:onBoarding漫游式引导组件
    • 37.组件实战:Upload拖拽上传
    • 38.组件实战:Form表单组件
    • 39.React组件库都是怎么构建的
    • 40.组件库实战:构建esm和cjs产物,发布到npm
    • 41.组件库实战:构建umd产物,通过unpkg访问
    • 42.数据不可变:immutable和immer
    • 43.基于ReactRouter实现keepalive
    • 44.Historyapi和ReactRouter实现原理
    • 45.ReactContext的实现原理和在antd里的应用
    • 46.ReactContext的性能缺点和解决方案
    • 47.手写一个Zustand
    • 48.原子化状态管理库Jotai
    • 49.用react-intl实现国际化
    • 50.国际化资源包如何通过Excel和GoogleSheet分享给产品经理
    • 51.基于react-dnd实现拖拽排序
    • 52.react-dnd实战:拖拽版TodoList
    • 53.ReactPlayground项目实战:需求分析、实现原理
    • 54.ReactPlayground项目实战:布局、代码编辑器
    • 55.ReactPlayground项目实战:多文件切换
    • 56.ReactPlayground项目实战:babel编译、iframe预览
    • 57.ReactPlayground项目实战:文件增删改
    • 58.ReactPlayground项目实战:错误显示、主题切换
    • 59.ReactPlayground项目实战:链接分享、代码下载
    • 60.ReactPlayground项目实战:WebWorker性能优化
    • 61.ReactPlayground项目实战:总结
    • 62.手写MiniReact:思路分析
    • 63.手写MiniReact:代码实现
    • 64.手写MiniReact:和真实React源码的对比
    • 65.React18的并发机制是怎么实现的
    • 66.Ref的实现原理
    • 67.低代码编辑器:核心数据结构、全局store
    • 68.低代码编辑器:拖拽组件到画布、拖拽编辑json
    • 69.低代码编辑器:画布区hover展示高亮框
    • 70.低代码编辑器:画布区click展示编辑框
    • 71.低代码编辑器:组件属性、样式编辑
    • 72.低代码编辑器:预览、大纲
    • 73.低代码编辑器:事件绑定
    • 74.低代码编辑器:动作弹窗
    • 75.低代码编辑器:自定义JS
    • 76.低代码编辑器:组件联动
    • 77.低代码编辑器:拖拽优化、Table组件
    • 78.低代码编辑器:Form组件、store持久化
    • 79.低代码编辑器:项目总结
    • 80.快速掌握ReactFlow画流程图
    • 81.ReactFlow振荡器调音:项目介绍
    • 82.ReactFlow振荡器调音:流程图绘制
    • 83.ReactFlow振荡器调音:合成声音
    • 84.AudioContext实现在线钢琴
    • 85.React服务端渲染:从SSR到hydrate
    • 86.小册总结

上传文件是常见的需求,我们经常用 antd 的 Upload 组件来实现。

它有一个上传按钮,下面是上传的文件列表的状态:

并且,还支持拖拽上传:

这节我们就来实现下这个 Upload 组件。

npx create-vite

用 create-vite 创建个 react 项目。

去掉 index.css 和 StrictMode

然后把开发服务跑起来:

npm install
npm run dev

访问下试试:

然后我们先用下 antd 的 Upload 组件:

npm i --save antd

改下 App.tsx

import React from "react";
import { UploadOutlined } from "@ant-design/icons";
import type { UploadProps } from "antd";
import { Button, message, Upload } from "antd";

const props: UploadProps = {
    name: "file",
    action: "https://run.mocky.io/v3/435e224c-44fb-4773-9faf-380c5e6a2188",
    headers: {},
    onChange(info) {
        if (info.file.status !== "uploading") {
            console.log(info.file, info.fileList);
        }
        if (info.file.status === "done") {
            message.success(`${info.file.name} file uploaded successfully`);
        } else if (info.file.status === "error") {
            message.error(`${info.file.name} file upload failed.`);
        }
    },
};

const App: React.FC = () => (
    <Upload {...props}>
        <Button icon={<UploadOutlined />}>Click to Upload</Button>
    </Upload>
);

export default App;

现在接口是 mock 的,这样不过瘾,我们用 express 起个服务来接收下文件。

根目录下新建 server.js

import express from "express";
import multer from "multer";
import cors from "cors";

const app = express();
app.use(cors());

const upload = multer({
    dest: "uploads/",
});

app.post("/upload", upload.single("file"), function (req, res, next) {
    console.log("req.file", req.file);
    console.log("req.body", req.body);

    res.end(
        JSON.stringify({
            message: "success",
        })
    );
});

app.listen(3333);

用 express 跑服务,然后用 cors 处理跨域请求,用 multer 来接收文件。

指定 dest 为 uploads 目录。

安装依赖,然后用 node 跑一下:

npm i --save express cors multer

node ./server.js

这里 node 能直接跑 es module 的代码是因为 package.json 里指定了 type 为 module:

也就是说默认所有 js 都是 es module 的。

然后改下上传路径:

试一下:

上传成功,服务端也接收到了文件:

只不过现在的文件名没有带后缀名,我们可以自定义一下:

import express from "express";
import multer from "multer";
import cors from "cors";
import path from "path";
import fs from "fs";

const app = express();
app.use(cors());

const storage = multer.diskStorage({
    destination: function (req, file, cb) {
        try {
            fs.mkdirSync(path.join(process.cwd(), "uploads"));
        } catch (e) {}
        cb(null, path.join(process.cwd(), "uploads"));
    },
    filename: function (req, file, cb) {
        const uniqueSuffix =
            Date.now() +
            "-" +
            Math.round(Math.random() * 1e9) +
            "-" +
            file.originalname;
        cb(null, uniqueSuffix);
    },
});
const upload = multer({
    dest: "uploads/",
    storage,
});

app.post("/upload", upload.single("file"), function (req, res, next) {
    console.log("req.file", req.file);
    console.log("req.body", req.body);

    res.end(
        JSON.stringify({
            message: "success",
        })
    );
});

app.listen(3333);

自定义 storage,指定文件存储的目录以及文件名。

重新跑下服务,然后再次上传:

现在,文件保存的路径就改了

上传的图片也能正常打开:

接口搞定之后,我们自己来实现下这个 Upload 组件。

新建 Upload/index.tsx

import { FC, useRef, ChangeEvent, PropsWithChildren } from "react";
import axios from "axios";

import "./index.scss";

export interface UploadProps extends PropsWithChildren {
    action: string;
    headers?: Record<string, any>;
    name?: string;
    data?: Record<string, any>;
    withCredentials?: boolean;
    accept?: string;
    multiple?: boolean;
}

export const Upload: FC<UploadProps> = (props) => {
    const {
        action,
        name,
        headers,
        data,
        withCredentials,
        accept,
        multiple,
        children,
    } = props;

    const fileInput = useRef<HTMLInputElement>(null);

    const handleClick = () => {
        if (fileInput.current) {
            fileInput.current.click();
        }
    };

    const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
        const files = e.target.files;
        if (!files) {
            return;
        }
        uploadFiles(files);
        if (fileInput.current) {
            fileInput.current.value = "";
        }
    };

    const uploadFiles = (files: FileList) => {
        let postFiles = Array.from(files);
        postFiles.forEach((file) => {
            post(file);
        });
    };

    const post = (file: File) => {
        const formData = new FormData();

        formData.append(name || "file", file);
        if (data) {
            Object.keys(data).forEach((key) => {
                formData.append(key, data[key]);
            });
        }

        axios.post(action, formData, {
            headers: {
                ...headers,
                "Content-Type": "multipart/form-data",
            },
            withCredentials,
        });
    };

    return (
        <div className="upload-component">
            <div className="upload-input" onClick={handleClick}>
                {children}
                <input
                    className="upload-file-input"
                    type="file"
                    ref={fileInput}
                    onChange={handleFileChange}
                    accept={accept}
                    multiple={multiple}
                />
            </div>
        </div>
    );
};

export default Upload;

还有 Upload/index.scss

.upload-input {
    display: inline-block;
}
.upload-file-input {
    display: none;
}

这些参数都很容易理解:

action 是上传的 url

headers 是携带的请求头

data 是携带的数据

name 是文件的表单字段名

accept 是 input 接受的文件格式

multiple 是 input 可以多选

然后渲染 children 外加一个隐藏的 file input

onChange 的时候,拿到所有 files 依次上传,之后把 file input 置空:

用 axios 来发送 post 请求,携带 FormData 数据,包含 file 和其它 data 字段:

再就是点击其它区域也触发 file input 的点击:

安装用到的 axios 包:

npm install --save axios

改下 App.tsx

import React from "react";
import { UploadOutlined } from "@ant-design/icons";
import { Button } from "antd";
import Upload, { UploadProps } from "./Upload";

const props: UploadProps = {
    name: "file",
    action: "http://localhost:3333/upload",
};

const App: React.FC = () => (
    <Upload {...props}>
        <Button icon={<UploadOutlined />}>Click to Upload</Button>
    </Upload>
);

export default App;

这里内层的 Button、Icon 还是用 antd 的,只是把 Upload 组件换成我们自己实现的。

然后测试下:

虽然界面还没加啥反馈,但请求已经发送成功了:

服务端也接受到了这个文件:

上传功能没问题,然后我们添加几个上传过程中的回调函数:

beforeUpload 是上传之前的回调,如果返回 false 就不上传,也可以返回 promise,比如在服务端校验的时候,等 resolve 之后才会上传

antd 的 Upload 组件就是这样的:

onProgress 是进度更新时的回调,可以拿到进度。

onSuccess 和 onError 是上传成功、失败时的回调。

onChange 是上传状态改变时的回调。

这几个回调分别在上传前、进度更新、成功、失败时调用:

import { FC, useRef, ChangeEvent, PropsWithChildren } from "react";
import axios from "axios";

import "./index.scss";

export interface UploadProps extends PropsWithChildren {
    action: string;
    headers?: Record<string, any>;
    name?: string;
    data?: Record<string, any>;
    withCredentials?: boolean;
    accept?: string;
    multiple?: boolean;
    beforeUpload?: (file: File) => boolean | Promise<File>;
    onProgress?: (percentage: number, file: File) => void;
    onSuccess?: (data: any, file: File) => void;
    onError?: (err: any, file: File) => void;
    onChange?: (file: File) => void;
}

export const Upload: FC<UploadProps> = (props) => {
    const {
        action,
        name,
        headers,
        data,
        withCredentials,
        accept,
        multiple,
        children,
        beforeUpload,
        onProgress,
        onSuccess,
        onError,
        onChange,
    } = props;

    const fileInput = useRef<HTMLInputElement>(null);

    const handleClick = () => {
        if (fileInput.current) {
            fileInput.current.click();
        }
    };

    const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
        const files = e.target.files;
        if (!files) {
            return;
        }
        uploadFiles(files);
        if (fileInput.current) {
            fileInput.current.value = "";
        }
    };

    const uploadFiles = (files: FileList) => {
        let postFiles = Array.from(files);
        postFiles.forEach((file) => {
            if (!beforeUpload) {
                post(file);
            } else {
                const result = beforeUpload(file);
                if (result && result instanceof Promise) {
                    result.then((processedFile) => {
                        post(processedFile);
                    });
                } else if (result !== false) {
                    post(file);
                }
            }
        });
    };

    const post = (file: File) => {
        const formData = new FormData();

        formData.append(name || "file", file);
        if (data) {
            Object.keys(data).forEach((key) => {
                formData.append(key, data[key]);
            });
        }

        axios
            .post(action, formData, {
                headers: {
                    ...headers,
                    "Content-Type": "multipart/form-data",
                },
                withCredentials,
                onUploadProgress: (e) => {
                    let percentage =
                        Math.round((e.loaded * 100) / e.total!) || 0;
                    if (percentage < 100) {
                        if (onProgress) {
                            onProgress(percentage, file);
                        }
                    }
                },
            })
            .then((resp) => {
                onSuccess?.(resp.data, file);
                onChange?.(file);
            })
            .catch((err) => {
                onError?.(err, file);
                onChange?.(file);
            });
    };

    return (
        <div className="upload-component">
            <div className="upload-input" onClick={handleClick}>
                {children}
                <input
                    className="upload-file-input"
                    type="file"
                    ref={fileInput}
                    onChange={handleFileChange}
                    accept={accept}
                    multiple={multiple}
                />
            </div>
        </div>
    );
};

export default Upload;

在 App.tsx 里传入对应参数:

import React from "react";
import { UploadOutlined } from "@ant-design/icons";
import { Button } from "antd";
import Upload, { UploadProps } from "./Upload";

const props: UploadProps = {
    name: "file",
    action: "http://localhost:3333/upload",
    beforeUpload(file) {
        if (file.name.includes("1.image")) {
            return false;
        }
        return true;
    },
    onSuccess(ret) {
        console.log("onSuccess", ret);
    },
    onError(err) {
        console.log("onError", err);
    },
    onProgress(percentage, file) {
        console.log("onProgress", percentage);
    },
    onChange(file) {
        console.log("onChange", file);
    },
};

const App: React.FC = () => (
    <Upload {...props}>
        <Button icon={<UploadOutlined />}>Click to Upload</Button>
    </Upload>
);

export default App;

包含 1.image 的文件返回 false,其余的返回 true

跑一下:

网速快的时候没有上传进度,改下网络设置:

几个回调函数都没问题。

接下来我们添加下面的文件列表:

新建 Upload/UploadList.tsx

import { FC } from "react";
import { Progress } from "antd";
import {
    CheckOutlined,
    CloseOutlined,
    DeleteOutlined,
    FileOutlined,
    LoadingOutlined,
} from "@ant-design/icons";

export interface UploadFile {
    uid: string;
    size: number;
    name: string;
    status?: "ready" | "uploading" | "success" | "error";
    percent?: number;
    raw?: File;
    response?: any;
    error?: any;
}

interface UploadListProps {
    fileList: UploadFile[];
    onRemove: (file: UploadFile) => void;
}

export const UploadList: FC<UploadListProps> = (props) => {
    const { fileList, onRemove } = props;

    return (
        <ul className="upload-list">
            {fileList.map((item) => {
                return (
                    <li
                        className={`upload-list-item upload-list-item-${item.status}`}
                        key={item.uid}>
                        <span className="file-name">
                            {(item.status === "uploading" ||
                                item.status === "ready") && <LoadingOutlined />}
                            {item.status === "success" && <CheckOutlined />}
                            {item.status === "error" && <CloseOutlined />}
                            {item.name}
                        </span>
                        <span className="file-actions">
                            <DeleteOutlined
                                onClick={() => {
                                    onRemove(item);
                                }}
                            />
                        </span>
                        {item.status === "uploading" && (
                            <Progress percent={item.percent || 0} />
                        )}
                    </li>
                );
            })}
        </ul>
    );
};

export default UploadList;

这个组件传入 UploadFile 的数组和 onRemove 回调作为参数:

UploadFile 里除了文件信息外,还有 status、response、error

上传状态 status 有 ready、uploading、success、error 四种。

然后把 UploadFile 数组渲染出来:

显示文件名、进度、删除按钮等。

点击删除的时候调用 onRemove 回调。

然后在 index.scss 里添加对应的样式:

.upload-input {
    display: inline-block;
}
.upload-file-input {
    display: none;
}

.upload-list {
    margin: 0;
    padding: 0;
    list-style-type: none;
}
.upload-list-item {
    margin-top: 5px;

    font-size: 14px;
    line-height: 2em;
    font-weight: bold;

    box-sizing: border-box;
    min-width: 200px;

    position: relative;

    &-success {
        color: blue;
    }

    &-error {
        color: red;
    }

    .file-name {
        .anticon {
            margin-right: 10px;
        }
    }

    .file-actions {
        display: none;

        position: absolute;
        right: 7px;
        top: 0;

        cursor: pointer;
    }

    &:hover {
        .file-actions {
            display: block;
        }
    }
}

在 Upload/index.tsx 里引入试试:

用 mock 的数据渲染 UploadList

const fileList: UploadFile[] = [
    {
        uid: "11",
        size: 111,
        name: "xxxx",
        status: "uploading",
        percent: 50,
    },
    {
        uid: "22",
        size: 111,
        name: "yyy",
        status: "success",
        percent: 50,
    },
    {
        uid: "33",
        size: 111,
        name: "zzz",
        status: "error",
        percent: 50,
    },
];

return (
    <div className="upload-component">
        <div className="upload-input" onClick={handleClick}>
            {children}
            <input
                className="upload-file-input"
                type="file"
                ref={fileInput}
                onChange={handleFileChange}
                accept={accept}
                multiple={multiple}
            />
        </div>

        <UploadList fileList={fileList} onRemove={() => {}} />
    </div>
);

浏览器看一下:

没啥问题。

然后把数据变成动态的:

声明一个 fileList 的 state,并封装一个更新它的方法:

在状态改变的时候调用更新方法来更新 fileList:

并且添加一个 onRemove 的回调:

在点击删除按钮的时候调用:

import { FC, useRef, ChangeEvent, PropsWithChildren, useState } from "react";
import axios from "axios";

import "./index.scss";
import UploadList, { UploadFile } from "./UploadList";

export interface UploadProps extends PropsWithChildren {
    action: string;
    headers?: Record<string, any>;
    name?: string;
    data?: Record<string, any>;
    withCredentials?: boolean;
    accept?: string;
    multiple?: boolean;
    beforeUpload?: (file: File) => boolean | Promise<File>;
    onProgress?: (percentage: number, file: File) => void;
    onSuccess?: (data: any, file: File) => void;
    onError?: (err: any, file: File) => void;
    onChange?: (file: File) => void;
    onRemove?: (file: UploadFile) => void;
}

export const Upload: FC<UploadProps> = (props) => {
    const {
        action,
        name,
        headers,
        data,
        withCredentials,
        accept,
        multiple,
        children,
        beforeUpload,
        onProgress,
        onSuccess,
        onError,
        onChange,
        onRemove,
    } = props;

    const fileInput = useRef<HTMLInputElement>(null);

    const [fileList, setFileList] = useState<Array<UploadFile>>([]);

    const updateFileList = (
        updateFile: UploadFile,
        updateObj: Partial<UploadFile>
    ) => {
        setFileList((prevList) => {
            return prevList.map((file) => {
                if (file.uid === updateFile.uid) {
                    return { ...file, ...updateObj };
                } else {
                    return file;
                }
            });
        });
    };

    const handleRemove = (file: UploadFile) => {
        setFileList((prevList) => {
            return prevList.filter((item) => item.uid !== file.uid);
        });
        if (onRemove) {
            onRemove(file);
        }
    };

    const handleClick = () => {
        if (fileInput.current) {
            fileInput.current.click();
        }
    };

    const handleFileChange = (e: ChangeEvent<HTMLInputElement>) => {
        const files = e.target.files;
        if (!files) {
            return;
        }
        uploadFiles(files);
        if (fileInput.current) {
            fileInput.current.value = "";
        }
    };

    const uploadFiles = (files: FileList) => {
        let postFiles = Array.from(files);
        postFiles.forEach((file) => {
            if (!beforeUpload) {
                post(file);
            } else {
                const result = beforeUpload(file);
                if (result && result instanceof Promise) {
                    result.then((processedFile) => {
                        post(processedFile);
                    });
                } else if (result !== false) {
                    post(file);
                }
            }
        });
    };

    const post = (file: File) => {
        let uploadFile: UploadFile = {
            uid: Date.now() + "upload-file",
            status: "ready",
            name: file.name,
            size: file.size,
            percent: 0,
            raw: file,
        };
        setFileList((prevList) => {
            return [uploadFile, ...prevList];
        });

        const formData = new FormData();

        formData.append(name || "file", file);
        if (data) {
            Object.keys(data).forEach((key) => {
                formData.append(key, data[key]);
            });
        }

        axios
            .post(action, formData, {
                headers: {
                    ...headers,
                    "Content-Type": "multipart/form-data",
                },
                withCredentials,
                onUploadProgress: (e) => {
                    let percentage =
                        Math.round((e.loaded * 100) / e.total!) || 0;
                    if (percentage < 100) {
                        updateFileList(uploadFile, {
                            percent: percentage,
                            status: "uploading",
                        });

                        if (onProgress) {
                            onProgress(percentage, file);
                        }
                    }
                },
            })
            .then((resp) => {
                updateFileList(uploadFile, {
                    status: "success",
                    response: resp.data,
                });

                onSuccess?.(resp.data, file);
                onChange?.(file);
            })
            .catch((err) => {
                updateFileList(uploadFile, { status: "error", error: err });

                onError?.(err, file);
                onChange?.(file);
            });
    };

    return (
        <div className="upload-component">
            <div className="upload-input" onClick={handleClick}>
                {children}
                <input
                    className="upload-file-input"
                    type="file"
                    ref={fileInput}
                    onChange={handleFileChange}
                    accept={accept}
                    multiple={multiple}
                />
            </div>

            <UploadList fileList={fileList} onRemove={handleRemove} />
        </div>
    );
};

export default Upload;

大功告成,我们测试下:

文件上传状态没问题,服务端也收到了上传的文件。

至此,我们的 Upload 组件就完成了。

然后我们再加上拖拽上传的功能:

创建 Upload/Dragger.tsx

import { FC, useState, DragEvent, PropsWithChildren } from "react";
import classNames from "classnames";

interface DraggerProps extends PropsWithChildren {
    onFile: (files: FileList) => void;
}

export const Dragger: FC<DraggerProps> = (props) => {
    const { onFile, children } = props;

    const [dragOver, setDragOver] = useState(false);

    const cs = classNames("upload-dragger", {
        "is-dragover": dragOver,
    });

    const handleDrop = (e: DragEvent<HTMLElement>) => {
        e.preventDefault();
        setDragOver(false);
        onFile(e.dataTransfer.files);
    };

    const handleDrag = (e: DragEvent<HTMLElement>, over: boolean) => {
        e.preventDefault();
        setDragOver(over);
    };

    return (
        <div
            className={cs}
            onDragOver={(e) => {
                handleDrag(e, true);
            }}
            onDragLeave={(e) => {
                handleDrag(e, false);
            }}
            onDrop={handleDrop}>
            {children}
        </div>
    );
};

export default Dragger;

因为拖拽文件到这里的时候,会有对应的样式,所以我们要在 dragover 和 dragleave 的时候分别设置不同的 dragOver 状态值,然后更改 className

然后在 drop 的时候,把文件传给 onFile 回调函数:

在 index.scss 里加上它的样式:

.upload-dragger {
    background: #eee;
    border: 1px dashed #aaa;
    border-radius: 4px;
    cursor: pointer;
    padding: 20px;
    width: 200px;
    height: 100px;
    text-align: center;

    &.is-dragover {
        border: 2px dashed blue;
        background: rgba(blue, 0.3);
    }
}

然后在 Upload/index.tsx 引入 Dragger 组件:

{
    drag ? (
        <Dragger
            onFile={(files) => {
                uploadFiles(files);
            }}>
            {children}
        </Dragger>
    ) : (
        children
    );
}

当传入 drag 参数的时候,渲染 dragger 组件,onFile 回调里调用 uploadFiles 方法来上传。

在 index.tsx 里试试:

浏览器访问下:

没啥问题。

可以改下 Upload 组件的 children:

const App: React.FC = () => (
    <Upload {...props} drag>
        <p>
            <InboxOutlined style={{ fontSize: "50px" }} />
        </p>
        <p>点击或者拖拽文件到此处</p>
    </Upload>
);

这样,拖拽上传就完成了。

案例代码上传了小册仓库

总结

今天我们实现了 Upload 组件。

首先用 express + multer 跑的服务端,创建 /upload 接口来接收文件。

然后在 Upload 组件里调用 axios,上传包含 file 的 FormData。

之后加上了 beforeUpload、onProgress、onSuccess、onChange 等回调函数。

最后又加上了 UploadList 来可视化展示上传文件的状态。

然后实现了 Dragger 组件,可以拖拽文件来上传。

这样,我们就实现了 Upload 组件。

上次更新: 6/21/25, 9:42 AM
贡献者: YNight
Prev
36.组件实战:onBoarding漫游式引导组件
Next
38.组件实战:Form表单组件