Multer + Express 实现文件上传

一、上期回顾

在上上一期博文中,我们一起学习了如下几个重点知识,在此向大家罗列:

  1. XMLHttpRequest 对象实现了Ajax请求的方案,以及简单的 GET 和 POST 请求
  2. HTTP请求头设置和HTTP响应头的获取
  3. FormData 对象的添加、获取、修改、删除……等功能,并且将大量主要用于上传文件
  4. 结合以上三个知识点,通过 formidable中间件 实现文件上传、下载并显示进度条的功能

如果以上重点知识还未掌握的同学,请先掌握以上知识,再观看本期的知识点,以达到更好的学习效果。观看上期文章,请点击下方链接👇👇👇

XMLHttpRequest 链接地址:https://blog.csdn.net/abraham_ly/article/details/113526496

那么下面进入今天的学习主题,今天我们要学习一个新的Node.js中间件,即 Multer 中间件,主要用于是实现文件上传的功能。

在这里插入图片描述

二、Multer 中间件

1、简介

Multer :是Node.js中的一个第三方包,或者说是第三方中间件。它用于解析或者说处理 multipart/form-data 类型的表单数据,该中间件主要用于文件上传功能的实现。但是 Multer 不会处理任何非 multipart/form-data 类型的表单数据。

所有,要想使用该中间件,则必须先将表单数据转换为 FormData对象,这样 Multer 中间件才有办法解析或处理 FormData 对象

2、安装

打开Node.js终端,切换到当前或者其他目录,输入并执行以下命令,安装最新版本的 multer中间件

npm install multer

3、使用

在安装、导入并使用multer中间件后,Multer 会添加一个 body 对象以及 filefiles 对象到express 模块的request(请求)对象中。然而 body 对象包含表单的文本域信息,即除input:file外的文本域filefiles 对象包含对象表单上传的文件信息,即 input:file内的文件

1、基本使用方法

// 导入express模块
var express = require('express')
// 导入multer中间件
var multer  = require('multer')
// 设置文件上传的地址,即文件路径
var upload = multer({ dest: 'uploads/' })

// 创建服务器
var app = express()

// app.请求类型(路由地址,上传时的文件名称,路由处理函数)
app.post('/profile', upload.single('avatar'), function (req, res, next) {
  // req.file 是 `avatar` 文件的信息
  // req.body 将具有文本域数据,如果存在的话
})

// app.请求类型(路由地址,上传时的文件名称和上传的最大数量,路由处理函数),对文件的最大上传数量做限制
app.post('/photos/upload', upload.array('photos'12)function (req, res, next) {
  // req.files 是 `photos` 文件数组的信息
  // req.body 将具有文本域数据,如果存在的话
})

// 创建混合文件,这里是两个文件,采用对象数组的形式存储每一个文件,并且设置了上传文件的名称和上传的最大数量,如果一次性上传多个文件,则需要使用这种方式
var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }{ name: 'gallery', maxCount: 8 }])
app.post('/cool-profile', cpUpload, function (req, res, next) {
  // req.files 是一个对象 (String -> Array) 键是文件名,值是文件数组
  //
  // 例如:
  //  req.files['avatar'][0] -> File
  //  req.files['gallery'] -> Array
  //
  // req.body 将具有文本域数据,如果存在的话
})

2、如果需要处理一个只有文本域的表单(不包含文件),应当使用.none()

// 导入express模块
var express = require('express')
// 创建服务器
var app = express()
// 导入multer中间件
var multer  = require('multer')
// 设置文件上传地址无
var upload = multer()

// 全局的路由中间件
app.post('/profile', upload.none()function (req, res, next) {
  // req.body 包含文本域
})

4、API接口说明

每个文件都应该具有以下信息:

