1. 程式人生 > >node.js+react全棧實踐-Form中按照指定路徑上傳檔案並

node.js+react全棧實踐-Form中按照指定路徑上傳檔案並

書接上回,講到“使用同一個新增彈框”中有未解決的問題,比如複雜的欄位,檔案,圖片上傳,這一篇就解決檔案上傳的問題。這裡的場景是在新增彈出框中要上傳一個圖片,並且這個上傳元件放在一個Form中,和其他文字欄位一起提交給介面。

這裡就有幾個要注意的問題:

  1. 圖片上傳時最好能在前端指定圖片型別,根據這個型別上傳到指定的目錄。比如這裡是新增使用者,上傳使用者圖片,那麼這裡就指定型別是“user”,那麼就把這個檔案上傳到伺服器的upload/user目錄中。這樣方便後期維護,比如要把專案中的檔案統一遷移到另外一個伺服器,只要把upload目錄複製出來就好了。
  2. 上傳元件是通用的,上傳完之後回傳給前端一個路徑資訊,由於使用的是and design中的Form,這時要把這個路徑賽到form的資料中一併提交給新增介面。

1.後端上傳檔案介面

1.1 使用multer

前面在寫新增資料,請求資料的時候使用的到中介軟體bodyParser,解析客戶端請求的時候,使用的json型別接受資料,這個很方便,但是上傳檔案的時候是一般是multipart/form-data這種型別,bodyParser不能解析這種型別。於是這裡引入另外一種中介軟體multer。multer專門處理multipart/form-data型別的表單資料,專業的。

multer有兩種使用方式,如果只是一般的網頁應用,直接指定dest,也就是上傳路徑就可以了。如果上傳時進行更多的控制,可以使用storage選項。這裡我從簡單的入手,直接指定檔案路徑上傳一個檔案。

// 指定檔案上傳路徑
var upload = multer({dest: path.join(__dirname, './../public/upload/tmp')}); 

這裡使用到node.js中的path模組,將./../public/upload/tmp這個相對路徑轉換成計算機本地路徑,注意這裡我們在express專案的public目錄下新建了upload/tmp目錄,至於為啥是tmp這樣的臨時資料夾,請繼續往下看。

接著定義上傳介面:

    router.post('/singleFile', upload.single('file'), function (req, res, next) {
    }) 

這裡我們定義了一個api/base/singleFile介面,接受Form中一個名叫file的上傳檔案標籤,這樣定義之後就可以吧檔案上傳到public/upload/tmp目錄下。

1.2 指定上傳目錄

multer這種指定路徑上傳的方式是一開始就指定好了,後面都上傳到這個目錄,就是說這個目錄不能是一個變數,那如何能夠根據前端傳過來的引數將圖片上傳到指定的目錄呢?我這裡首先想到的就是“剪下”檔案。既然用的是node.js,檔案操作的api就少不了剪下檔案了。還有官方文件上說明了,回撥函式中除了檔案之外,還可以有req.body,如果有文字域資料,將在這個req.body中,這個和bodyParser是類似的。

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跟蹤一下執行過程:

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裡面,體驗會更好

&n