關(guān)于 Ant Design 上傳組件 <Upload> 的入門教程
Ant Design[1] 是螞蟻集團開源的一款 React UI 庫。UI 庫內(nèi)置了很多開箱即用的組件,特別適應于開發(fā)公司內(nèi)部網(wǎng)頁,當然由于其出色的交互和強大能力,也有一些公司選擇它直接用于構(gòu)建對外的客戶端網(wǎng)頁。
在之前的一篇文章中,我們介紹了 Ant Design 中關(guān)于動態(tài)表單的構(gòu)建教程[2]。本文,我們將繼續(xù)探索另一個常見使用場景——文件上傳組件 <Upload> 的應用。
圖片
<Upload> 初印象
我們先從 <Upload> 基礎(chǔ)功能講起。如下所示:
import { Upload, Button } from 'antd'
function Example() {
return (
<div>
<Upload>
<Button>click to upload</Button>
</Upload >
</div>
)
}
<Upload> 組件本身提供了上傳能力,至于上傳能力的觸發(fā),則有賴于其 children 內(nèi)容。<Upload> 組件的 children 內(nèi)容我們完全可以自定義,本案例場景,我們簡單放入一個按鈕即可。
展示效果如下:
圖片
點擊"click to upload"按鈕,就能觸發(fā)文件彈窗的出現(xiàn)。
我們可以通過控制臺查看最終生成的 DOM 結(jié)構(gòu):
圖片
發(fā)現(xiàn),在渲染出來的 <Button> 組件的上方,有一個 display: none 的 <input type="file">!所以其觸發(fā)機制是:在點擊 <Button> 組件時,antd 內(nèi)部會關(guān)聯(lián)到 <input type="file"> 的點擊,于是便彈出一個文件上傳對話框。
使用 <Upload> 時報錯了
如果我們只寫了上面的代碼,并嘗試點擊“打開”上傳文件時,會看到報錯。
圖片
這是因為我們沒有指定文件上傳服務。默認 <Upload> 使用當前 URL 作為文件上傳地址。
圖片
也能看到,在文件上傳時,請求的內(nèi)容類型 Content-Type 是 multipart/form-data,在上傳文件時,也必然要使用這種請求類型。
現(xiàn)在,因為我們并沒實現(xiàn)該請求類型的文件上傳服務,自然就報錯了。
接下來,我們著手實現(xiàn)一個文件上傳服務。
實現(xiàn)一個簡單的文件上傳服務
首先,安裝依賴。
npm install express formidable cors
express 作為我們的后端路由服務器;formidable 則是一個 Node.js 模塊,用于解析表單數(shù)據(jù),尤其是文件上傳場景;cors 則是讓我們的服務開放跨域請求的能力。
我們的服務端代碼實現(xiàn)如下:
// 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,在這個版本的實現(xiàn)上,我們只是先簡單打印出來提交的表單內(nèi)容
下面啟動服務:
node --watch .\server\app.js
瀏覽器訪問:http://localhost:3000/,就能看到一個簡陋的文件上傳測試頁。
圖片
我們點擊選中 2 個文件進行上傳,結(jié)果就能看到 /api/upload 的返回結(jié)果。
圖片
files 代表文件表單域,fields 則代表非文件表單域。接著,我們替換掉 res.json({ fields, files }) 的內(nèi)容。
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] 目錄之下。在此之前,我們已經(jīng)將 uploads 目錄設(shè)置為可靜態(tài)訪問了。
// Serve static files from the 'uploads' directory
app.use('/uploads', express.static('uploads'));
現(xiàn)在,在此點擊上傳文件。
圖片
展示上傳成功。
圖片
這個時候,我們就會在項目中看到 uploads/ 目錄下已經(jīng)有剛才上傳的文件了。
圖片
這樣,我們就實現(xiàn)了一個簡單的文件上傳服務。下面,就可以讓 <Upload> 來接入了。
Upload 接入 /api/upload
<Upload> 接入上傳服務,是通過指定 props 實現(xiàn)的。
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:這是核心。用于指定上傳文件的服務路徑。也就是我們前一步實現(xiàn)的 /api/upload
- name:上傳文件時,后端服務獲取文件信息的表單字段名
- onChange:用于觀察上傳結(jié)果。上傳成功或失敗都會觸發(fā)
下面,我們再試一下文件上傳。
圖片
提示文件被成功上傳了!
在控制臺中,我們還能看到打印出來的響應內(nèi)容。
圖片
onChange 回調(diào)函數(shù)的 info 參數(shù)中,包含 2 個屬性: file 和 fileList。file 指代當前上傳的文件;fileList 則代表當前 <Upload> 組件中已有/還剩下的文件列表。
你可以通過 file.response 獲得服務端的響應數(shù)據(jù),同時可以通過 originFileObj 屬性獲得上傳文件的原始信息。
與 <Form> 組件配合使用
當然,在實際的業(yè)務場景中,在文件上傳之后流程并未結(jié)束,我們通常還需要將服務端返回的圖片路徑跟隨其他表單字段一起保存起來,比如更新個人中心里的頭像這類場景。
這個時候 <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);
// 發(fā)送表單數(shù)據(jù)到后端
};
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" 配置,這樣在上傳完圖片后,可以小圖預覽。
選擇文件,點擊上傳:
圖片
點擊“提交”,控制臺便能看到打印出來的表單數(shù)據(jù)了。
圖片
可以看到,最近一次的 onChang 事件里的內(nèi)容,會作為數(shù)據(jù)在提交表單時使用。
另外,uploadProps 還提供了 fileList prop 參數(shù)用來手動控制預覽文件列表的展示行為,比如我們只想查看最近兩次的圖片預覽,就可以這樣做:
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 張,之前舊的就會被剔除。
福利內(nèi)容:Ctrl + V 上傳文件
Ant Design 的 <Upload> 并未直接提供粘貼上傳文件的支持。這個時候就要通過 ref 操作其實例來實現(xiàn)了。
首先,uploadProps 增加 ref 支持,這樣我們就能訪問到 <Upload> 的底層實例對象了。
const props = {
ref: uploadRef,
// ...
};
<Form.Item
name="file"
rules={[{ required: true, message: '請選擇文件' }]}
>
<Upload {...props}>
上傳文件
</Upload >
</Form.Item>
接著為要支持粘貼上傳的文本框綁定 onPaste 事件,提供粘貼行為的監(jiān)聽。
<textarea
notallow={handlePaste}
placeholder="粘貼文件到此處"
/>
在處理函數(shù)內(nèi)部像下面這樣寫。
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] } }); // 手動觸發(fā) Upload 組件的上傳操作
}
}
};
- 首先,獲取獲取剪貼板上的文件(只取第一個)
- 手動調(diào)用 uploadRef.current.upload.uploader.onChange() 方法,傳入手動拼湊的事件對象 { target: { files: [file] } } 即可。
注意,這種上傳能力是我自己摸索出來的,并不是官方做法。最好是作為漸進功能使用。
總結(jié)
本文我們循序漸進地講解了 Ant Design 中 <Upload> 上傳組件的用法。涵蓋基礎(chǔ)用法、簡單上傳服務地編寫以及 uploadProps 的核心 prop 的定義和使用。