1、文件属性说明
Key(键)Description(描述)Note(注意)
fieldnameField name 由表单指定
originalname用户计算机上的文件的名称
encoding文件编码
mimetype文件的 MIME 类型
size文件大小(字节单位)
destination保存路径DiskStorage(磁盘存储引擎)
filename保存在 destination 中的文件名DiskStorage
path已上传文件的完整路径DiskStorage
buffer一个存放了整个文件的 BufferMemoryStorage(内存存储引擎)
2、multer 方法说明
  • multer(options)构造函数

    Multer 接受一个 options 对象,其中最基本的是dest属性,这将告诉Multer将上传文件保存到什么地方。如果省略了 options 对象,这些文件将保存在内存之中,永远不会写入磁盘。

    为了避免命名冲突,Multer会修改上传上传的文件名。这个冲重命名功能可以根据您的需要定制。

    以下是可以传递给 multer 的选项:

    Key(键)Description(描述)
    dest or storage在哪里存储文件
    fileFilter文件过滤器,控制哪些文件可以被接受
    limits限制上传的数据
    preservePath保存包含文件名的完整文件路径

    通常,一般的网页应用,只需要设置dest属性即可,像这样:

    var upload = multer({ dest: 'uploads/' });
    

    如果你想在上传时进行更多的控制,那么你可以使用storage代替dest。 Multer 具有 DiskStorage(磁盘存储引擎)MemoryStorage(内存存储引擎)两个存储引擎;另外还可以从第三方获得更多的可用引擎。

  • .single(fieldname)

接受一个以fieldname命名的文件。这个文件的信息保存在req.file中。

// app.请求类型(路由地址,上传时的文件名称,路由处理函数)
app.post('/profile', upload.single('avatar')function (req, res, next) {
  // req.file 是 `avatar` 文件的信息
  // req.body 将具有文本域数据,如果存在的话
})
  • .array(fieldname[,maxCount])

接受一个以fieldname命名的文件数组,可以配置maxCount来限制上传的最大数量,这些文件的信息保存在req.files中。

// app.请求类型(路由地址,上传时的文件名称和上传的最大数量,路由处理函数),对文件的最大上传数量做限制
app.post('/photos/upload', upload.array('photos'12)function (req, res, next) {
  // req.files 是 `photos` 文件数组的信息
  // req.body 将具有文本域数据,如果存在的话
})
  • .fields(fields)

    接受指定的fields的混合文件,这些文件的信息保存在req.files中,fields应该是一个对象数组,应该具有name属性和可选的maxCount属性。

    Example:

    var cpUpload = upload.fields([{ name: 'avatar', maxCount: 1 }{ name: 'gallery', maxCount: 8 }])
    app.post('/cool-profile', cpUpload, function (req, res, next) {
      // req.files 是一个对象 (String -> Array) 键是文件名,值是文件数组
      //
      // 例如:
      //  req.files['avatar'][0] -> File
      //  req.files['gallery'] -> Array
      //
      // req.body 将具有文本域数据,如果存在的话
    })
    
  • .none()

只接受文本域。如果任何文件上传到这个模式,将发生 “LIMIT_UNEXPECTED_FILE” 错误。这和upload.fields([])的效果是一样的。

// 全局的路由中间件
app.post('/profile', upload.none()function (req, res, next) {
  // req.body 包含文本域
})
  • .any()

接受一切上传的文件。文件数组将保存在req.files中。

// 全局的路由中间件
app.post('/profile', upload.none()function (req, res, next) {
	// req.files 包含文件数组
  // req.body 包含文本信息
})

⚠ 警告:确保你总是处理了用户的文件上传。请永远不要将 multer 中间件作为全局中间件使用,因为恶意用户可以上传文件到一个你无法预料的路由之中,那么就应该在你需要处理文件上传的路由上使用

// 在单独的路由上去使用multer中间件
router.post("/add", upload.single("conpicimage"), articleHandler.addArticle);

5、storage(存储引擎)

