通過FormData的方式上傳文件
簡單實現
前端代碼
<div>
<input id="file" type="file" />
<input type="button" value="文件上傳" onclick="uploadFile()" />
</div>
<script>
function uploadFile() {
const file = document.getElementById('file').files[0];
const xhr = new XMLHttpRequest();
const fd = new FormData();
fd.append('file', file);
xhr.open('POST', 'http://127.0.0.1:8000/upload', true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
};
xhr.send(fd);
}
</script>
Node接收端代碼
if(url ==='/upload' && method === 'POST') {
//文件類型
const arr = []
req.on('data', (buffer) => {
arr.push(buffer);
})
req.on('end', () => {
const buffer = Buffer.concat(arr);
const content = buffer.toString();
const result = decodeContent(content);
const fileName = content.match(/(?<=filename=").*?(?=")/)[0];
fileStream(fileName).write(result);
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('上傳完成')
})
}
/**
* @step1 過濾第一行
* @step2 過濾最後一行
* @step3 過濾最先出現Content-Disposition的一行
* @step4 過濾最先出現Content-Type:的一行
*/
const decodeContent = content => {
let lines = content.split('\n');
const findFlagNo = (arr, flag) => arr.findIndex(o => o.includes(flag));
// 查找 ----- Content-Disposition Content-Type 位置並且刪除
const startNo = findFlagNo(lines, '------');
lines.splice(startNo, 1);
const ContentDispositionNo = findFlagNo(lines, 'Content-Disposition');
lines.splice(ContentDispositionNo, 1);
const ContentTypeNo = findFlagNo(lines, 'Content-Type');
lines.splice(ContentTypeNo, 1);
// 最後的 ----- 要在數組末往前找
const endNo = lines.length - findFlagNo(lines.reverse(), '------') - 1;
// 先反轉回來
lines.reverse().splice(endNo, 1);
return Buffer.from(lines.join('\n'));
}
基於formidable庫的文件上傳
Node.js
// server.js
// 以下代碼在mac環境中運行成功
const http = require('http')
const formidable = require('formidable')
const fs = require('fs')
http.createServer(function(req, res) {
if(req.url === '/uploadform') {
const formStr = `
<form action="fileupload" method="post" enctype="multipart/form-data">
<input type="file" name="filetoupload">
<input type="submit" value="Upload">
</form>
`
res.writeHead(200, { 'Content-Type': "text/html; charset=UTF-8"})
res.write(formStr)
return res.end()
} else if(req.url === '/fileupload') {
const form = new formidable.IncomingForm()
form.parse(req, (err, fields, files) => {
const file = files.filetoupload
const uploadPath = process.cwd() + '/upload'
if(!fs.existsSync(uploadPath)) {
fs.mkdirSync(uploadPath)
}
fs.rename(file.filepath, uploadPath + '/' + file.originalFilename, function(err) {
if(err) throw err
res.write('File uploaded and moved!')
res.end()
})
})
}
}).listen(8086)
安裝對應依賴包,執行node server.js命令,然後訪問localhost:8086/uploadform即可訪問文件上傳界面
Koa.js
const koa = require('koa')
const formidable = require('formidable')
const app = new koa()
app.on('error', err => {
console.error('server error', err)
})
app.use(async (ctx, next) => {
if (ctx.url === '/uploadform') {
const formStr = `
<form action="fileupload" method="post" enctype="multipart/form-data">
<input type="file" name="filetoupload">
<input type="submit" value="Upload">
</form>
`
ctx.status = 200
ctx.body = formStr
ctx.set('Content-Type', 'text/html; charset=UTF-8')
} else if (ctx.url === '/fileupload') {
const form = formidable({});
await new Promise((resolve, reject) => {
form.parse(ctx.req, (err, fields, files) => {
if (err) {
reject(err);
return;
}
ctx.set('Content-Type', 'application/json');
ctx.status = 200;
ctx.state = { fields, files };
ctx.body = JSON.stringify(ctx.state, null, 2);
resolve();
});
});
await next();
}
})
app.use((ctx) => {
console.log('The next middleware is called');
console.log('Results:', ctx.state);
});
app.listen(3000, () => {
console.log('Server listening on http://localhost:3000 ...');
});
上傳文件時對文件進行操作
有時候我們在上傳文件時,需要對文件進行一些操作,等待這些操作執行成功後,才能返回結果。比如上傳一個包含多個UTF-8文本文件的zip壓縮包,在上傳後對文件進行解壓,然後讀取解壓後文件的內容。
const fs = require('fs')
const util = require('util')
const stream = require('stream')
const pipeline = util.promisify(stream.pipeline)
const compressing = require('compressing')
const fsExtra = require('fs-extra')
// 基於koa.js
app.use(async (ctx) {
if(ctx.url === '/fileupload') {
const file = ctx.request.files.uploadFile
const reader = fs.createReadStream(file.filepath, { encoding: 'binary'})
// 寫入二進制文件(zip)
const filePath = process.cwd() + '/public/upload'
if(!fs.existsSync(filePath)) {
fs.mkdirSync(filePath)
}
const fileName = decodeURIComponent(file.originalFilename)
const writer = fs.createWriteStream(filePath + '/' + fileName, { encoding: 'binary'})
await pipeline(reader, writer)
// 解壓zip文件
const unzipPath = filePath + '/' + fileName.substring(0, fileName.length - 4)
if(!fs.existsSync(unzipPath)) {
fs.mkdirSync(unzipPath)
}
await compressing.zip.uncompress(filePath + '/' + fileName, unzipPath)
// 讀取解壓後的文件的文本內容
const files = fs.readdirSync(unzipPath)
for(let i = 0; i < files.length; i++) {
const unzipFile = files[i]
const fileStat = fs.statSync(unzipPath + '/' + unzipFile)
if(fileStat.isFile()) {
const content = fs.readFileSync(unzipPath + '/' + unzipFile, { encoding: 'utf-8' })
console.log(content)
}
}
// 刪除zip文件和解壓後的文件
fs.unlink(filePath + '/' + fileName, (err) => {
if(err) {
console.log(err)
}
})
fsExtra.remove(unzipPath, (err) => {
if(err) {
console.log(err)
}
})
ctx.status = 200
}
})
不使用FormData的方式來上傳文件
直接上傳File實例
From表單是一種規範,我就不遵守規範可以嗎?當然可以,不遵守規範即代表你用了新的規範,或者説不叫規範,而是一種前後端都認可的方式,只要你的後端支持就好。
文件上傳的實質是上傳文件的內容以及文件的格式,當我們使用HTML提供的Input上傳文件的時候,它將文件的內容讀進內存裏,那我們直接將內存裏的數據當成普通的數據提交到服務端可以麼?看下面的例子。
<!-- 前端代碼:-->
<div>
<input id="file" type="file" />
<input type="button" value="文件上傳" onclick="uploadFile()" />
</div>
<script>
function uploadFile() {
const file = document.getElementById('file').files[0];
const xhr = new XMLHttpRequest();
xhr.open('POST', `http://127.0.0.1:8000/upload?name=${file.name}`, true);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4 && xhr.status == 200) {
alert(xhr.responseText);
}
}
xhr.send(file);
}
</script>
// Nodejs服務端代碼
...
if(reqUrl.pathname ==='/upload' && method === 'POST') {
const fileName = qs.parse(reqUrl.query).name;
req.pipe(fileStream(fileName));
req.on('end', () => {
res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
res.end('上傳完成');
})
}
...
File
賦值給file變量的是一個File對象構造的實例,File對象繼承自Blob,並擴展了與文件系統相關的功能
FileReader
FileReader 是一個對象,其唯一目的是從 Blob(因此也從 File)對象中讀取數據。
讀取數據的方法有以下四種:
readAsArrayBuffer()readAsBinaryString()readAsDataURL()readAsText()
讀取文件示例:
<input type="file" onchange="readFile(this)">
<script>
function readFile(input) {
let file = input.files[0];
let reader = new FileReader();
reader.readAsText(file);
reader.onload = function() {
console.log(reader.result);
};
reader.onerror = function() {
console.log(reader.error);
};
}
</script>
使用FileReader的readAsDataURL方法讀取文件內容轉換為Base64字符串後上傳
參考:
Node.js 文件上傳
揭秘前端文件上傳原理(一)
揭秘前端文件上傳原理(二)
File 和 FileReader
通過異步迭代簡化 Node.js 流
Node.js stream