書接上回,講到「使用同一個新增彈框」中有未解決的問題,好比複雜的字段,文件,圖片上傳,這一篇就解決文件上傳的問題。這裏的場景是在新增彈出框中要上傳一個圖片,而且這個上傳組件放在一個Form中,和其餘文本字段一塊兒提交給接口。html
這裏就有幾個要注意的問題:前端
- 圖片上傳時最好能在前端指定圖片類型,根據這個類型上傳到指定的目錄。好比這裏是新增用戶,上傳用戶圖片,那麼這裏就指定類型是「user」,那麼就把這個文件上傳到服務器的upload/user目錄中。這樣方便後期維護,好比要把項目中的文件統一遷移到另一個服務器,只要把upload目錄複製出來就行了。
- 上傳組件是通用的,上傳完以後回傳給前端一個路徑信息,因爲使用的是and design中的Form,這時要把這個路徑賽到form的數據中一併提交給新增接口。
1.後端上傳文件接口
1.1 使用multer
前面在寫新增數據,請求數據的時候使用的到中間件bodyParser,解析客戶端請求的時候,使用的json類型接受數據,這個很方便,可是上傳文件的時候是通常是multipart/form-data這種類型,bodyParser不能解析這種類型。因而這裏引入另一種中間件multer。multer專門處理multipart/form-data類型的表單數據,專業的。node
multer有兩種使用方式,若是隻是通常的網頁應用,直接指定dest,也就是上傳路徑就能夠了。若是上傳時進行更多的控制,可使用storage選項。這裏我從簡單的入手,直接指定文件路徑上傳一個文件。react
// 指定文件上傳路徑 var upload = multer({dest: path.join(__dirname, './../public/upload/tmp')});
這裏使用到node.js中的path模塊,將./../public/upload/tmp這個相對路徑轉換成計算機本地路徑,注意這裏咱們在express項目的public目錄下新建了upload/tmp目錄,至於爲啥是tmp這樣的臨時文件夾,請繼續往下看。git
接着定義上傳接口:github
router.post('/singleFile', upload.single('file'), function (req, res, next) { })
這裏咱們定義了一個api/base/singleFile接口,接受Form中一個名叫file的上傳文件標籤,這樣定義以後就能夠吧文件上傳到public/upload/tmp目錄下。express
1.2 指定上傳目錄
multer這種指定路徑上傳的方式是一開始就指定好了,後面都上傳到這個目錄,就是說這個目錄不能是一個變量,那如何可以根據前端傳過來的參數將圖片上傳到指定的目錄呢?我這裏首先想到的就是「剪切」文件。既然用的是node.js,文件操做的api就少不了剪切文件了。還有官方文檔上說明了,回調函數中除了文件以外,還能夠有req.body,若是有文本域數據,將在這個req.body中,這個和bodyParser是相似的。json
app.post('/profile', upload.single('avatar'), function (req, res, next) { // req.file 是 `avatar` 文件的信息 // req.body 將具備文本域數據,若是存在的話 })
有了req.file,req.body這兩個對象以後剩下的工做就交給node.js了,代碼以下:後端
// 文件上傳 router.post('/singleFile', upload.single('file'), function (req, res, next) { if(req.body.fileLocation) { const newName = req.file.path.replace(/\\tmp/, '\\' + req.body.fileLocation) + path.parse(req.file.originalname).ext fs.rename(req.file.path, newName, err => { if (err) { res.json(result.createResult(false, { message: err.message })) } else { let fileName = newName.split('\\').pop() res.json(result.createResult(true, { path: `${req.body.fileLocation}/${fileName}` })) } }) } else { res.json(result.createResult(false, {message: '未指定文件路徑'})) } })
注意在這裏還使用了fs模塊的rename方法,這個方法能夠將文件重命名並修改文件路徑,就是剪切文件了。這裏用replace方法把tmp目錄替換成前端傳過來的fileLocalhost,而後將文件移動到這個fileLocation目錄中。下面使用postman來debug跟蹤一下執行過程:api
postman請求:
上傳到tmp目錄:
移動到指定的user目錄:
postman返回:
至此,接口就寫好了,下面就是在前端調用這個接口。
2. 前端Form裏調用接口
2.1 定義字段類型
在上一篇node.js+react全棧實踐-開篇中,使用的是統一的數據添加組件來添加,數據。columns.js中未指定字段類型,都是文本框,這顯然不切合實際,在這裏再加上一個屬性type:file表示在添加數據組件中,這個字段對應一個上傳文件組件。另外,若是對文件類型,大小有限制,這裏也能夠添加accept,size字段。代碼以下:
const thumb = { title: '頭像', dataIndex: 'thumb', key: 'thumb', render: src => <img className={style.tableImg} alt='' src={ `${config.baseUrl.resource.upload}${src}` }/>, type: 'file', accept: 'image/gif,image/jpeg', size: 2 }
2.2 Upload上傳組件
剩下的就要研究一下ant design中的Upload組件,看一下文檔就明白了。關鍵代碼以下:
{field.map((f, index) => { switch (f.type) { case 'file': return <FormItem name='file' headers={headers} key={f.key} label={f.title}> {getFieldDecorator(f.key)(<div> <Upload name="file" accept={f.accept} data={data} listType="picture-card" showUploadList={false} action="http://localhost:3332/api/base/singleFile" beforeUpload={this.beforeFileUpload.bind(this, f)} onChange={this.handleFileChange.bind(this, f)}> {imageURL ? <img src={imageURL} alt="avatar" style={{ width: '100%' }} /> : uploadButton} </Upload> </div>)} </FormItem> default: return <FormItem key={f.key} label={f.title}> {getFieldDecorator(f.key, { rules: [{ validator: this.customerValidator.bind(this, f) }] })(<Input placeholder={'請輸入' + f.title}/>)} </FormItem> } })}
name:這個是字段名字,若是是要調用api/base/singleFile這個接口,就要設置爲file,和上面的upload.single('file')是對應起來的
accept:接受的文件類型,從columns.js中thumb字段中獲取,也能夠在beforUpload回調中驗證類型
data:這個就是除了文件以外額外的參數,能夠指定爲{fileLocation: 'user'}表示要上傳到user子目錄,這裏要讚美一下ant design,已經考慮了額外參數
listType:顯示樣式,參考antd design文檔,不解釋
showUploadList:同上,不解釋
action:上傳文件接口,注意這裏要使用本地api文件中定義的接口,不能使用服務端的接口路徑,不然會代理失敗的
beforUpload:上傳文件以前的鉤子,這裏要讚美一下ant design,能夠額外傳一個參數f,帶入字段信息,這樣就能夠獲取字段的accept,size信息,進行驗證
onChange:文件狀態改變時的鉤子,繼續讚美一下ant design,同上,能夠額外傳遞一個參數
這裏有一個小疑問:antd design中解釋onChange:「上傳中、完成、失敗都會調用這個函數」,我測試了一下,確實會調用三次,可是有兩次都返回了response,status都是done,和我想象的不同。這上傳成功了,按說有上傳中,完成個回調,那都是done是怎麼回事,「完成」調用了兩次?
onChang回調:
到這裏,接口已調通,文件已經可以成功的從前端傳到後端了。
2.3 Form獲取文件路徑
最後一個問題,這裏使用Form組件填充,收集數據,Form中上傳組件是單獨的跑起來的,最後獲得的是一個url,不是文件自己,如何將這個url給到form中呢?這裏使用的是form.setFieldsValue({name: value})這個方法,簡答粗暴。代碼以下:
handleFileChange(field, info) { let file = info.file if (file.response && file.response.success && file.response.data && file.response.data.path) { let { upload } = this.state upload.imageURL = `${config.baseUrl.resource.upload}${file.response.data.path}` // 爲Form對應的字段設置值 this.props.form.setFieldsValue({ [`${field.key}`]: file.response.data.path }) this.setState({ upload }) upload.loading = false } }
注意這裏FormItem是動態加載出來的,並不知道是那個字段,因此onChange回調中額外傳遞了參數f,這樣,setFildsValue中就知道這是要設置Form中哪個數據。
最後看一下效果:
上傳文件:
數據表:
未解決的問題:
1.上傳過程當中若是由於其餘問題致使失敗,而且是在轉移以前失敗,服務器上upload/tmp目錄會有不少的垃圾文件,這裏能夠在轉移以後把tmp目錄中的文件所有刪掉
2.文件的校驗是放在beforUpdate鉤子裏經過全局提示message.error彈出,這個是否是能夠放在getFieldDecorator的rules裏面,體驗會更好
原文出處:https://www.cnblogs.com/tylerdonet/p/12021663.html