Nodejs教程16:POST檔案上傳

LeeChen發表於2019-03-06

閱讀更多系列文章請訪問我的GitHub部落格,示例程式碼請訪問這裡

簡單的檔案上傳例子

處理檔案上傳資料,也是前後端互動中重要的功能,它的處理方式與資料不同。

接下來,通過一個例子檢視服務端接收到的檔案上傳資料。

首先,在post_file.html中,新建一個用與上傳檔案的表單:

form的屬性enctype="multipart/form-data"代表表單上傳的是檔案。

enctype的預設值為enctype="application/x-www-form-urlencoded"表示上傳的是資料型別,此時服務端接收到的資料為“username=lee&password=123456&file=upload.txt”。

程式碼示例:/lesson16/post_file.html

<form action="http://localhost:8080/upload" method="POST" enctype="multipart/form-data">
  使用者:<input type="text" name="username" value="lee"><br/>
  密碼:<input type="password" name="password" value="123456"><br/>
  <input type="file" name="file" id=""><br/>
  <input type="submit" value="提交">
</form>
複製程式碼

其次,在server.js中,檢視接收到的表單提交資料:

程式碼示例:/lesson16/server.js

const http = require('http')

const server = http.createServer((req, res) => {
  let arr = []

  req.on('data', (buffer) => {
    arr.push(buffer)
  })

  req.on('end', () => {
    let buffer = Buffer.concat(arr)

    console.log(buffer.toString())
  })
})

server.listen(8080)
複製程式碼

最後,在表單中上傳/lesson16/upload.txt檔案,並檢視列印出的結果:

------WebKitFormBoundaryL5AGcit70yhKB92Y
Content-Disposition: form-data; name="username"

lee
------WebKitFormBoundaryL5AGcit70yhKB92Y
Content-Disposition: form-data; name="password"

123456
Content-Disposition: form-data; name="file"; filename="upload.txt"
Content-Type: text/plain

upload
------WebKitFormBoundaryL5AGcit70yhKB92Y--
複製程式碼

檔案上傳資料分析

通過分析上面這個例子中,服務端接收到的資料,可以得到以下資訊:

  1. 表單上傳的資料,被分隔符“------WebKitFormBoundaryL5AGcit70yhKB92Y”隔開,分隔符在每次上傳時都不同。分隔符資料可以從req.headers['content-type']中獲取,如:const boundary = '--' + req.headers['content-type'].split('; ')[1].split('=')[1]
  2. 前兩段資料中,分別可以獲取到表單上傳的欄位名name="username",以及資料“lee”。
  3. 第三段資料中,多了一個欄位filename="upload.txt",它表示的是檔案的原始名稱。以及可以獲取到檔案型別“Content-Type: text/plain”,表示這是一個文字檔案。最後是檔案的內容“upload”。

由此可以看出,檔案上傳資料雖然有些亂,但還是有規律的,那麼處理思路就是按照規律,將資料切割之後,取出其中有用的部分。

檔案上傳資料簡化

先回顧一下上面的資料,並將回車符標記出來:

------WebKitFormBoundaryL5AGcit70yhKB92Y\r\n
Content-Disposition: form-data; name="username"\r\n
\r\n
lee\r\n
------WebKitFormBoundaryL5AGcit70yhKB92Y\r\n
Content-Disposition: form-data; name="password"\r\n
\r\n
123456\r\n
Content-Disposition: form-data; name="file"; filename="upload.txt"\r\n
Content-Type: text/plain\r\n
\r\n
upload\r\n
------WebKitFormBoundaryL5AGcit70yhKB92Y--
複製程式碼

可以看出,每段資料的結構其實是這樣的:

------WebKitFormBoundaryL5AGcit70yhKB92Y\r\nContent-Disposition: form-data; name="username"\r\n\r\nlee\r\n
複製程式碼

將每段上傳資料簡化如下:

<分隔符>\r\n欄位頭\r\n\r\n內容\r\n
複製程式碼

也就是說,整個表單的資料,就是按照這樣的資料格式組裝而成。

需要注意的是,在表單資料的結尾不再是\r\n,而是“--”。

檔案上傳資料處理步驟

  1. 用<分隔符>切分資料:
[
  ‘’,
  "\r\n欄位資訊\r\n\r\n內容\r\n",
  "\r\n欄位資訊\r\n\r\n內容\r\n",
  "\r\n欄位資訊\r\n\r\n內容\r\n",
  '--'
]
複製程式碼
  1. 刪除陣列頭尾資料:
