Node.js Express 文件上传与下载功能实现
Node.js Express 文件上传功能实现
准备工作
在开始实现文件上传功能之前,需要确保已经安装了 Node.js
和 Express
。如果尚未安装,可以通过以下命令进行安装:
npm install express
同时,我们还需要一个处理文件上传的中间件,常用的是 multer
。通过以下命令安装 multer
:
npm install multer
理解 multer
multer
是一个在 Node.js
中处理 multipart/form-data
表单数据的中间件,非常适合处理文件上传。multer
将上传的文件存储在内存或磁盘上,并提供了一系列的配置选项来控制文件存储的方式。
基本文件上传实现
- 创建 Express 应用:
首先,创建一个基本的
Express
应用,代码如下:
const express = require('express');
const app = express();
const port = 3000;
app.listen(port, () => {
console.log(`Server running on port ${port}`);
});
- 引入 multer 并配置:
在应用中引入
multer
并进行基本配置。multer
需要一个存储引擎,这里我们使用磁盘存储引擎将文件存储在本地磁盘上。
const multer = require('multer');
// 配置存储引擎
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});
const upload = multer({ storage: storage });
在上述代码中,destination
函数指定了文件上传后的存储目录为 uploads/
,如果该目录不存在需要提前创建。filename
函数指定了上传文件的命名规则,这里使用当前时间戳加上原始文件名。
- 处理文件上传路由:
添加一个处理文件上传的路由。假设我们有一个表单,通过
POST
请求将文件发送到/upload
路由。
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).send('No file uploaded');
}
res.send('File uploaded successfully');
});
在这个路由处理函数中,upload.single('file')
表示只处理单个文件上传,其中 'file'
是表单中文件字段的名称。如果没有接收到文件,返回 400 错误。如果文件上传成功,返回成功信息。
处理多个文件上传
如果需要处理多个文件上传,可以使用 upload.array
或 upload.fields
。
- 使用 upload.array:
假设表单中有多个文件字段,名称都为
file
。
app.post('/uploadMultiple', upload.array('file', 5), (req, res) => {
if (!req.files || req.files.length === 0) {
return res.status(400).send('No files uploaded');
}
res.send('Multiple files uploaded successfully');
});
upload.array('file', 5)
中 'file'
是文件字段名称,5
表示最多允许上传 5 个文件。
- 使用 upload.fields:
如果表单中有多个不同名称的文件字段,可以使用
upload.fields
。
app.post('/uploadFields', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'documents', maxCount: 3 }
]), (req, res) => {
const errors = [];
if (!req.files.avatar || req.files.avatar.length === 0) {
errors.push('No avatar uploaded');
}
if (!req.files.documents || req.files.documents.length === 0) {
errors.push('No documents uploaded');
}
if (errors.length > 0) {
return res.status(400).send(errors.join(', '));
}
res.send('Files uploaded successfully');
});
在上述代码中,upload.fields
接受一个数组,数组中每个对象指定了文件字段的名称和最大允许上传的数量。
限制文件类型和大小
- 限制文件类型:
可以通过
fileFilter
选项来限制上传文件的类型。
const upload = multer({
storage: storage,
fileFilter: function (req, file, cb) {
const allowedMimes = [
'image/jpeg',
'image/png',
'application/pdf'
];
if (allowedMimes.includes(file.mimetype)) {
cb(null, true);
} else {
cb(new Error('Invalid file type'));
}
}
});
在 fileFilter
函数中,检查文件的 mimetype
是否在允许的类型列表中。如果不在,通过 cb
传递一个错误。
- 限制文件大小:
可以通过
limits
选项来限制文件大小。
const upload = multer({
storage: storage,
limits: {
fileSize: 1024 * 1024 * 5 // 5MB
}
});
上述代码将文件大小限制为 5MB,如果上传的文件超过这个大小,multer
会抛出一个错误。
处理上传错误
在文件上传过程中,可能会出现各种错误,如文件类型不允许、文件大小超过限制等。我们需要在路由处理函数中捕获这些错误并进行适当的处理。
app.post('/upload', upload.single('file'), (req, res) => {
if (!req.file) {
return res.status(400).send('No file uploaded');
}
res.send('File uploaded successfully');
}).on('error', (err) => {
if (err.code === 'LIMIT_FILE_SIZE') {
res.status(413).send('File size exceeds limit');
} else if (err.message === 'Invalid file type') {
res.status(400).send('Invalid file type');
} else {
res.status(500).send('Internal server error');
}
});
通过监听 error
事件,我们可以根据不同的错误类型返回相应的 HTTP 状态码和错误信息。
Node.js Express 文件下载功能实现
基本文件下载
- 设置静态文件服务:
如果要下载的文件是静态文件,可以使用
express.static
中间件来设置静态文件服务。假设我们有一个downloads
目录存放要下载的文件。
app.use('/downloads', express.static('downloads'));
这样,通过访问 http://localhost:3000/downloads/filename
就可以直接下载 downloads
目录下的 filename
文件。
- 动态生成下载内容: 有时候,我们可能需要动态生成文件内容并提供下载。例如,生成一个文本文件并提供下载。
app.get('/downloadText', (req, res) => {
const text = 'This is a dynamically generated text for download';
res.set({
'Content-Disposition': 'attachment; filename="dynamicText.txt"',
'Content-Type': 'text/plain'
});
res.send(text);
});
在上述代码中,通过设置 Content-Disposition
头来指定文件作为附件下载,并指定文件名。Content-Type
设置为 text/plain
表示文件内容是纯文本。
下载二进制文件
- 下载图片文件示例: 假设要下载一个图片文件,可以读取图片文件并设置相应的头信息。
const fs = require('fs');
const path = require('path');
app.get('/downloadImage', (req, res) => {
const filePath = path.join(__dirname, 'images', 'example.jpg');
const stat = fs.statSync(filePath);
res.set({
'Content-Length': stat.size,
'Content-Type': 'image/jpeg',
'Content-Disposition': 'attachment; filename="example.jpg"'
});
const readStream = fs.createReadStream(filePath);
readStream.pipe(res);
});
在这个例子中,首先获取文件的状态信息,以设置 Content-Length
头。然后创建一个可读流,并通过 pipe
将其连接到响应,这样就可以将文件内容发送给客户端进行下载。
- 处理大文件下载: 对于大文件下载,直接读取整个文件到内存可能会导致内存溢出。可以使用流的方式分段读取和传输。
app.get('/downloadLargeFile', (req, res) => {
const filePath = path.join(__dirname, 'largeFiles', 'bigFile.zip');
const readStream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 }); // 每次读取 1MB
const stat = fs.statSync(filePath);
res.set({
'Content-Length': stat.size,
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="bigFile.zip"'
});
readStream.on('error', (err) => {
res.status(500).send('Error reading file');
});
readStream.pipe(res);
});
通过设置 highWaterMark
来控制每次读取的缓冲区大小,有效地处理大文件下载,同时监听可读流的 error
事件,以便在读取文件出错时返回错误信息。
下载进度跟踪
- 客户端实现下载进度跟踪:
在客户端(如 HTML5 的
fetch
API)可以通过监听progress
事件来跟踪下载进度。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Download Progress</title>
</head>
<body>
<button id="downloadButton">Download File</button>
<progress id="progressBar" value="0" max="100"></progress>
<script>
const downloadButton = document.getElementById('downloadButton');
const progressBar = document.getElementById('progressBar');
downloadButton.addEventListener('click', () => {
const xhr = new XMLHttpRequest();
xhr.open('GET', '/downloadLargeFile', true);
xhr.responseType = 'blob';
xhr.onprogress = function (e) {
if (e.lengthComputable) {
const percentComplete = (e.loaded / e.total) * 100;
progressBar.value = percentComplete;
}
};
xhr.onload = function () {
if (xhr.status === 200) {
const blob = new Blob([xhr.response], { type: 'application/zip' });
const link = document.createElement('a');
link.href = window.URL.createObjectURL(blob);
link.download = 'bigFile.zip';
link.click();
}
};
xhr.send();
});
</script>
</body>
</html>
在上述代码中,通过 XMLHttpRequest
的 onprogress
事件获取下载进度,并更新页面上的进度条。
- 服务端辅助实现下载进度跟踪:
在服务端,可以通过一些额外的手段来辅助客户端更好地跟踪下载进度。例如,通过设置
Content-Range
头。
app.get('/downloadLargeFile', (req, res) => {
const filePath = path.join(__dirname, 'largeFiles', 'bigFile.zip');
const readStream = fs.createReadStream(filePath, { highWaterMark: 1024 * 1024 });
const stat = fs.statSync(filePath);
let bytesSent = 0;
readStream.on('data', (chunk) => {
bytesSent += chunk.length;
const contentRange = `bytes ${bytesSent - chunk.length}-${bytesSent - 1}/${stat.size}`;
res.set('Content-Range', contentRange);
});
res.set({
'Content-Length': stat.size,
'Content-Type': 'application/zip',
'Content-Disposition': 'attachment; filename="bigFile.zip"'
});
readStream.on('error', (err) => {
res.status(500).send('Error reading file');
});
readStream.pipe(res);
});
通过在每次读取数据时更新 Content-Range
头,客户端可以根据这个头信息更准确地计算下载进度。
断点续传
-
原理: 断点续传是指在下载或上传过程中,如果出现网络中断等异常情况,下次可以从上次中断的位置继续进行,而不是重新开始。在 HTTP 协议中,通过
Range
头来实现断点续传。客户端在请求头中发送Range
头,指定要下载的字节范围,服务端根据这个范围读取文件并返回相应部分的数据。 -
服务端实现:
app.get('/downloadResume', (req, res) => {
const filePath = path.join(__dirname, 'largeFiles', 'bigFile.zip');
const stat = fs.statSync(filePath);
const range = req.headers.range;
if (range) {
const parts = range.replace(/bytes=/, '').split('-');
const start = parseInt(parts[0], 10);
const end = parts[1]? parseInt(parts[1], 10) : stat.size - 1;
const chunksize = (end - start) + 1;
const file = fs.createReadStream(filePath, { start, end });
const head = {
'Content-Range': `bytes ${start}-${end}/${stat.size}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'application/zip'
};
res.writeHead(206, head);
file.pipe(res);
} else {
const head = {
'Content-Length': stat.size,
'Content-Type': 'application/zip'
};
res.writeHead(200, head);
fs.createReadStream(filePath).pipe(res);
}
});
在上述代码中,首先检查请求头中的 Range
字段。如果存在 Range
字段,解析出要下载的字节范围,设置相应的 Content-Range
等头信息,并从文件的指定位置开始读取数据。如果没有 Range
字段,则正常返回整个文件。
- 客户端实现:
在客户端,需要在网络中断后重新发起请求时,根据已下载的字节数设置
Range
头。以下是一个简单的示例,使用fetch
API:
async function downloadWithResume() {
let downloadedBytes = 0;
const response = await fetch('/downloadResume');
const totalLength = parseInt(response.headers.get('content-length'), 10);
const fileStream = new WritableStream({
write(chunk) {
downloadedBytes += chunk.length;
// 模拟网络中断
if (downloadedBytes > 1024 * 1024) {
throw new Error('Network error');
}
},
close() {
console.log('Download complete');
}
});
try {
await response.body.pipeTo(fileStream);
} catch (error) {
const newRange = `bytes=${downloadedBytes}-`;
const newResponse = await fetch('/downloadResume', {
headers: {
Range: newRange
}
});
await newResponse.body.pipeTo(fileStream);
}
}
downloadWithResume();
在这个示例中,通过 WritableStream
来模拟文件下载过程,当模拟网络中断时,根据已下载的字节数设置 Range
头,重新发起请求进行断点续传。
通过以上详细的介绍和代码示例,全面地实现了 Node.js Express
中的文件上传与下载功能,包括各种常见的需求和场景处理。无论是简单的文件上传下载,还是复杂的限制条件、进度跟踪和断点续传等功能,都可以在实际项目中灵活应用。