Multer + Express 实现文件上传

学习目标:

    • 一、上期回顾
    • 二、Multer 中间件
      • 1、简介
      • 2、安装
      • 3、使用
      • 4、API接口说明
        • 1、文件属性说明
        • 2、multer 方法说明
      • 5、storage(存储引擎)
        • 磁盘存储引擎(`DiskStorage`)
        • 内存存储引擎(`MemoryStorage`)
      • 6、fileFilter(文件过滤)
      • 7、错误处理机制
    • 三、文件上传
      • 1、案例目录展示
      • 2、服务器的搭建
      • 3、单个文件上传
      • 4、多个文件上传
    • 四、总结

一、上期回顾

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

  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 + Express 实现文件上传_第1张图片

二、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(注意)
fieldname Field name 由表单指定
originalname 用户计算机上的文件的名称
encoding 文件编码
mimetype 文件的 MIME 类型
size 文件大小(字节单位)
destination 保存路径 DiskStorage(磁盘存储引擎)
filename 保存在 destination 中的文件名 DiskStorage
path 已上传文件的完整路径 DiskStorage
buffer 一个存放了整个文件的 Buffer MemoryStorage(内存存储引擎)

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、案例目录展示

  • 服务器端目录

Multer + Express 实现文件上传_第2张图片

  • 前端目录

Multer + Express 实现文件上传_第3张图片

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;
  • 前台页面元素

<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>
    
    <link rel="stylesheet" href="./assets/layui-v2.5.7/css/layui.css">
head>

<body style="padding: 20px; background-color: #f2f2f2;">
    
    <div class="layui-main">
        
        <div class="layui-card">
            <div class="layui-card-header" style="background-color: #009688; color: #fff;">单个文件上传div>
            <div class="layui-card-body">
                
                <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>
                
            div>
            
        div>
        
    div>
    
    <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;
  • 前台页面元素

<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>
    
    <link rel="stylesheet" href="./assets/layui-v2.5.7/css/layui.css">
head>

<body style="padding: 20px; background-color: #f2f2f2;">
    
    <div class="layui-main">
        
        <div class="layui-card">
            <div class="layui-card-header" style="background-color: #009688; color: #fff;">单个文件上传div>
            <div class="layui-card-body">
                
                <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>
                
            div>
            
        div>
        
    div>
    
    <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对象才能实现。所以不管什么方法,能实现我们的需求,并且思路清晰,那么就是好的中间件,像框架、插件都是这个道理。

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

你可能感兴趣的:(Node.js,javascript,node.js,html5,css3)