Express+multer 实现文件上传,并在 router 中指定文件存放路径

内容简单说明

  文件上传是 web 开发中比较常见的一个功能虽然说起来是文件上传,实际上,可以看做是对 multipart/form-data 数据的处理。在 npm 中,有很多处理类似数据的库,包括周下载量近 2kw 的 form-data,周下载量近 3mw 的 formidable。

  不过,如果 nodejs 后端使用的 express 框架,其官方也有一个自己的文件上传中间件,用它自己的话来说就是:“Multer 是一个 node.js 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。”

  使用 multer 比较简单,一般就是
    1、导入 multer,
    2、指定文件上传地址(如果有必要的话,不指定只是写到内存中),
    3、在 router 的路径后,回调函数前,写一个upload.single(photo)(单文件)或者upload.array('photos', 12)(多文件),在 router 的回调中,就可以使用req.file 或者 req.files获取文件了。

  在这里,因为指定的上传地址是在 multer(opts)中的 opts 配置,所以 opts 配置号一个地址之后,后续修改就不是那么方便。如果需要对不同文件不同路由路径指定不同的文件上传地址,那应该如何处理?

  multer 的简单使用后文会给个示例,但是最终的目的,是想要在 express 的 router 回调函数中,可以指定文件上传的路径,而不是所有的文件都上传到唯一指定的路径。例如,路由是“testUpload”,我在 router 处理时指定存放到测试使用的上传路径。路由是“formalUpload”,我在处理时可以指定存放到正式的上传路径。

express+multer 基本文件上传示例

  因为主要是测试 multer 内容,所以一切从简,就在一个简单的 express 项目中测试就好

1、创建一个 express 项目(前提:已安装 express-generator),并安装 multer

express --view=ejs express-mutler-demo
// 进入项目根目录
npm i multer

2、上传页面编写

  修改 views/index.ejs 的<body>标签内容如下:

  <div>
    <h3>Express + multer 簡陋上傳文件</h3>
    <form method="post" action="/upload" id="upload-form" encType="multipart/form-data">
      <input id='upload' type="file" name="file" />
      <input type="submit" value="上傳">
    </form>

    <!-- 进度条 -->
    <progress id="uploadprogress" min="0" max="100" value="0">0</progress>
    <p id='msg'></p>
  </div>

  <!-- 引入jquery.js -->
  <script src="https://code.jquery.com/jquery-3.4.1.min.js"
    integrity="sha256-CSXorXvZcTkaix6Yvo6HppcZGetbYMGWSFlBw8HfCJo=" crossorigin="anonymous"></script>

  <script>
    let form = $("#upload-form");
    form.on('submit', function (event) {

      // 清除提交结果显示信息
      $("#msg").html("");

      // 在原页面处理,不跳转
      event.preventDefault();

      // 检查是否支持FormData
      if (window.FormData) {
        let formData = new FormData();

        // 建立一个file表单项,值为上传的文件
        formData.append('file', $('#upload').get(0).files[0]);
        let xhr = new XMLHttpRequest();
        xhr.open('POST', $(this).attr('action'));

        // 进度条占比计算
        xhr.upload.onprogress = function (event) {
          if (event.lengthComputable) {
            let complete = (event.loaded / event.total * 100 | 0);
            $("#uploadprogress").val(complete);
            $("#uploadprogress").innerHTML = complete;
          }
        };
        // 定义上传完成后的回调函数
        xhr.onload = function (e) {
          if (xhr.status === 200) {
            $("#msg").html("上传成功!");
            // alert('上传成功!');
          } else {
            // alert('文件上传出错了!')
            $("#msg").html("上传失败!");
          }
        };
        // 发送表单数据
        xhr.send(formData);
      }
    });
  </script>

  代码内容很简单,就是一个 form 用来模拟文件上传,为了最简单,直接使用的 XMLHttpRequest 实现上传,还没事整了个进度条。
  本来想用原始的方法,还是引入了 jquery。更简略类似下面也 ok。

<script>
    function PostData() {
        $.ajax({
            type: "POST",
            url: "XXX",
            data : "",
            success: function(msg) {
            }
        });
        return false;
    }
</script>
<form onsubmit="return PostData()">
    <input type="text" value="">
    <input type="submit">
</form>

  依旧以第一个为准,页面大概是这个样子(运行 express 项目,在 localhost:3000 看到):

input画面.png

3、multer 的简单配置

  新建一个 util/Upload.js,编写 multer 配置并导出:

const multer = require('multer');

// 文件上传配置
const fileStorage = multer.diskStorage({
    destination: function (req, file, callback) {
        callback(null, "/defaultUploadDir");
    },
    filename: function (req, file, callback) {
        callback(null, file.originalname);
    }
});
// 导出配置
module.exports = {
    fileUpdate: multer({ 'storage': fileStorage }),
}

注意:上传地址 "/defaultUploadDir"要先手动创建,否则报错。

4、在对应 router 中使用 multer

  在 routes/index.js 中,添加以下 router 代码:

