Node.js 文件上传与下载功能的实现
Node.js 文件上传功能的实现
理解文件上传原理
在 Web 开发中,文件上传是将本地文件传输到服务器端的过程。当用户在前端页面选择文件并提交表单时,浏览器会将文件数据以特定格式发送到服务器。在 Node.js 环境下处理文件上传,我们需要解析这些数据,提取出文件内容,并将其保存到服务器的指定位置。
HTTP 协议中,文件上传通常使用 multipart/form - data
格式。这种格式会将表单数据(包括文件)分割成多个部分,每个部分都有自己的边界标识。每个部分包含头部信息(如文件名、文件类型等)和数据内容。Node.js 服务器需要正确解析这些部分,才能成功处理文件上传。
使用 Express 框架处理文件上传
Express 是 Node.js 中最流行的 Web 应用框架之一,它提供了丰富的中间件来简化各种 Web 开发任务,包括文件上传。为了处理文件上传,我们需要使用 multer
中间件,它是专门为 Express 设计的文件上传中间件。
- 安装依赖
首先,确保项目目录下有
package.json
文件。如果没有,可以通过npm init -y
命令快速初始化一个。然后安装express
和multer
:
npm install express multer
- 编写上传代码
以下是一个简单的 Express 应用示例,展示如何使用
multer
实现文件上传:
const express = require('express');
const multer = require('multer');
const app = express();
// 设置存储引擎
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 });
app.post('/upload', upload.single('file'), (req, res) => {
res.send('文件上传成功');
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
在上述代码中:
- 我们首先引入了
express
和multer
模块。 - 创建了一个
multer
的存储引擎storage
。destination
函数指定了文件上传后的存储目录为uploads/
,如果该目录不存在,需要手动创建。filename
函数定义了上传文件的命名规则,这里使用当前时间戳加上原始文件名。 - 使用
multer
创建了一个upload
实例,并指定使用single
方法处理单个文件上传,参数'file'
表示前端表单中文件字段的名称。 - 在
/upload
路由中,通过upload.single('file')
中间件处理文件上传,上传成功后返回简单的成功信息。
处理多个文件上传
如果需要处理多个文件上传,multer
同样提供了方便的方法。我们可以使用 array
或 fields
方法。
- 使用
array
方法array
方法用于处理多个具有相同字段名的文件上传。例如:
const express = require('express');
const multer = require('multer');
const app = express();
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 });
app.post('/uploadMultiple', upload.array('files', 5), (req, res) => {
res.send('多个文件上传成功');
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
在这个例子中,upload.array('files', 5)
表示处理字段名为 files
的多个文件上传,最多允许上传 5 个文件。
- 使用
fields
方法fields
方法用于处理多个不同字段名的文件上传。假设前端表单有两个文件字段avatar
和resume
:
const express = require('express');
const multer = require('multer');
const app = express();
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 });
app.post('/uploadFields', upload.fields([
{ name: 'avatar', maxCount: 1 },
{ name: 'resume', maxCount: 1 }
]), (req, res) => {
res.send('多个字段文件上传成功');
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
这里 upload.fields
接受一个数组,数组中的每个对象指定了字段名和该字段允许上传的最大文件数。
自定义文件过滤
有时候,我们可能需要对上传的文件进行过滤,只允许特定类型或大小的文件上传。multer
提供了 fileFilter
选项来实现这一点。
- 按文件类型过滤
以下示例只允许上传图片文件(
.jpg
、.png
、.jpeg
):
const express = require('express');
const multer = require('multer');
const app = express();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});
const fileFilter = (req, file, cb) => {
if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png' || file.mimetype === 'image/jpg') {
cb(null, true);
} else {
cb(new Error('只允许上传图片文件'));
}
};
const upload = multer({ storage: storage, fileFilter: fileFilter });
app.post('/uploadImage', upload.single('image'), (req, res) => {
if (req.file) {
res.send('图片上传成功');
} else {
res.status(400).send('上传失败,只允许上传图片文件');
}
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
在 fileFilter
函数中,我们检查文件的 mimetype
,如果是允许的图片类型,则调用 cb(null, true)
表示通过过滤;否则,调用 cb(new Error('错误信息'))
拒绝上传。
- 按文件大小过滤 假设我们只允许上传小于 1MB 的文件:
const express = require('express');
const multer = require('multer');
const app = express();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});
const fileFilter = (req, file, cb) => {
if (file.size < 1024 * 1024) {
cb(null, true);
} else {
cb(new Error('文件大小超过限制'));
}
};
const upload = multer({ storage: storage, fileFilter: fileFilter });
app.post('/uploadSmallFile', upload.single('file'), (req, res) => {
if (req.file) {
res.send('文件上传成功');
} else {
res.status(400).send('上传失败,文件大小超过限制');
}
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
这里通过检查 file.size
来判断文件大小是否符合要求。
处理文件上传错误
在文件上传过程中,可能会出现各种错误,如文件类型不允许、文件大小超过限制、存储目录不可写等。我们需要妥善处理这些错误,给用户提供友好的反馈。
- 全局错误处理
在 Express 应用中,可以使用全局错误处理中间件来捕获所有路由中的错误。结合
multer
的错误处理:
const express = require('express');
const multer = require('multer');
const app = express();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});
const fileFilter = (req, file, cb) => {
if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png' || file.mimetype === 'image/jpg') {
cb(null, true);
} else {
cb(new Error('只允许上传图片文件'));
}
};
const upload = multer({ storage: storage, fileFilter: fileFilter });
app.post('/uploadImage', upload.single('image'), (req, res) => {
if (req.file) {
res.send('图片上传成功');
}
});
// 全局错误处理中间件
app.use((err, req, res, next) => {
console.error(err);
res.status(400).send(err.message);
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
在这个例子中,当 multer
中间件抛出错误(如文件类型不允许)时,全局错误处理中间件会捕获并返回错误信息给客户端。
- 特定路由错误处理 也可以在特定路由中处理错误,例如:
const express = require('express');
const multer = require('multer');
const app = express();
const storage = multer.diskStorage({
destination: function (req, file, cb) {
cb(null, 'uploads/');
},
filename: function (req, file, cb) {
cb(null, Date.now() + '-' + file.originalname);
}
});
const fileFilter = (req, file, cb) => {
if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png' || file.mimetype === 'image/jpg') {
cb(null, true);
} else {
cb(new Error('只允许上传图片文件'));
}
};
const upload = multer({ storage: storage, fileFilter: fileFilter });
app.post('/uploadImage', upload.single('image'), (req, res, next) => {
if (req.file) {
res.send('图片上传成功');
} else {
next(new Error('上传失败'));
}
}, (err, req, res, next) => {
console.error(err);
res.status(400).send(err.message);
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
这里在 /uploadImage
路由中,通过 next
函数将错误传递给后续的错误处理函数进行处理。
Node.js 文件下载功能的实现
简单文件下载
在 Node.js 中实现文件下载,最基本的方式是使用 fs
(文件系统)模块读取文件内容,并通过 HTTP 响应将文件内容发送给客户端。以下是一个简单的示例,实现下载服务器上的一个文本文件:
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.get('/download', (req, res) => {
const filePath = path.join(__dirname, 'example.txt');
const stat = fs.statSync(filePath);
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Type', 'text/plain');
res.setHeader('Content-Disposition', 'attachment; filename="example.txt"');
const readStream = fs.createReadStream(filePath);
readStream.pipe(res);
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
在上述代码中:
- 我们首先引入了
express
、fs
和path
模块。path
模块用于处理文件路径,确保在不同操作系统上路径的正确性。 - 在
/download
路由中,通过path.join
构建文件的绝对路径。然后使用fs.statSync
获取文件的状态信息,包括文件大小。 - 设置 HTTP 响应头:
Content-Length
表示文件的大小,Content-Type
设置为text/plain
表示文件类型为纯文本,Content-Disposition
中的attachment
指示浏览器将文件作为附件下载,filename
指定下载后的文件名。 - 使用
fs.createReadStream
创建一个可读流来读取文件内容,并通过pipe
方法将可读流连接到响应对象,实现文件内容的传输。
动态文件下载
有时候,我们需要根据用户请求动态生成或选择要下载的文件。例如,根据用户传递的文件名参数下载不同的文件。
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.get('/download/:filename', (req, res) => {
const fileName = req.params.filename;
const filePath = path.join(__dirname, 'files', fileName);
try {
const stat = fs.statSync(filePath);
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
const readStream = fs.createReadStream(filePath);
readStream.pipe(res);
} catch (err) {
res.status(404).send('文件不存在');
}
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
在这个例子中,:filename
是路由参数。通过 req.params.filename
获取用户请求的文件名,然后构建文件路径。如果文件存在,则设置响应头并进行文件下载;如果文件不存在,捕获错误并返回 404 错误信息。
下载大文件的优化
当下载大文件时,直接将整个文件读入内存再发送可能会导致内存溢出。为了优化大文件下载,可以采用以下几种方法:
-
分块读取和传输 在前面的示例中,我们已经使用了
fs.createReadStream
和pipe
方法,它们默认就是分块读取和传输数据的。createReadStream
会以小块(通常是 64KB)的方式读取文件,通过pipe
直接将数据写入响应流,而不需要将整个文件加载到内存中。 -
设置适当的缓冲区大小
createReadStream
可以接受一个options
对象,其中highWaterMark
属性可以设置缓冲区大小。例如:
const readStream = fs.createReadStream(filePath, { highWaterMark: 16384 }); // 16KB 缓冲区
readStream.pipe(res);
适当调整缓冲区大小可以平衡内存使用和传输效率。较小的缓冲区占用内存少,但可能会导致更多的系统调用;较大的缓冲区可能会提高传输效率,但会占用更多内存。
- 使用 HTTP 范围请求(Range Requests)
HTTP 范围请求允许客户端请求文件的部分内容,这在网络不稳定或需要断点续传的场景中非常有用。在 Node.js 中处理范围请求需要解析
Range
请求头,并相应地调整文件读取的起始和结束位置。
以下是一个简单处理范围请求的示例:
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
app.get('/downloadLargeFile', (req, res) => {
const filePath = path.join(__dirname, 'largeFile.txt');
const stat = fs.statSync(filePath);
const { range } = req.headers;
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 });
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${stat.size}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunksize,
'Content-Type': 'text/plain',
'Content-Disposition': 'attachment; filename="largeFile.txt"'
});
file.pipe(res);
} else {
res.writeHead(200, {
'Content-Length': stat.size,
'Content-Type': 'text/plain',
'Content-Disposition': 'attachment; filename="largeFile.txt"'
});
fs.createReadStream(filePath).pipe(res);
}
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
在这个示例中:
- 首先检查请求头中的
range
字段。如果存在range
,则解析出起始和结束位置,计算要读取的块大小。 - 设置
206 Partial Content
状态码,并设置Content-Range
等响应头,告知客户端返回的是文件的部分内容。 - 使用
fs.createReadStream
并指定start
和end
选项,读取文件的指定部分并通过管道传输给客户端。 - 如果没有
range
字段,则按常规方式返回整个文件。
下载不同类型文件的处理
不同类型的文件在下载时可能需要设置不同的 Content-Type
。例如:
- 图片文件:
image/jpeg
、image/png
等。 - PDF 文件:
application/pdf
。 - ZIP 文件:
application/zip
。
以下是一个根据文件扩展名动态设置 Content-Type
的示例:
const express = require('express');
const fs = require('fs');
const path = require('path');
const app = express();
const contentTypeMap = {
'.jpg': 'image/jpeg',
'.png': 'image/png',
'.pdf': 'application/pdf',
'.zip': 'application/zip'
};
app.get('/downloadFile/:filename', (req, res) => {
const fileName = req.params.filename;
const filePath = path.join(__dirname, 'files', fileName);
try {
const stat = fs.statSync(filePath);
const ext = path.extname(fileName);
const contentType = contentTypeMap[ext] || 'application/octet-stream';
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Type', contentType);
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
const readStream = fs.createReadStream(filePath);
readStream.pipe(res);
} catch (err) {
res.status(404).send('文件不存在');
}
});
const port = 3000;
app.listen(port, () => {
console.log(`服务器在端口 ${port} 上运行`);
});
在这个例子中,通过 path.extname
获取文件扩展名,然后在 contentTypeMap
中查找对应的 Content-Type
。如果找不到,则使用默认的 application/octet-stream
。
处理文件下载错误
在文件下载过程中,也可能出现各种错误,如文件不存在、权限不足等。与文件上传类似,我们需要妥善处理这些错误,给用户提供友好的反馈。
- 文件不存在错误 在前面的动态文件下载示例中,我们已经处理了文件不存在的错误:
app.get('/download/:filename', (req, res) => {
const fileName = req.params.filename;
const filePath = path.join(__dirname, 'files', fileName);
try {
const stat = fs.statSync(filePath);
// 下载文件相关代码
} catch (err) {
res.status(404).send('文件不存在');
}
});
这里通过捕获 fs.statSync
可能抛出的错误,判断文件是否存在。如果不存在,返回 404 状态码和错误信息。
- 权限不足错误
当服务器进程没有足够权限读取文件时,
fs.createReadStream
会抛出错误。可以在pipe
方法的错误事件中处理:
app.get('/download/:filename', (req, res) => {
const fileName = req.params.filename;
const filePath = path.join(__dirname, 'files', fileName);
try {
const stat = fs.statSync(filePath);
res.setHeader('Content-Length', stat.size);
res.setHeader('Content-Type', 'application/octet-stream');
res.setHeader('Content-Disposition', `attachment; filename="${fileName}"`);
const readStream = fs.createReadStream(filePath);
readStream.pipe(res);
readStream.on('error', (err) => {
if (err.code === 'EACCES') {
res.status(403).send('权限不足,无法下载文件');
} else {
res.status(500).send('下载文件时发生错误');
}
});
} catch (err) {
res.status(404).send('文件不存在');
}
});
在这个例子中,监听 readStream
的 error
事件。如果错误码是 EACCES
,表示权限不足,返回 403 状态码和相应错误信息;其他错误返回 500 状态码。
通过以上详细的介绍和代码示例,希望能帮助你全面掌握 Node.js 中文件上传与下载功能的实现。在实际应用中,需要根据具体需求进行进一步的优化和扩展。