zoukankan      html  css  js  c++  java
  • 从零开始的野路子React/Node(9)Antd + multer实现文件上传

    最近心血来潮,打算自己捣腾个web app来练练手(虽然大概率会半路弃坑……),其中有一部分是关于文件上传的,在实现的过程中遇到了一些坑,于是打算把血泪教训都记录下来。

    前端的部分采用了Antd,不得不说真是香,至少比我司内部的前端库香了1000倍……事半功倍。后端部分则主要通过multer来实现,目测应该是一种比较通用的做法?

    1、捯饬前端

    首先我们新建一个upload_file文件夹,在里面放我们的前端后端各种东西。

    而后老规矩,通过create-react-app uf_ui来创建前端部分。npm install antd来安装一下antd。

    然后我想要实现的功能是页面上一个按钮,点击按钮会弹出对话框(Modal),在其中上传文件,并可以加入一些其他的备注等等。那么我们先来准备一下对话框的部分。

    我们在src目录下新建components,然后新建2个文件,ModalContainer.js作为对话框容器,而UploadFile.js负责对话框内部的内容:

     

    ModalContainer.js的内容比较简单:

     1 import React, { useState } from 'react';
     2 import { Modal, Button } from 'antd';
     3 
     4 export default function ModalContainer(props) {
     5     const [visible, setVisible] = useState(false); //是否显示模态框
     6     
     7     const showModal = () => {
     8         setVisible(true); //显示
     9     };
    10 
    11     const handleCancel = () => {
    12         setVisible(false); //隐藏
    13     };
    14 
    15     return (
    16         <>
    17             <Button
    18             type={props.buttonType} 
    19             icon={props.icon} 
    20             onClick={showModal}>
    21                 {props.title}
    22             </Button>
    23 
    24             <Modal
    25             title={props.title}
    26             visible={visible}
    27             footer={null}
    28             onCancel={handleCancel}
    29             {...props}
    30             >
    31                 {React.cloneElement(props.content, 
    32                     { handleCancel: handleCancel })}
    33             </Modal>
    34         </>
    35     )
    36 }

    其中Button是页面载入后会显示的按钮,点击就会弹出对话框;而Modal则负责对话框本体,我们将footer设置成null来去掉默认的取消和确认按钮(也就是生成一个没有按钮的对话框)。此外,我们设置了ModalContainer将使用content参数来传入一个组件以显示内部的内容。

    但是,由于我们去掉了取消和确认按钮,我们必须把关闭对话框的任务也交给content传入的组件来做(也就是说content的组件会包含取消和确认按钮),因此,我们必须把handleCancel这个函数传入content组件中,作为其props的一部分,React.cloneElement就实现了这一点,它将完整地复制content组件,并将handleCancel也注入它的props中。这样一来,我们在写content组件时,就能通过调用props.handleCancel来关闭对话框了。

    接下来,我们把组件加入到App.js中:

     1 import './App.css';
     2 import { UploadOutlined } from '@ant-design/icons';
     3 import ModalContainer from "./components/ModalContainer";
     4 import UploadFile from "./components/UploadFile";
     5 
     6 function App() {
     7   return (
     8     <div className="App">
     9       <ModalContainer 
    10       icon={<UploadOutlined/>} 
    11       title="上传文件" 
    12       width={600}
    13       content={<UploadFile/>}/>
    14     </div>
    15   );
    16 }
    17 
    18 export default App;

    可以看到我们将UploadFile组件传入了content,作为对话框的内容。由于我们还没写任何东西,所以弹出后会是一片空白。此外,注意antd的图标库和antd本体在import时是分开的(虽然不用单独安装)。此外,想使用antd的样式的话,可以在App.css第一行前加入一句:@import 'antd/dist/antd.css';

    npm start之后可以看到一个按钮,点击后一篇空白。

     

    接下来我们来写一下UploadFile.js,对于UploadFile,我希望它是一个表单,有一个上传文件的按钮,有一个输入框来负责自定义文件名称,另一个用来写一些备注,我们先把这个表单整出来:

     1 import { 
     2     Upload, 
     3     Form, 
     4     Button, 
     5     Input } from 'antd';
     6 import { UploadOutlined } from '@ant-design/icons';
     7 
     8 const layout = {
     9     labelCol: { span: 6 },
    10     wrapperCol: { span: 18 },
    11 }; //表单的整体布局
    12 const tailLayout = {
    13     wrapperCol: { offset: 16, span: 8 },
    14 }; //取消和确定按钮的布局
    15 
    16 const { TextArea } = Input;
    17 
    18 export default function UploadFile(props) {
    19     const [form] = Form.useForm(); //用于之后取数据
    20 
    21     return (
    22         <>
    23             <Form 
    24             {...layout}
    25             form={form}
    26             name="upload_file">
    27 
    28                 <Form.Item
    29                 label="文件名称"
    30                 name="name"
    31                 rules={[{ required: true,
    32                     message: "请输入文件名" }]}>
    33                     <Input/>
    34                 </Form.Item>
    35 
    36                 <Form.Item
    37                 label="备注"
    38                 name="desc"
    39                 rules={[{ required: false,
    40                     message: "请输入备注" }]}>
    41                     <TextArea rows={4}/>
    42                 </Form.Item>
    43 
    44                 <Form.Item
    45                 label="上传文件"
    46                 name="file"
    47                 rules={[{ required: true,
    48                     message: "请上传文件" }]}>
    49                     <Upload>
    50                         <Button icon={<UploadOutlined />}>开始上传</Button>
    51                     </Upload>
    52                 </Form.Item>
    53 
    54             </Form>
    55         </>
    56     )
    57 }

    表单中一共3样东西,name对应文件名的输入,desc对应备注,file对应一个上传文件的按钮。此时我们再看页面,点击“上传文件”按钮,会看到表单弹出:

     

    点击其中的开始上传,就可以选择文件了。随便上传一个试试,发现失败(红色):

     

    那是因为我们还没有配置上传的request。此外,我们发现对话框没有确认和取消按钮,只能靠大叉退出,我们可以先通过<Form.Item>加上:

     1 import { 
     2     Upload, 
     3     Form, 
     4     Button, 
     5     Input, 
     6     Space } from 'antd';
     7 import { UploadOutlined } from '@ant-design/icons';
     8 
     9 const layout = {
    10     labelCol: { span: 6 },
    11     wrapperCol: { span: 18 },
    12 }; //表单的整体布局
    13 const tailLayout = {
    14     wrapperCol: { offset: 16, span: 8 },
    15 }; //取消和确定按钮的布局
    16 
    17 const { TextArea } = Input;
    18 
    19 export default function UploadFile(props) {
    20     const [form] = Form.useForm(); //用于之后取数据
    21 
    22     return (
    23         <>
    24             <Form 
    25             {...layout}
    26             form={form}
    27             name="upload_file">
    28 
    29                 <Form.Item
    30                 label="文件名称"
    31                 name="name"
    32                 rules={[{ required: true,
    33                     message: "请输入文件名" }]}>
    34                     <Input/>
    35                 </Form.Item>
    36 
    37                 <Form.Item
    38                 label="备注"
    39                 name="desc"
    40                 rules={[{ required: false,
    41                     message: "请输入备注" }]}>
    42                     <TextArea rows={4}/>
    43                 </Form.Item>
    44 
    45                 <Form.Item
    46                 label="上传文件"
    47                 name="file"
    48                 rules={[{ required: true,
    49                     message: "请上传文件" }]}>
    50                     <Upload>
    51                         <Button icon={<UploadOutlined />}>开始上传</Button>
    52                     </Upload>
    53                 </Form.Item>
    54 
    55                 <Form.Item 
    56                 {...tailLayout}>
    57                     <Space>
    58                         <Button type="primary" htmlType="submit">
    59                             确认
    60                         </Button>
    61                         <Button onClick={props.handleCancel}>
    62                             取消
    63                         </Button>
    64                     </Space>
    65                 </Form.Item>
    66             </Form>
    67         </>
    68     )
    69 }

    可以看到取消按钮的onClick会调用props.handleCancel,也就是我们刚才在ModalContainer中所做的注入。现在对话框里应该多了2个按钮,点击“取消”会关闭对话框,而点击确认则会提示表单还没填完(因为这两项我们设置了required: true):

     

    除此之外,我们还需要有个函数来负责点击“确认”按钮后的一系列步骤,我们自然希望点击确认后会提交表单,然后关闭窗口我们可以用axios来负责处理相关的request,但更重要的问题是,我们如何获得表单内填写的内容呢?antd的表单提供了getFieldValue和getFieldsValue这两个函数,但在实际使用过程中总是无法正确地取到值(感觉这俩更像是获取props而非states),不过我们可以通过自己写一个handleSubmit函数,并使之与表单的onFinish挂钩来完成取值过程。

    …
    export default function UploadFile(props) {
        const [form] = Form.useForm(); //用于之后取数据
    
        const handleSubmit = values => {
            console.log(values)
            props.handleCancel();
        }
    
        return (
            <>
                <Form 
                {...layout}
                form={form}
                name="upload_file"
                onFinish={handleSubmit}></Form>
            </>
        )
    }

    当我们提交表单时,会触发onFinish,而它会调用handleSubmit来记录下表单的值,并且关闭对话框,我们来测试一下看看:

     OK,我们成功地记录下了表单的值(虽然我们的文件并没有真正地传到后台)。

    2、动态获取文件名

    接下来我想做的一件事是,我每次只上传一个文件,当上传文件时,文件名称一栏会自动改成该文件的名称(不包括扩展名),这样一来我可以直接采用默认的文件名来提交表单了。

    要实现这一点,我们首先得需要将上传的文件限制在1个,为此我们需要一个文件列表,并给Upload组件的onChange传入一个handleChange函数,在每次上传新文件时,新的文件会顶掉文件列表中的旧文件,再把文件列表传回给Upload组件的fileList,从而实现实时更新,并限制在1个文件:

    import React, { useState } from 'react';
    …
    
    export default function UploadFile(props) {
        const [form] = Form.useForm(); //用于之后取数据
        const [fileList, setFileList] = useState(null); //文件列表
        console.log(fileList)
    
        …
    
        const handleChange = (info) => {
            let files = [...info.fileList];
            files = files.slice(-1);
            setFileList(files);
        }
    
        return (
            <>
                <Form 
                {...layout}
                form={form}
                name="upload_file"
                onFinish={handleSubmit}><Form.Item
                    label="上传文件"
                    name="file"
                    rules={[{ required: true,
                        message: "请上传文件" }]}>
                        <Upload
                        onChange={handleChange}
                        fileList={fileList}>
                            <Button icon={<UploadOutlined />}>开始上传</Button>
                        </Upload>
                    </Form.Item>
    </Form>
            </>
        )
    }

    我们可以看到每次文件上传后,新的会替换旧的,且列表内永远只有1个文件:

     

    那接下来就是获取文件名了,同样,我们用一个变量负责记录文件名,同样在handleChange中完成提取文件名和变更该变量,最后我们用表单的setFieldsValue和useEffect来实时更新文件名称一栏:

    import React, { useState, useEffect } from 'react';
    …
    
    export default function UploadFile(props) {
        const [form] = Form.useForm(); //用于之后取数据
        const [fileList, setFileList] = useState(null); //文件列表
        const [fileName, setFileName] = useState(null); //文件名
        console.log(fileName)
    
        …
    
        const handleChange = (info) => {
            let files = [...info.fileList];
            files = files.slice(-1);
            setFileList(files);
    
            if (files && files.length > 0) {
                const [fname, fextname] =  files[0]["name"].split(/.(?=[^.]+$)/); //分割文件名
                setFileName(fname);
            }
        }
    
        useEffect(() => {
            //实时更新
            form.setFieldsValue({
                name: fileName,
            });
        }, [fileName]);
    
        return (
            <>
                <Form 
                {...layout}
                form={form}
                name="upload_file"
                onFinish={handleSubmit}></Form>
            </>
        )
    }

    我们可以看到,每次上传都会自动改变文件名称了:

    至此,前端部分初步完成了。

    3、捯饬后端

    后端相对简单,新建一个uf_api文件夹作为后端部分,cd进入,npm init来进行初始化。另外我们再新建一个dest文件夹,作为存放我们上传的文件的地方。

    我们可以按照上一篇中第2部分的方法(见https://www.cnblogs.com/silence-gtx/p/14092286.html)来搭建后端以及配置tsconfig.json,由于暂时不需要数据库,所以其他部分可以暂时忽略。

    我们在src目录下新建controllers文件夹,并新建一个UploadController.ts来作为处理上传功能的controller:

    接下来,我们会使用multer来负责处理上传的文件,并将其储存到硬盘上的指定位置,还有fs负责创建文件夹的操作。我们还需要tsoa来帮助我们更方便地写controller(全部npm install 一下)。

     1 import { 
     2     Controller, 
     3     Request,
     4     Post,
     5     Route
     6 } from 'tsoa';
     7 import express from 'express';
     8 const multer = require('multer');
     9 const fs = require('fs');
    10 
    11 const storagePath = "F:/node_project/upload_file/dest"; //设置储存路径
    12 
    13 @Route("upload")
    14 export class UploadController extends Controller {
    15     @Post("/")
    16     public async uploadFile(
    17         @Request() request: express.Request
    18     ): Promise<any> {
    19         await this.handleUpload(request);
    20         return true;
    21     }
    22 
    23     private async createFolder (folder: string) {
    24         //创建文件夹,若文件夹已存在,则跳过
    25         try {
    26             fs.accessSync(folder);
    27             console.log('目标文件夹已创建')
    28         } catch (error) {
    29             fs.mkdirSync(folder);
    30             console.log('创建目标文件夹')
    31         }
    32     };
    33 
    34     private async handleUpload (
    35         request: express.Request
    36     ): Promise<void> {
    37         this.createFolder(storagePath);
    38 
    39         var storage = multer.diskStorage({
    40             destination: function (
    41                 req: express.Request, 
    42                 file: any, 
    43                 callback:any) {
    44                     callback(null, storagePath)
    45                 }, //负责处理路径
    46             filename: function (
    47                 req: express.Request, 
    48                 file: any, 
    49                 callback:any
    50             ) {
    51                 console.log(file)
    52                 callback(null, file.originalname)
    53             } //负责处理文件名,originalname为你上传文件的名称
    54         });
    55 
    56         var upload = multer({ storage: storage });
    57 
    58         const multerSingle = upload.single("file");
    59         // 前端传过来的form-data应该将上传的文件放在file下,即form-data包含 {"file": 你的文件}
    60         // antd的Upload组件会自动使用"file"
    61 
    62         return new Promise((resolve, reject) => {
    63             multerSingle(request, undefined, async (error: any) => {
    64                 if (error) {
    65                     reject(error);
    66                 }
    67                 resolve();
    68                 console.log("文件已上传")
    69             });
    70         });
    71     }
    72 }

    UploadController包含3个函数,公有函数负责暴露接口并调用handleUpload来处理文件的上传,私有函数createFolder负责创建目标文件夹(如果已经有了就自动忽略),私有函数handleUpload负责具体的文件上传逻辑(注意,返回的类型必须是void,否则会报错)。

    接下来,我们在uf_api根目录下创建tsoa的配置文件tsoa.json,并在src下新建routes,然后,和上一期一样,在cmd中执行yarn run tsoa routes生成路由:

     1 {
     2     "entryFile": "src/app.ts",
     3     "noImplicitAdditionalProperties": "throw-on-extras",
     4     "controllerPathGlobs": ["src/**/*Controller.ts"],
     5     "spec": {
     6       "outputDirectory": "src/routes",
     7       "specVersion": 3
     8     },
     9     "routes": {
    10       "routesDir": "src/routes"
    11     }
    12 }

    同样地,在app.ts中加入路由(另外,别忘了cors,否则一会儿就没法跟前端连通了):

     1 import express from 'express';
     2 import bodyParser from 'body-parser';
     3 var cors = require('cors');
     4 import { RegisterRoutes } from "./routes/routes";
     5 
     6 // 创建一个express实例
     7 const app: express.Application = express();
     8 
     9 var corsOptions = {
    10     credentials:true,
    11     origin:'http://localhost:3000',
    12     optionsSuccessStatus:200
    13 };
    14 app.use(cors(corsOptions));
    15 
    16 app.use(
    17     bodyParser.urlencoded({
    18       extended: true,
    19     })
    20 );
    21 app.use(bodyParser.json());
    22 
    23 RegisterRoutes(app); // 添加路由
    24 
    25 app.listen(5000, ()=> {
    26     console.log('Example app listening on port 5000!');
    27 });

    接着把package.json中的start的值改为tsoa spec-and-routes && tsc && node ./build/app.js,然后我们npm start试试:

    接下来,我们用Postman测试一下看看,输入Controller对应的地址,调成POST请求之后,在下方的Body中选择form-data,然后在KEY一栏就可以选择是Text还是File,测试上传文件的话我们就选File:

    回忆一下刚才controller的注释中有提到我们的文件需要对应”file”,因此,我们在KEY这儿填上file,VALUE就上传一个文件,点击Send,成功的话应该会返回一个true:

    我们可以看看console里的内容:

    再到dest下看看:

    文件已经成功上传,跑通了。接下来就可以试着贯通前后端了。

    4、连通与改进

    我们再回到前端的UploadFile.js,将后端的地址加入到Upload的action中去:

      1 import React, { useState, useEffect } from 'react';
      2 import { 
      3     Upload, 
      4     Form, 
      5     Button, 
      6     Input, 
      7     Space } from 'antd';
      8 import { UploadOutlined } from '@ant-design/icons';
      9 
     10 const layout = {
     11     labelCol: { span: 6 },
     12     wrapperCol: { span: 18 },
     13 }; //表单的整体布局
     14 const tailLayout = {
     15     wrapperCol: { offset: 16, span: 8 },
     16 }; //取消和确定按钮的布局
     17 
     18 const { TextArea } = Input;
     19 
     20 export default function UploadFile(props) {
     21     const [form] = Form.useForm(); //用于之后取数据
     22     const [fileList, setFileList] = useState(null); //文件列表
     23     const [fileName, setFileName] = useState(null); //文件名
     24     console.log(fileName)
     25 
     26     const handleSubmit = values => {
     27         console.log(values)
     28         props.handleCancel();
     29     }
     30 
     31     const handleChange = (info) => {
     32         let files = [...info.fileList];
     33         files = files.slice(-1);
     34         setFileList(files);
     35 
     36         if (files && files.length > 0) {
     37             const [fname, fextname] =  files[0]["name"].split(/.(?=[^.]+$)/); //分割文件名
     38             setFileName(fname);
     39         }
     40     }
     41 
     42     useEffect(() => {
     43         //实时更新
     44         form.setFieldsValue({
     45             name: fileName,
     46         });
     47     }, [fileName]);
     48 
     49     return (
     50         <>
     51             <Form 
     52             {...layout}
     53             form={form}
     54             name="upload_file"
     55             onFinish={handleSubmit}>
     56 
     57                 <Form.Item
     58                 label="文件名称"
     59                 name="name"
     60                 rules={[{ required: true,
     61                     message: "请输入文件名" }]}>
     62                     <Input/>
     63                 </Form.Item>
     64 
     65                 <Form.Item
     66                 label="备注"
     67                 name="desc"
     68                 rules={[{ required: false,
     69                     message: "请输入备注" }]}>
     70                     <TextArea rows={4}/>
     71                 </Form.Item>
     72 
     73                 <Form.Item
     74                 label="上传文件"
     75                 name="file"
     76                 rules={[{ required: true,
     77                     message: "请上传文件" }]}>
     78                     <Upload
     79                     action="http://localhost:5000/upload"
     80                     onChange={handleChange}
     81                     fileList={fileList}>
     82                         <Button icon={<UploadOutlined />}>开始上传</Button>
     83                     </Upload>
     84                 </Form.Item>
     85 
     86                 <Form.Item 
     87                 {...tailLayout}>
     88                     <Space>
     89                         <Button type="primary" htmlType="submit">
     90                             确认
     91                         </Button>
     92                         <Button onClick={props.handleCancel}>
     93                             取消
     94                         </Button>
     95                     </Space>
     96                 </Form.Item>
     97             </Form>
     98         </>
     99     )
    100 }

    我们试一试:

     

     

    前后端的工作都毫无问题,成功!到此,我们已经初步完成了上传文件的功能。

    但是转念一想,每次我们选择文件后,文件都会被直接上传到目标文件夹,并且直接覆盖同名文件。可是万一我只是手滑怎么办呢?为了避免这种现象,我想到的是单独再做一个上传按钮,每次选择文件后并不直接上传,需要点击按钮才能完成上传,即手动上传。此外,我还可以重命名我要上传的文件(比如用文件名称一栏的值代替原文件名再上传)。这样就可以一定程度上避免手滑的问题(当然,更保险一点应该做个文件已存在提示,不过本文没有做……)。

    我们先来实现手动上传功能,我们要先去掉Upload的action以阻止默认上传行为的发生,然后自己用axios写一个来实现上传功能,此外再加个按钮以及一个handleUpload函数来触发上传功能(可以参考antd的手动上传案例)。

    先从axios开始吧,新建一个UploadSvc.js:

     1 import axios from 'axios';
     2 
     3 const api = "http://localhost:5000"; //后段地址
     4 
     5 class UploadSvc {
     6     uploadDataset(file) {
     7         let config = {
     8             headers: {
     9                 "Content-Type": "multipart/form-data"
    10             }
    11         } // 我们上传的是form-data
    12         return new Promise((resolve) => resolve(axios.post(`${api}/upload`, file, config)));
    13     }
    14 }
    15 
    16 export default new UploadSvc();

    uploadDataset将接受一个file,然后以form-data的形式发送后端的相应路径。

    接下来我们将UploadSvc引入UploadFile.js中,并加上其他东西:

      1 import React, { useState, useEffect } from 'react';
      2 import { 
      3     Upload, 
      4     Form, 
      5     Button, 
      6     Input, 
      7     Space,
      8     message } from 'antd';
      9 import { UploadOutlined } from '@ant-design/icons';
     10 import UploadSvc from "./UploadSvc";
     11 
     12 const layout = {
     13     labelCol: { span: 6 },
     14     wrapperCol: { span: 18 },
     15 }; //表单的整体布局
     16 const tailLayout = {
     17     wrapperCol: { offset: 16, span: 8 },
     18 }; //取消和确定按钮的布局
     19 
     20 const { TextArea } = Input;
     21 
     22 export default function UploadFile(props) {
     23     const [form] = Form.useForm(); //用于之后取数据
     24     const [fileList, setFileList] = useState(null); //文件列表
     25     const [fileName, setFileName] = useState(null); //文件名
     26     const [uploadName, setUploadName] = useState(null); //文件全名(包含扩展名)
     27     const [uploading, setUploading] = useState(false); //是否在上传中
     28 
     29     console.log(uploadName)
     30 
     31     const handleSubmit = values => {
     32         //处理表单提交
     33         console.log(values)
     34         props.handleCancel();
     35     }
     36 
     37     const handleChange = (info) => {
     38         //处理文件名一栏根据上传文件改变自动变化
     39         let files = [...info.fileList];
     40         files = files.slice(-1);
     41         setFileList(files);
     42 
     43         if (files && files.length > 0) {
     44             const [fname, fextname] =  files[0]["name"].split(/.(?=[^.]+$)/); //分割文件名
     45             setFileName(fname);
     46             setUploadName(files[0]["name"]); //设置全名
     47         }
     48     }
     49 
     50     const handleUpload = () => {
     51         //处理手动上传
     52         const formData = new FormData();
     53         if (fileList && fileName !== "") {
     54             let file = fileList[0]
     55             formData.append('file', file.originFileObj, uploadName); //一定要用file.originFileObj!!!
     56             setUploading(true) //设置状态为上传中
     57 
     58             UploadSvc.uploadDataset(formData)
     59             .then(response => {
     60                 message.success(`文件 ${uploadName} 已上传`, 2) //成功的提示消息将持续2秒
     61                 setUploading(false) //重置上传状态
     62             })
     63         } 
     64     }
     65 
     66     useEffect(() => {
     67         //实时更新
     68         form.setFieldsValue({
     69             name: fileName,
     70         });
     71     }, [fileName]);
     72 
     73     return (
     74         <>
     75             <Form 
     76             {...layout}
     77             form={form}
     78             name="upload_file"
     79             onFinish={handleSubmit}>
     80 
     81                 <Form.Item
     82                 label="文件名称"
     83                 name="name"
     84                 rules={[{ required: true,
     85                     message: "请输入文件名" }]}>
     86                     <Input/>
     87                 </Form.Item>
     88 
     89                 <Form.Item
     90                 label="备注"
     91                 name="desc"
     92                 rules={[{ required: false,
     93                     message: "请输入备注" }]}>
     94                     <TextArea rows={4}/>
     95                 </Form.Item>
     96 
     97                 <Form.Item
     98                 label="上传文件"
     99                 name="file"
    100                 rules={[{ required: true,
    101                     message: "请上传文件" }]}>
    102                     <Upload
    103                     onChange={handleChange}
    104                     fileList={fileList}>
    105                         <Button icon={<UploadOutlined />}>开始上传</Button>
    106                     </Upload>
    107                     <Button
    108                     type="primary"
    109                     onClick={handleUpload}
    110                     disabled={fileList === null || fileList.length === 0}
    111                     loading={uploading}
    112                     style={{ marginTop: 16 }}
    113                     >
    114                     {uploading ? "上传中..." : "开始上传"}
    115                     </Button>
    116                 </Form.Item>
    117 
    118                 <Form.Item 
    119                 {...tailLayout}>
    120                     <Space>
    121                         <Button type="primary" htmlType="submit">
    122                             确认
    123                         </Button>
    124                         <Button onClick={props.handleCancel}>
    125                             取消
    126                         </Button>
    127                     </Space>
    128                 </Form.Item>
    129             </Form>
    130         </>
    131     )
    132 }

    这里千万要注意的是,我们使用formData.append给formData添加要上传的文件时,一定要使用file.originFileObj而不是file本身(卡了我几个小时的一个点)!因为antd默认去除的这个file并不是文件本体,而是包含了一些metadata的一个Object,其中的originFileObj才是真正的文件本体。

    现在我们来试试,选择文件,不点击“开始上传”按钮:

    可以看到Upload默认的上传已经由于缺少action而失败,此时后端的console是没有任何反应的,说明上传没有发生。然后我们点击“开始上传”:

    可以看到这次弹出了上传成功的消息,查看一下后端:

    上传确实成功了。

    然后,我们还发现,即使我们修改了文件名称一栏,上传后的文件名依然没有改变,为了实现这一点,我们需要Form.Provider:

    import React, { useState, useEffect } from 'react';
    import { 
        Upload, 
        Form, 
        Button, 
        Input, 
        Space,
        message } from 'antd';
    import { UploadOutlined } from '@ant-design/icons';
    import UploadSvc from "./UploadSvc";
    
    …
    
    export default function UploadFile(props) {
        const [form] = Form.useForm(); //用于之后取数据
        const [fileList, setFileList] = useState(null); //文件列表
        const [fileName, setFileName] = useState(null); //文件名
        const [extName, setExtName] = useState(null); //扩展名
        const [uploadName, setUploadName] = useState(null); //文件全名(包含扩展名)
        const [uploading, setUploading] = useState(false); //是否在上传中
    
        console.log(uploadName)
    
        …
    
        const handleChange = (info) => {
            //处理文件名一栏根据上传文件改变自动变化
            let files = [...info.fileList];
            files = files.slice(-1);
            setFileList(files);
    
            if (files && files.length > 0) {
                const [fname, fextname] =  files[0]["name"].split(/.(?=[^.]+$)/); //分割文件名
                setFileName(fname); //设置文件名
                setExtName(fextname); //设置扩展名
                setUploadName(files[0]["name"]); //设置全名
            }
        }
    
        …
    
        return (
            <>
                <Form.Provider
                onFormChange={
                    () => uploadName && setUploadName(form.getFieldValue('name') + '.' + extName)
                }>
                    <Form 
                    {...layout}
                    form={form}
                    name="upload_file"
                    onFinish={handleSubmit}></Form>
                </Form.Provider>
            </>
        )
    }

    我们通过它的onFormChange,来实现每次表单内容更新时,更新uploadName这个变量。现在我们可以试试我们的功能有没有成功:

    看一下dest,发现成功上传了新的文件:

    最后,加上一点细节:

      1 import React, { useState, useEffect } from 'react';
      2 import { 
      3     Upload, 
      4     Form, 
      5     Button, 
      6     Input, 
      7     Space,
      8     message } from 'antd';
      9 import { UploadOutlined } from '@ant-design/icons';
     10 import UploadSvc from "./UploadSvc";
     11 
     12 const layout = {
     13     labelCol: { span: 6 },
     14     wrapperCol: { span: 18 },
     15 }; //表单的整体布局
     16 const tailLayout = {
     17     wrapperCol: { offset: 16, span: 8 },
     18 }; //取消和确定按钮的布局
     19 
     20 const { TextArea } = Input;
     21 
     22 export default function UploadFile(props) {
     23     const [form] = Form.useForm(); //用于之后取数据
     24     const [fileList, setFileList] = useState(null); //文件列表
     25     const [fileName, setFileName] = useState(null); //文件名
     26     const [extName, setExtName] = useState(null); //扩展名
     27     const [uploadName, setUploadName] = useState(null); //文件全名(包含扩展名)
     28     const [uploading, setUploading] = useState(false); //是否在上传中
     29 
     30     console.log(uploadName)
     31 
     32     const handleSubmit = values => {
     33         //处理表单提交
     34         console.log(values)
     35         props.handleCancel();
     36     }
     37 
     38     const handleChange = (info) => {
     39         //处理文件名一栏根据上传文件改变自动变化
     40         let files = [...info.fileList];
     41         files = files.slice(-1);
     42         setFileList(files);
     43 
     44         if (files && files.length > 0) {
     45             const [fname, fextname] =  files[0]["name"].split(/.(?=[^.]+$)/); //分割文件名
     46             setFileName(fname); //设置文件名
     47             setExtName(fextname); //设置扩展名
     48             setUploadName(files[0]["name"]); //设置全名
     49         }
     50     }
     51 
     52     const handleUpload = () => {
     53         //处理手动上传
     54         const formData = new FormData();
     55         if (fileList && fileName !== "") {
     56             let file = fileList[0]
     57             formData.append('file', file.originFileObj, uploadName); //一定要用file.originFileObj!!!
     58             setUploading(true) //设置状态为上传中
     59 
     60             UploadSvc.uploadDataset(formData)
     61             .then(response => {
     62                 message.success(`文件 ${uploadName} 已上传`, 2) //成功的提示消息将持续2秒
     63                 setUploading(false) //重置上传状态
     64                 form.setFieldsValue({
     65                     file: true //满足data的required: true
     66                 })
     67             })
     68         } 
     69     }
     70 
     71     useEffect(() => {
     72         //实时更新
     73         form.setFieldsValue({
     74             name: fileName,
     75         });
     76     }, [fileName]);
     77 
     78     return (
     79         <>
     80             <Form.Provider
     81             onFormChange={
     82                 () => uploadName && setUploadName(form.getFieldValue('name') + '.' + extName)
     83             }>
     84                 <Form 
     85                 {...layout}
     86                 form={form}
     87                 name="upload_file"
     88                 onFinish={handleSubmit}>
     89 
     90                     <Form.Item
     91                     label="文件名称"
     92                     name="name"
     93                     rules={[{ required: true,
     94                         message: "请输入文件名" }]}>
     95                         <Input/>
     96                     </Form.Item>
     97 
     98                     <Form.Item
     99                     label="备注"
    100                     name="desc"
    101                     rules={[{ required: false,
    102                         message: "请输入备注" }]}>
    103                         <TextArea rows={4}/>
    104                     </Form.Item>
    105 
    106                     <Form.Item
    107                     label="上传文件"
    108                     name="file"
    109                     rules={[{ required: true,
    110                         message: "请上传文件" }]}>
    111                         <Upload
    112                         onChange={handleChange}
    113                         beforeUpload={file => {
    114                             fileList && setFileList([...fileList, file])
    115                             return false;
    116                         }}
    117                         fileList={fileList}>
    118                             <Button icon={<UploadOutlined />}>开始上传</Button>
    119                         </Upload>
    120                         <Button
    121                         type="primary"
    122                         onClick={handleUpload}
    123                         disabled={fileList === null || fileList.length === 0}
    124                         loading={uploading}
    125                         style={{ marginTop: 16 }}
    126                         >
    127                         {uploading ? "上传中..." : "开始上传"}
    128                         </Button>
    129                     </Form.Item>
    130 
    131                     <Form.Item 
    132                     {...tailLayout}>
    133                         <Space>
    134                             <Button type="primary" htmlType="submit">
    135                                 确认
    136                             </Button>
    137                             <Button onClick={props.handleCancel}>
    138                                 取消
    139                             </Button>
    140                         </Space>
    141                     </Form.Item>
    142                 </Form>
    143             </Form.Provider>
    144         </>
    145     )
    146 }

    通过给Upload添加beforeUpload来避免触发默认上传行为(Not Found报错,文件名发红)。

    此外,由于上传文件(file部分)的required是true,也就是说是表单中必须要填写的内容,如果我们上传完就此提交,会报错,导致无法提交表单:

    我们需要在handleUpload中给Form的file部分设置一个值来满足required的要求。这样一来,我们就可以顺利提交了:

     一套比较完整的文件上传就做好了。

    代码见:

    https://github.com/SilenceGTX/upload_file

  • 相关阅读:
    c++ 启发式搜索解决八数码问题
    基于linux或windows的c/s的循环服务器求一元二次方程的根
    基于linux或windows平台上的c/s简单通信
    第七章总结
    第六章总结
    第五章总结
    第四章总结
    第一章总结
    第三章总结
    第二章总结
  • 原文地址:https://www.cnblogs.com/silence-gtx/p/14251145.html
Copyright © 2011-2022 走看看