磁盘存储引擎(DiskStorage

磁盘存储引擎可以让你控制文件的存储。

// 配置磁盘存储引擎的选项
var storage = multer.diskStorage({
  destination: function (req, file, cb) {
    cb(null, '/tmp/my-uploads')
  },
  filename: function (req, file, cb) {
    cb(null, file.fieldname + '-' + Date.now())
  }
})

// 使用磁盘存储引擎
var upload = multer({ storage: storage })

有两个选项可用,destinationfilename。它们都是用来确定文件存储位置的函数。

  1. destination:是用来确定上传的文件应该存储在哪个文件夹中,也可以提供一个String字符串(例如'tmp/uploads')。如果没有设置destination,则使用操作系统默认的临时文件夹。

    注意 ❗:如果你提供的destination是一个函数,你需要负责创建文件夹。当提供一个字符串,multer中间件 将确保这个文件夹是你创建的。

  2. filename:用于确定文件夹中的文件名的确定。如果没有设置,每个文件将设置一个随机文件名,并且是没有扩展名的

    注意 ❗

    • multer中间件 不会为你添加任何扩展名,所有你的程序应该返回一个反正的文件名。
    • 每个函数都传递了请求对象(req)和一些关于这个文件的的信息(file),有助于你的决定
    • req.body 可能还没有完全填充,这取决于客户端发送字段和文件到服务器的顺序
内存存储引擎(MemoryStorage

内存存储引擎将文件存储在内存中的 Buffer 对象中,它没有任何选项

// 配置内存存储引擎
var storage = multer.memoryStorage()
// 使用内存存储引擎
var upload = multer({ storage: storage })

当使用内存存储引擎,文件信息将包含一个 buffer 字段,里面包含了整个文件数据。

警告⚠ :当你使用内存存储,上传非常大或者非常小的文件,都会导致你的应用程序内存溢出。

6、fileFilter(文件过滤)

设置一个函数,用来控制什么文件可以上传,什么文件应该跳过,这个函数应该看起来像这样:

function fileFilter (req, file, cb) {

  // 这个函数应该调用 `cb` 用boolean值来
  // 指示是否应接受该文件

  // 拒绝这个文件,使用`false`,像这样:
  cb(null, false)

  // 接受这个文件,使用`true`,像这样:
  cb(null, true)

  // 如果有问题,你可以总是这样发送一个错误:
  cb(new Error('I don\'t have a clue!'))

}

7、错误处理机制

如果上传文件时,遇到一个异常错误,我们需要捕获multer抛出的异常,可以使用multer对象下的 MulterError类(即err instanceof multer.MulterError),异常捕获代码如下:

var multer = require('multer')
  // 注意❗:当进行multer异常捕获时,single函数一定要写在外边,如果不进行异常捕获,那么可以像上文一样直接挂载在路由上
var upload = multer().single('avatar')

app.post('/profile', function (req, res) {
  upload(req, res, function (err) {
    if (err instanceof multer.MulterError) {
      // 发生错误
    } else if (err) {
      // 发生错误
    }

    // 一切都好
  })
})

三、文件上传

1、案例目录展示

  • 服务器端目录

在这里插入图片描述

  • 前端目录

在这里插入图片描述

2、服务器的搭建

// 导入express模块
const express = require("express");
// 导入cors模块
const cors = require("cors");
// 导入uploads路由模块,单个文件上传的模块
const R_upload = require("./route/upload");
// 导入uploads路由模块,多个文件上传的模块
const R_uploads = require("./route/uploads");
// 创建服务器
const app = express();

// 配置跨域请求的cors中间件
app.use(cors());
// 使用可以解析表单数据的中间件
app.use(express.urlencoded({ extended: false }));
// 使用R_upload模块,单个文件上传的路由
app.use("/api", R_upload);
// 使用R_uploads模块,多个文件上传的路由
app.use("/apis", R_uploads);

// 监听并开启服务器
app.listen(8024, "127.0.0.1", () => {
    console.log("127.0.0.1:8024 服务器已成功开启!");
});

3、单个文件上传

  • 服务器路由接口
// 导入express模块
const express = require("express");
// 导入路径模块
const path = require("path");
// 创建路由对象
const router = express.Router();
// 导入multer模块
const multer = require("multer");
// 配置磁盘存储引擎
const storage = multer.diskStorage({
    // 设置文件上传的路径
    destination: function(req, file, cb) {
        // 使用path模块拼接路径
        cb(null, path.join(__dirname + "./../files/upload"));
    },
    // 设置文件名称
    filename: function(req, file, cb) {
        // 获取当前的时间戳
        let name = new Date().getTime();
        // 以.分割原始文件名称,并返回两个数组
        let extend = file.originalname.split(".");
        // 通过for循环获取数组的最后一个元素,即原始文件的扩展名
        for (let i = extend.length - 1; i < extend.length; i++) {
            // 记录并替换扩展名
            extend = extend[i];
            // 跳出循环
            break;
        }
        // 拼接文件名称和扩展名,生成文件
        let filename = name + "." + extend;
        // 设置文件的名称
        cb(null, filename);
    }
});
// 使用磁盘存储引擎
const upload = multer({
    // 使用磁盘存储引擎
    storage: storage
    // 可接受的文件名称
}).single("photo");
// POST请求,并上传一个以 photo 命名的文件
router.post("/upload", upload, (req, res) => {
    // 如果请求的file对象为空,或者file对象中的fieldname字段名不等于photo,那么就证明没有选择照片或者选择的照片名称不是指定的
    if (!req.file || req.file.fieldname !== "photo") {
        // 响应状态码和提示信息
        return res.send({
            status: 1,
            msg: "照片未选择"
        });
    }
    // 捕获multer引起的异常
    upload(req, res, function(err) {
        // 判断是否是multer模块内部引擎的异常
        if (err instanceof multer.MulterError) {
            return res.send({
                status: 1,
                msg: "文件上传出现异常,请稍后再试!"
            });
            // 判断是否是其他异常
        } else if (err) {
            return res.send({
                status: 1,
                msg: "服务器异常,请联系管理员!"
            });
        }
        // 一切都好
        return res.send({
            status: 0,
            msg: "文件上传成功!"
        });
    });
});
// 共享路由模块
module.exports = router;
  • 前台页面元素
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>单个文件上传</title>
    <!-- css引入 -->
    <link rel="stylesheet" href="./assets/layui-v2.5.7/css/layui.css">
</head>

<body style="padding: 20px; background-color: #f2f2f2;">
    <!-- 水平居中的容器-start -->
    <div class="layui-main">
        <!-- 面板-start -->
        <div class="layui-card">
            <div class="layui-card-header" style="background-color: #009688; color: #fff;">单个文件上传</div>
            <div class="layui-card-body">
                <!-- form表单-start -->
                <form class="layui-form" action="">
                    <!-- 文件表单域 -->
                    <div class="layui-form-item" style="width: 300px;">
                        <label class="layui-form-label">请选择照片</label>
                        <div class="layui-input-block">
                            <input style="padding-top: 5px;" name="photo" type="file" name="title" required lay-verify="required" placeholder="请输入标题" autocomplete="off" class="layui-input">
                        </div>
                    </div>
                    <!-- 按钮组 -->
                    <div class="layui-form-item">
                        <div class="layui-input-block">
                            <button class="layui-btn" lay-submit="" lay-filter="formDemo">立即提交</button>
                            <button type="reset" class="layui-btn layui-btn-primary">重置</button>
                        </div>
                    </div>
                </form>
                <!-- form表单-end -->
            </div>
            <!-- 面板-end -->
        </div>
        <!-- 水平居中的容器-end -->
    </div>
    <!-- js引入 -->
    <script src="./assets/jQuery-v3.5.1/jquery-3.5.1.min.js"></script>
    <script src="./assets/layui-v2.5.7/layui.all.js"></script>
    <script src="./assets/layui-v2.5.7/layui.js"></script>
    <script>
        $(function() {
            // 表单的提示事件
            $("form.layui-form").on("submit", function(e) {
                // 清除默认事件
                e.preventDefault();
                // 如果没有选择文件
                if (!$("input:file")[0]) {
                    return layer.open({
                        title: "提示",
                        icon: 5,
                        content: "请选择文件!",
                        time: 2000
                    })
                }
                // 创建formdata对象
                let formdata = new FormData();
                // 向formdata中添加属性
                formdata.append("photo", $("input:file")[0].files[0])
                $.ajax({
                    type: "POST",
                    url: "http://127.0.0.1:8024/api/upload",
                    data: formdata,
                    cache: false,
                    processData: false,
                    contentType: false,
                    success: function(res) {
                        // 状态码为0则上传文件失败
                        if (res.status !== 0) {
                            // 采用layui的弹出层
                            return layer.open({
                                title: "提示",
                                icon: 5,
                                content: "文件上传失败,请稍后再试!",
                                time: 2000
                            })
                        }
                        // 否则上传成功,使用layui弹出层提示用户
                        layer.open({
                            title: "提示",
                            icon: 6,
                            content: "文件上传成功!",
                            time: 2000
                        })
                    }
                })
            });
        });
    </script>
</body>

</html>

4、多个文件上传

  • 服务器路由接口
// 导入express模块
const express = require("express");
// 导入路径模块
const path = require("path");
// 创建路由对象
const router = express.Router();
// 导入multer模块
const multer = require("multer");
// 配置磁盘存储引擎
const storage = multer.diskStorage({
    // 设置文件上传的路径
    destination: function(req, file, cb) {
        // 使用path模块拼接路径
        cb(null, path.join(__dirname + "./../files/uploads"));
    },
    // 设置文件名称
    filename: function(req, file, cb) {
        // 获取当前的时间戳
        let name = new Date().getTime();
        // 以.分割原始文件名称,并返回两个数组
        let extend = file.originalname.split(".");
        // 通过for循环获取数组的最后一个元素,即原始文件的扩展名
        for (let i = extend.length - 1; i < extend.length; i++) {
            // 记录并替换扩展名
            extend = extend[i];
            // 跳出循环
            break;
        }
        // 拼接文件名称和扩展名,生成文件
        let filename = name + "." + extend;
        // 设置文件的名称
        cb(null, filename);
    }
});
// 使用磁盘存储引擎
const upload = multer({
    // 使用磁盘存储引擎
    storage: storage
        // 可接受多个指定名称的文件和数量
}).fields([{
    name: "photo-zs",
    maxCount: 1
}, {
    name: "photo-ls",
    maxCount: 1
}, {
    name: "photo-ww",
    maxCount: 1
}]);

// POST请求,并上传一个以 photo 命名的文件
router.post("/uploads", upload, (req, res) => {
    // 如果file对象为空,那么就证明没有选择照片,或者照片的数量不等于3
    if (!req.files || Object.keys(req.files).length !== 3) {
        // 响应状态码和提示信息
        return res.send({
            status: 1,
            msg: "照片未选择"
        });
    }
    // 捕获multer引起的异常
    upload(req, res, function(err) {
        // 判断是否是multer模块内部引擎的异常
        if (err instanceof multer.MulterError) {
            return res.send({
                status: 1,
                msg: "文件上传出现异常,请稍后再试!"
            });
            // 判断是否是其他异常
        } else if (err) {
            return res.send({
                status: 1,
                msg: "服务器异常,请联系管理员!"
            });
        }
        // 一切都好
        return res.send({
            status: 0,
            msg: "文件上传成功!"
        });
    });
});
// 共享路由模块
module.exports = router;
  • 前台页面元素
<!DOCTYPE html>
<html lang="en">

<head>
    <meta charset="UTF-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>单个文件上传</title>
    <!-- css引入 -->
    <link rel="stylesheet" href="./assets/layui-v2.5.7/css/layui.css">
</head>

<body style="padding: 20px; background-color: #f2f2f2;">
    <!-- 水平居中的容器-start -->
    <div class="layui-main">
        <!-- 面板-start -->
        <div class="layui-card">
            <div class="layui-card-header" style="background-color: #009688; color: #fff;">单个文件上传</div>
            <div class="layui-card-body">
                <!-- form表单-start -->
                <form class="layui-form" action="">
                    <!-- 文件表单域 -->
                    <div class="layui-form-item" style="width: 300px;">
                        <label class="layui-form-label">请选择照片</label>
                        <div class="layui-input-block">
                            <input style="padding-top: 5px;" multiple name="photo-zs" type="file" name="title" required lay-verify="required" placeholder="请选择文件" autocomplete="off" class="layui-input">
                        </div>
                    </div>
                    <!-- 按钮组 -->
                    <div class="layui-form-item">
                        <div class="layui-input-block">
                            <button class="layui-btn" lay-submit="" lay-filter="formDemo">立即提交</button>
                            <button type="reset" class="layui-btn layui-btn-primary">重置</button>
                        </div>
                    </div>
                </form>
                <!-- form表单-end -->
            </div>
            <!-- 面板-end -->
        </div>
        <!-- 水平居中的容器-end -->
    </div>
    <!-- js引入 -->
    <script src="./assets/jQuery-v3.5.1/jquery-3.5.1.min.js"></script>
    <script src="./assets/layui-v2.5.7/layui.all.js"></script>
    <script src="./assets/layui-v2.5.7/layui.js"></script>
    <script>
        $(function() {
            //表单的提示事件
            $("form.layui-form").on("submit", function(e) {
                // 清除默认事件
                e.preventDefault();
                // 获取到input中的所有文件
                let files = $("input:file")[0].files;
                // 判断是否选择了图片
                if (files.length <= 0) {
                    return layer.open({
                            title: "提示",
                            icon: 5,
                            content: "请选择文件!",
                            time: 2000
                        })
                        // 如果选择文件的个数不为3
                } else if (files.length !== 3) {
                    return layer.open({
                        title: "提示",
                        icon: 5,
                        content: "必须选择三个文件!",
                        time: 2000
                    })
                }
                // 定义formdata对象所需的三个key
                let filesName = ["photo-zs", "photo-ls", "photo-ww"];
                // 创建formdata对象
                let formdata = new FormData();
                // 通过循环遍历向formdata中添加文件和key
                for (let i = 0; i < files.length; i++) {
                    formdata.append(filesName[i], files[i]);
                }
                // 使用ajax请求服务器
                $.ajax({
                    type: "POST",
                    url: "http://127.0.0.1:8024/apis/uploads",
                    data: formdata,
                    cache: false,
                    processData: false,
                    contentType: false,
                    success: function(res) {
                        // 状态码为0则上传文件失败
                        if (res.status !== 0) {
                            // 采用layui的弹出层
                            return layer.open({
                                title: "提示",
                                icon: 5,
                                content: "文件上传失败,请稍后再试!",
                                time: 2000
                            })
                        }
                        // 否则上传成功,使用layui弹出层提示用户
                        layer.open({
                            title: "提示",
                            icon: 6,
                            content: "文件上传成功!",
                            time: 2000
                        })
                    }
                })
            });
        });
    </script>
</body>

</html>

四、总结

其实实现文件上传功能的方法特别多,比如 layui框架 就内置了文件上传的方法和学习文档,以及咱们已经学习过的 formidable 中间件 和 今天学习的 multer 中间件,它们都是实现文件上传的利器,都各有利弊,但又都是一样的原理,也必须通过FormData对象才能实现。所以不管什么方法,能实现我们的需求,并且思路清晰,那么就是好的中间件,像框架、插件都是这个道理。

所以在新的一年里,我希望正在学习编程或者打算学习编程的同学都能够专心、用心,不能糊里糊涂的学,因为我们学习的目标不是为了玩,而是为了就业。祝愿各位朋友在新的一年了能够涅槃重生,早日就业,拿到自己期望的工资,在这里也祝愿像博主一样今年打算找工作的朋友,在面试、机试的时候能够超长发挥,顺利进入自己喜欢的公司,感谢大家的支持!

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

御弟謌謌

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值