Ant Design[1] 是蚂蚁集团开源的一款 React UI 库。UI 库内置了很多开箱即用的组件,特别适应于开发公司内部网页,当然由于其出色的交互和强大能力,也有一些公司选择它直接用于构建对外的客户端网页。
在之前的一篇文章中,我们介绍了 Ant Design 中关于动态表单的构建教程[2]。本文,我们将继续探索另一个常见使用场景——文件上传组件 <Upload> 的应用。
图片
<Upload> 初印象
我们先从 <Upload> 基础功能讲起。如下所示:
import { Upload, Button } from 'antd'
function Example() {
return (
<div>
<Upload>
<Button>click to upload</Button>
</Upload >
</div>
)
}
<Upload> 组件本身提供了上传能力,至于上传能力的触发,则有赖于其 children 内容。<Upload> 组件的 children 内容我们完全可以自定义,本案例场景,我们简单放入一个按钮即可。
展示效果如下:
图片
点击"click to upload"按钮,就能触发文件弹窗的出现。
我们可以通过控制台查看最终生成的 DOM 结构:
图片
发现,在渲染出来的 <Button> 组件的上方,有一个 display: none 的 <input type="file">!所以其触发机制是:在点击 <Button> 组件时,antd 内部会关联到 <input type="file"> 的点击,于是便弹出一个文件上传对话框。
使用 <Upload> 时报错了
如果我们只写了上面的代码,并尝试点击“打开”上传文件时,会看到报错。
图片
这是因为我们没有指定文件上传服务。默认 <Upload> 使用当前 URL 作为文件上传地址。
图片
也能看到,在文件上传时,请求的内容类型 Content-Type 是 multipart/form-data,在上传文件时,也必然要使用这种请求类型。
现在,因为我们并没实现该请求类型的文件上传服务,自然就报错了。
接下来,我们着手实现一个文件上传服务。
实现一个简单的文件上传服务
首先,安装依赖。
npm install express formidable cors
express 作为我们的后端路由服务器;formidable 则是一个 Node.js 模块,用于解析表单数据,尤其是文件上传场景;cors 则是让我们的服务开放跨域请求的能力。
我们的服务端代码实现如下:
// server/app.js
import express from 'express';
import cors from 'cors';
import formidable from 'formidable';
const app = express();
app.use(cors()); // Enable CORS for all routes
// Serve static files from the 'uploads' directory
app.use('/uploads', express.static('uploads'));
app.get('/', (req, res) => {
res.send(`
<h2>With <code>"express"</code> npm package</h2>
<form actinotallow="/api/upload" enctype="multipart/form-data" method="post">
<div>Text field title: <input type="text" name="title" /></div>
<div>File: <input type="file" name="someExpressFiles" multiple="multiple" /></div>
<input type="submit" value="Upload" />
</form>
`);
});
app.post('/api/upload', (req, res, next) => {
const form = formidable({});
form.parse(req, (err, fields, files) => {
if (err) {
next(err);
return;
}
res.json({ fields, files });
});
});
app.listen(3000, () => {
console.log('Server listening on http://localhost:3000 ...');
});
总体代码量并不多:
- 首先,根路由(/)返回了一端 HTML 用于测试文件上传能力
- 其次,文件上传的服务路由是 POST /api/upload,在这个版本的实现上,我们只是先简单打印出来提交的表单内容
下面启动服务:
node --watch .\server\app.js
浏览器访问:http://localhost:3000/,就能看到一个简陋的文件上传测试页。
图片
我们点击选中 2 个文件进行上传,结果就能看到 /api/upload 的返回结果。
图片
files 代表文件表单域,fields 则代表非文件表单域。接着,我们替换掉 res.json({ fields, files }) 的内容。
res.json({ fields, files });
替换为:
const oldpath = files.someExpressFiles[0].filepath;
const originalFilename = files.someExpressFiles[0].originalFilename;
const extension = path.extname(originalFilename);
const today = new Date();
const year = today.getFullYear();
const month = ('0' + (today.getMonth() + 1)).slice(-2); // pad with leading zero if necessary
const newFilename = `${year}/${month}/${originalFilename.replace(extension, '')}-${Date.now()}${extension}`;
const newpath = path.join('uploads', newFilename);
fs.cp(oldpath, newpath, (err) => {
if (err) {
res.writeHead(500, { 'Content-Type': 'text/plain' });
res.end('Error moving file');
return;
}
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end(JSON.stringify({
message: 'File uploaded successfully',
path: newpath,
}));
});
目的也比较简单,就是把上传的文件存储到本地硬盘。存储位置位于 uploads/[year]/[month] 目录之下。在此之前,我们已经将 uploads 目录设置为可静态访问了。
// Serve static files from the 'uploads' directory
app.use('/uploads', express.static('uploads'));
现在,在此点击上传文件。
图片
展示上传成功。
图片
这个时候,我们就会在项目中看到 uploads/ 目录下已经有刚才上传的文件了。
图片
这样,我们就实现了一个简单的文件上传服务。下面,就可以让 <Upload> 来接入了。
Upload 接入 /api/upload
<Upload> 接入上传服务,是通过指定 props 实现的。
function Example() {
const props = {
action: 'http://localhost:3000/api/upload',
name: 'someExpressFiles',
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.`);
}
},
};
return (
<div>
<Upload {...props}>
<Button>click to upload</Button>
</Upload >
</div>
)
}
在上传场景中,name、action 和 onChange 是 3 个被广泛使用的 prop。
- action:这是核心。用于指定上传文件的服务路径。也就是我们前一步实现的 /api/upload
- name:上传文件时,后端服务获取文件信息的表单字段名
- onChange:用于观察上传结果。上传成功或失败都会触发
下面,我们再试一下文件上传。
图片
提示文件被成功上传了!
在控制台中,我们还能看到打印出来的响应内容。
图片
onChange 回调函数的 info 参数中,包含 2 个属性: file 和 fileList。file 指代当前上传的文件;fileList 则代表当前 <Upload> 组件中已有/还剩下的文件列表。
你可以通过 file.response 获得服务端的响应数据,同时可以通过 originFileObj 属性获得上传文件的原始信息。
与 <Form> 组件配合使用
当然,在实际的业务场景中,在文件上传之后流程并未结束,我们通常还需要将服务端返回的图片路径跟随其他表单字段一起保存起来,比如更新个人中心里的头像这类场景。
这个时候 <Upload> 就要搭配 <Form> 一起使用了。
以下面的 DEMO 为例:
function Example() {
const [form] = Form.useForm();
const props = {
name: 'someExpressFiles',
listType: "picture-card",
action: 'http://localhost:3000/api/upload',
};
const onFinish = (values) => {
console.log('Submit values:', values);
// 发送表单数据到后端
};
return (
<div>
<Form form={form} onFinish={onFinish}>
<Form.Item
name="name"
rules={[{ required: true, message: '请输入姓名' }]}
>
<Input placeholder="姓名" />
</Form.Item>
<Form.Item
name="avatar"
rules={[{ required: true, message: '请选择头像' }]}
>
<Upload {...props}>
上传文件
</Upload >
</Form.Item>
<Form.Item>
<Button type="primary" htmlType="submit">
提交
</Button>
</Form.Item>
</Form>
</div>
)
}
本例中为 uploadProps 增加了 listType: "picture-card" 配置,这样在上传完图片后,可以小图预览。
选择文件,点击上传:
图片
点击“提交”,控制台便能看到打印出来的表单数据了。
图片
可以看到,最近一次的 onChang 事件里的内容,会作为数据在提交表单时使用。
另外,uploadProps 还提供了 fileList prop 参数用来手动控制预览文件列表的展示行为,比如我们只想查看最近两次的图片预览,就可以这样做:
const [fileList, setFileList] = useState([]);
const props = {
name: 'someExpressFiles',
listType: "picture-card",
fileList,
action: 'http://localhost:3000/api/upload',
onChange({ file, fileList: newFileList }) {
setFileList(newFileList.slice(-2));
},
};
如此依赖,上传的文件始终展示最新上传的 2 张,之前旧的就会被剔除。
福利内容:Ctrl + V 上传文件
Ant Design 的 <Upload> 并未直接提供粘贴上传文件的支持。这个时候就要通过 ref 操作其实例来实现了。
首先,uploadProps 增加 ref 支持,这样我们就能访问到 <Upload> 的底层实例对象了。
const props = {
ref: uploadRef,
// ...
};
<Form.Item
name="file"
rules={[{ required: true, message: '请选择文件' }]}
>
<Upload {...props}>
上传文件
</Upload >
</Form.Item>
接着为要支持粘贴上传的文本框绑定 onPaste 事件,提供粘贴行为的监听。
<textarea
notallow={handlePaste}
placeholder="粘贴文件到此处"
/>
在处理函数内部像下面这样写。
const handlePaste = (e) => {
e.preventDefault();
// 1)
const files = e.clipboardData.files;
if (files.length > 0) {
const file = files[0];
if (uploadRef.current) {
// 2)
uploadRef.current.upload.uploader.onChange({ target: { files: [file] } }); // 手动触发 Upload 组件的上传操作
}
}
};
- 首先,获取获取剪贴板上的文件(只取第一个)
- 手动调用 uploadRef.current.upload.uploader.onChange() 方法,传入手动拼凑的事件对象 { target: { files: [file] } } 即可。
注意,这种上传能力是我自己摸索出来的,并不是官方做法。最好是作为渐进功能使用。
总结
本文我们循序渐进地讲解了 Ant Design 中 <Upload> 上传组件的用法。涵盖基础用法、简单上传服务地编写以及 uploadProps 的核心 prop 的定义和使用。