MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

Node.js Express 文件上传与下载功能实现

2021-11-081.3k 阅读

Node.js Express 文件上传功能实现

准备工作

在开始实现文件上传功能之前,需要确保已经安装了 Node.jsExpress。如果尚未安装,可以通过以下命令进行安装:

npm install express

同时,我们还需要一个处理文件上传的中间件,常用的是 multer。通过以下命令安装 multer

npm install multer

理解 multer

multer 是一个在 Node.js 中处理 multipart/form-data 表单数据的中间件,非常适合处理文件上传。multer 将上传的文件存储在内存或磁盘上,并提供了一系列的配置选项来控制文件存储的方式。

基本文件上传实现

  1. 创建 Express 应用: 首先,创建一个基本的 Express 应用,代码如下:
const express = require('express');
const app = express();
const port = 3000;

app.listen(port, () => {
    console.log(`Server running on port ${port}`);
});
  1. 引入 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 函数指定了上传文件的命名规则,这里使用当前时间戳加上原始文件名。

  1. 处理文件上传路由: 添加一个处理文件上传的路由。假设我们有一个表单,通过 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.arrayupload.fields

  1. 使用 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 个文件。

  1. 使用 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 接受一个数组,数组中每个对象指定了文件字段的名称和最大允许上传的数量。

限制文件类型和大小

  1. 限制文件类型: 可以通过 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 传递一个错误。

  1. 限制文件大小: 可以通过 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 文件下载功能实现

基本文件下载

  1. 设置静态文件服务: 如果要下载的文件是静态文件,可以使用 express.static 中间件来设置静态文件服务。假设我们有一个 downloads 目录存放要下载的文件。
app.use('/downloads', express.static('downloads'));

这样,通过访问 http://localhost:3000/downloads/filename 就可以直接下载 downloads 目录下的 filename 文件。

  1. 动态生成下载内容: 有时候,我们可能需要动态生成文件内容并提供下载。例如,生成一个文本文件并提供下载。
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 表示文件内容是纯文本。

下载二进制文件

  1. 下载图片文件示例: 假设要下载一个图片文件,可以读取图片文件并设置相应的头信息。
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 将其连接到响应,这样就可以将文件内容发送给客户端进行下载。

  1. 处理大文件下载: 对于大文件下载,直接读取整个文件到内存可能会导致内存溢出。可以使用流的方式分段读取和传输。
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 事件,以便在读取文件出错时返回错误信息。

下载进度跟踪

  1. 客户端实现下载进度跟踪: 在客户端(如 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>

在上述代码中,通过 XMLHttpRequestonprogress 事件获取下载进度,并更新页面上的进度条。

  1. 服务端辅助实现下载进度跟踪: 在服务端,可以通过一些额外的手段来辅助客户端更好地跟踪下载进度。例如,通过设置 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 头,客户端可以根据这个头信息更准确地计算下载进度。

断点续传

  1. 原理: 断点续传是指在下载或上传过程中,如果出现网络中断等异常情况,下次可以从上次中断的位置继续进行,而不是重新开始。在 HTTP 协议中,通过 Range 头来实现断点续传。客户端在请求头中发送 Range 头,指定要下载的字节范围,服务端根据这个范围读取文件并返回相应部分的数据。

  2. 服务端实现

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 字段,则正常返回整个文件。

  1. 客户端实现: 在客户端,需要在网络中断后重新发起请求时,根据已下载的字节数设置 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 中的文件上传与下载功能,包括各种常见的需求和场景处理。无论是简单的文件上传下载,还是复杂的限制条件、进度跟踪和断点续传等功能,都可以在实际项目中灵活应用。