Чтобы прочитать больше статей из этой серии, пожалуйста, посетите мойБлог 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--
Анализ данных загрузки файлов
Анализируя данные, полученные сервером в приведенном выше примере, можно получить следующую информацию:
- Данные, загружаемые формой, разделяются разделителем "------WebKitFormBoundaryL5AGcit70yhKB92Y", и разделитель для каждой загрузки разный. Данные разделителя можно получить из req.headers['content-type'], например:
const boundary = '--' + req.headers['content-type'].split('; ')[1].split('=')[1]
. - В первых двух частях данных можно получить имя поля name="username", загруженное формой, и данные "lee" соответственно.
- В третьем фрагменте данных есть дополнительное поле 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, а "--".
Этапы обработки данных загрузки файла
- Разделить данные с помощью
:
[
‘’,
"\r\n字段信息\r\n\r\n内容\r\n",
"\r\n字段信息\r\n\r\n内容\r\n",
"\r\n字段信息\r\n\r\n内容\r\n",
'--'
]
- Удалите данные головы и хвоста массива:
[
"\r\n字段信息\r\n\r\n内容\r\n",
"\r\n字段信息\r\n\r\n内容\r\n",
"\r\n字段信息\r\n\r\n内容\r\n",
]
- Удалите \r\n в начале и в конце каждого элемента данных:
[
"字段信息\r\n\r\n内容",
"字段信息\r\n\r\n内容",
"字段信息\r\n\r\n内容",
]
- Удалите \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)