从零开始的野路子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负责对话框内部的内容:

 从零开始的野路子React/Node(9)Antd + multer实现文件上传

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之后可以看到一个按钮,点击后一篇空白。

 从零开始的野路子React/Node(9)Antd + multer实现文件上传

接下来我们来写一下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对应一个上传文件的按钮。此时我们再看页面,点击“上传文件”按钮,会看到表单弹出:

 从零开始的野路子React/Node(9)Antd + multer实现文件上传

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

 从零开始的野路子React/Node(9)Antd + multer实现文件上传

那是因为我们还没有配置上传的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):

 从零开始的野路子React/Node(9)Antd + multer实现文件上传

除此之外,我们还需要有个函数来负责点击“确认”按钮后的一系列步骤,我们自然希望点击确认后会提交表单,然后关闭窗口我们可以用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来记录下表单的值,并且关闭对话框,我们来测试一下看看:

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

 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个文件:

 从零开始的野路子React/Node(9)Antd + multer实现文件上传

那接下来就是获取文件名了,同样,我们用一个变量负责记录文件名,同样在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>
        </>
    )
}

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

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

 

3、捯饬后端

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

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

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

接下来,我们会使用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试试:

从零开始的野路子React/Node(9)Antd + multer实现文件上传

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

再到dest下看看:

从零开始的野路子React/Node(9)Antd + multer实现文件上传

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

 

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 }

我们试一试:

 从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

 从零开始的野路子React/Node(9)Antd + multer实现文件上传

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

 

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

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

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

 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才是真正的文件本体。

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

上传确实成功了。

然后,我们还发现,即使我们修改了文件名称一栏,上传后的文件名依然没有改变,为了实现这一点,我们需要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这个变量。现在我们可以试试我们的功能有没有成功:

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

最后,加上一点细节:

  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,也就是说是表单中必须要填写的内容,如果我们上传完就此提交,会报错,导致无法提交表单:

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

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

从零开始的野路子React/Node(9)Antd + multer实现文件上传

 

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

 

代码见:

https://github.com/SilenceGTX/upload_file

上一篇:nodeJs 使用 Multer 实现本地文件/图片上传到服务器指定目录


下一篇:express中文件的上传 multer