[
  "\r\n欄位資訊\r\n\r\n內容\r\n",
  "\r\n欄位資訊\r\n\r\n內容\r\n",
  "\r\n欄位資訊\r\n\r\n內容\r\n",
]
複製程式碼
  1. 將每一項資料頭尾的的\r\n刪除:
[
  "欄位資訊\r\n\r\n內容",
  "欄位資訊\r\n\r\n內容",
  "欄位資訊\r\n\r\n內容",
]
複製程式碼
  1. 將每一項資料中間的\r\n\r\n刪除,得到最終結果:
[
	"欄位資訊", "內容",
	"欄位資訊", "內容",
	"欄位資訊", "內容",
]
複製程式碼

Buffer的資料處理

由於檔案都是二進位制資料,不能直接將其轉換為字串後再進行處理,否則資料會出錯,因此要通過Buffer模組進行資料處理操作。

Buffer模組提供了indexOf方法獲取Buffer資料中,其引數所在位置的index值。

Buffer模組提供了slice方法,可通過index值切分Buffer資料。

先測試一下這兩個方法:

示例程式碼:/lesson16/buffer.js

let buffer = Buffer.from('lee\r\nchen\r\ntest')

const index = buffer.indexOf('\r\n')

console.log(index)
console.log(buffer.slice(0, index).toString())
複製程式碼

可以看到列印結果分別為3和"lee",也就是說,我們先找到了"\r\n"所在的index為3,之後從Buffer資料的index為0的位置,切割到index為3的位置,得到了正確的結果。

由此,可以封裝一個專門用於切割Buffer資料的方法:

示例程式碼:/lesson16/bufferSplit.js

module.exports = function bufferSplit(buffer, separator) {
  let result = [];
  let index = 0;

  while ((index = buffer.indexOf(separator)) != -1) {
    result.push(buffer.slice(0, index));
    buffer = buffer.slice(index + separator.length);
  }
  result.push(buffer);

  return result;
}
複製程式碼

有了bufferSplit方法,就可以正式開始處理資料了。

檔案上傳資料處理

根據上面的思路,就可以實現一個完整的檔案上傳流程。

程式碼示例:/lesson16/server.js

const http = require('http')
const fs = require('fs')
const bufferSplit = require('./bufferSplit')

const server = http.createServer((req, res) => {
  const boundary = `--${req.headers['content-type'].split('; ')[1].split('=')[1]}`  // 獲取分隔符
  let arr = []

  req.on('data', (buffer) => {
    arr.push(buffer)
  })

  req.on('end', () => {
    const buffer = Buffer.concat(arr)
    console.log(buffer.toString())

    // 1. 用<分隔符>切分資料
    let result = bufferSplit(buffer, boundary)
    console.log(result.map(item => item.toString()))

    // 2. 刪除陣列頭尾資料
    result.pop()
    result.shift()
    console.log(result.map(item => item.toString()))

    // 3. 將每一項資料頭尾的的\r\n刪除
    result = result.map(item => item.slice(2, item.length - 2))
    console.log(result.map(item => item.toString()))

    // 4. 將每一項資料中間的\r\n\r\n刪除,得到最終結果
    result.forEach(item => {
      console.log(bufferSplit(item, '\r\n\r\n').map(item => item.toString()))

      let [info, data] = bufferSplit(item, '\r\n\r\n')  // 資料中含有檔案資訊,保持為Buffer型別

      info = info.toString()  // info為欄位資訊,這是字串型別資料,直接轉換成字串,若為檔案資訊,則資料中含有一個回車符\r\n,可以據此判斷資料為檔案還是為普通資料。

      if (info.indexOf('\r\n') >= 0) {  // 若為檔案資訊,則將Buffer轉為檔案儲存
        // 獲取欄位名
        let infoResult = info.split('\r\n')[0].split('; ')
        let name = infoResult[1].split('=')[1]
        name = name.substring(1, name.length - 1)

        // 獲取檔名
        let filename = infoResult[2].split('=')[1]
        filename = filename.substring(1, filename.length - 1)
        console.log(name)
        console.log(filename)

        // 將檔案儲存到伺服器
        fs.writeFile(`./upload/${filename}`, data, err => {
          if (err) {
            console.log(err)
          } else {
            console.log('檔案上傳成功')
          }
        })
      } else {  // 若為資料,則直接獲取欄位名稱和值
        let name = info.split('; ')[1].split('=')[1]
        name = name.substring(1, name.length - 1)
        const value = data.toString()
        console.log(name, value)
      }
    })
  })
})

server.listen(8080)
複製程式碼

相關文章