Учебник Nodejs 16: Загрузка файла POST

Node.js

Чтобы прочитать больше статей из этой серии, пожалуйста, посетите мойБлог GitHub, пример кода, пожалуйста, посетитездесь.

Простой пример загрузки файла

Обработка данных загрузки файлов также является важной функцией взаимодействия с внешним и внутренним интерфейсом, и метод ее обработки отличается от обработки данных.

Далее используйте пример для просмотра данных загрузки файла, полученных сервером.

Сначала в post_file.html создайте новую форму для загрузки файлов:

Атрибут 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" может быть получен, что указывает на то, что это текстовый файл. В конце находится содержимое файла «загрузить».

Из этого видно, что, хотя данные загрузки файла немного беспорядочны, они все же регулярны, поэтому идея обработки состоит в том, чтобы вырезать данные в соответствии с правилами и удалить полезные части.

Упрощение загрузки файлов

Давайте просмотрим данные выше и отметим возврат каретки:

------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 предоставляет метод indexOf для получения значения индекса расположения параметра в данных буфера.

Модуль Buffer предоставляет метод slice, который может нарезать данные 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" равен 3, а затем вырезаем из позиции, где индекс данных буфера равен 0 в позицию, где индекс равен 3 позиции, получили правильный результат.

Из этого можно инкапсулировать метод, предназначенный для вырезания данных из буфера:

Пример кода: /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)