router.post('/upload', upload.fileUpdate.single('file'), function (req, res, next) {

  const file = req.file;
  console.log(file);

  //如果得到了文件,就返回上传成功
  if (file) {
    return res.status(200).json({ success: true });
  } else {
    return res.status(500).json({ success: false });
  }
});

  记得在最上面引入 multer 配置:

const upload = require('../util/Upload');

  几个简单注意点:
    1、这个路由路径和路由方法,要和前台页面中的 action 和 method 一致;
    2、多文件就要 upload.array(),单文件就用 upload.single()(后续都是单文件示例中说明);
    3、第二点()里面的标志字符串要和前台页面中的<input id='upload' type="file" name="file" />name 属性一致。

  如果步骤都正确,成功上传,应该可以看到前台页面如下:

上传成功.png

  router 的回调中取得上传文件的信息,如下:


上传成功router取得文件信息.png

  文件上传的位置:

第一次上传成功后台地址.png

关于使用 multer 文本域数据

  multer 的 readme 所说:”Multer 会添加一个 body 对象 以及 file 或 files 对象 到 express 的 request 对象中。 body 对象包含表单的文本域信息,file 或 files 对象包含对象表单上传的文件信息。“
  实际测试,在前台页面 index.ejs 创建 formData 后,append 一个文本数据:

let formData = new FormData();

// 补入此句
formData.append('dest', 'file_upload');

  刷新页面之后,重新上传,可以在 multer 配置中,在 diskStorage 的 destination 的 callback 中,可以得到 req.body 包含了 dest 属性。如下图:

req.body获取文本域数据.png

  这是好事,很好的,这样,在前台上传文件时,就可以把需要上传的地址放到这里,那么不同的文件上传就可以存放的不同的地址了。
  那么会有哪些问题呢?
    1、前端需要知道后台的上传路径,不合理。
    2、并不是所有使用 formData.append()添加的属性都能在文件上传 destination 生成前,在 req.body 中获取到。

    这是一个实际遇到的问题,我在使用 angular 时,使用 HttpClient 实现文件上传操作,类似:

  upload(file: any) {
    // 文件使用FormData发送
    const formData: FormData = new FormData();
    formData.append('file', file, file.name);
    formData.append('file_name', file, file.name);
    return this.http.post(this.URL + '/upload', formData );
  }

  后台的 req.body 在获取到上传的文件前并不会有 file_name 属性的值,即在 multer 配置在 diskStorage 的 destination 的 callback 中,可以得到 req.body 是空,在对应 upload 的 router 回调中,才取得 req.body 的 file_name 属性。

在 router 的回调中,指定文件上传的路径。

  在”关于使用 multer 文本域数据“这部分有讲到,前台直接传入文件上传的路径不合理,在接受到上传的文件前得到指定的上传路径也不一定成功,而直接使用配置好的 multer,其文件上传目的地 destination 又只有固定一个。该如何实现?

  把 multer 的配置,封装到一个返回 promise 的函数,指定传入一个文件路径参数,并在 router 的回调中使用该函数,传入上传路径。

  修改 utils/Upload.js 文件,补入以下内容:

// multer文件上传,可指定上传路径,不在router参数里直接用
let uploadFunction = (req, res, dest) => {

    let storage = multer.diskStorage({
        destination: function (req, file, cb) {
            let newDestination = dest;
            let stat = null;
            try {
                // 检查传入的路径是否存在,不存在则创件
                stat = fs.statSync(newDestination);
            } catch (err) {
                fs.mkdirSync(newDestination);
            }
            if (stat && !stat.isDirectory()) {
                throw new Error('文件目录: "' + dest + '已存在!"');
            }
            cb(null, newDestination);
        },
        filename: function (req, file, callback) {
            callback(null, file.originalname);
        }
    });

    let upload = multer({
        storage: storage
    }).single('file');

    return new Promise((resolve, reject) => {
        upload(req, res, (err) => {
            if (err) {
                return reject(err);
            }
            resolve();
        })
    })
};

  记得导出:

module.exports = {
    fileUpdate: multer({ 'storage': fileStorage }),
    uploadFunction,
}

  在 router 中使用,修改原 routes/index.js 的 upload 路由如下:

router.post('/upload', /*upload.fileUpdate.single('file'), */ async function (req, res, next) {

  // 指定文件上传路径
  let uploadPath = 'test_upload';
  // 等到文件上传完成
  await upload.uploadFunction(req, res, uploadPath);

  const file = req.file;
  console.log(req.file);
  //如果得到了文件,就返回上传成功
  if (file) {
    return res.status(200).json({ success: true });
  } else {
    return res.status(500).json({ success: false });
  }
});

  当然,await 需要在 async 函数中使用,也最好放到 trycatch 中。

  如果步骤正确,结果应该和第一步中的一样,文件上传成功。在后台的项目中会新建一个 test_upload 文件夹,并有上传的文件。

第二次文件上传成功位置.png

  代码已放到 github,有需求可查阅。

  以上内容,全部亲测有效,如果有问题,请提出交流,谢谢。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,132评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,802评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,566评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,858评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,867评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,695评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,064评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,705评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,915评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,677评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,796评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,432评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,041评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,992评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,223评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,185评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,535评论 2 343

推荐阅读更多精彩内容