基于node formidable的图片上传和转发

最近在研究node后端的图片数据的上传、保存和转发,可以实现直接将图片保存在服务器本地(而非数据库)

这里我们需要在前端上传一个图片,之后在服务器的某个文件夹保存这个图片,并重命名

之后前端发送特定的服务,我们可以在文件夹中找到对应的图片并返回给前端

一、服务构建

在本地8000端口起一个前端服务(在本例中用的umi框架)

并在本地3333接口起一个后端服务(在本例中使用node,详情见第三节)

二、前端上传实现

前端随便使用上传组件或者手动写axios请求就可以,只要能实现把一张图片流发送到指定服务就行(一定要保证是通过二进制传输)。

这里用umi框架,用antd的upload上传组件举例

const beforeUpload = (file) => {
    const isJpgOrPng = file.type === 'image/jpeg' || file.type === 'image/png';
    if (!isJpgOrPng) {
      message.error('You can only upload JPG/PNG file!');
    }
    return isJpgOrPng;
  }

  const getBase64 = (img, callback) => {
    const reader = new FileReader();
    reader.addEventListener('load', () => callback(reader.result));
    reader.readAsDataURL(img);
  }

  const handleChange = (info) => {
    if (info.file.status === 'uploading') {
      setLoading(true)
      return;
    }
    if (info.file.status === 'done') {
      // Get this url from response in real world.
      getBase64(info.file.originFileObj, imageUrl => {
        setLoading(false)
        setImageUrl(imageUrl)
      });
    }
  };

  const uploadButton = (
    <div>
      {loading ? <LoadingOutlined /> : <PlusOutlined />}
      <div style={{ marginTop: 8 }}>Upload</div>
    </div>
  );

  return (
          <div style={{ display: 'flex', justifyContent: 'space-between' }}>
              accept=".jpg,.jpeg,.png,.JPG,.JPEG,.PNG"
              name="avatar"
              method='post'
              listType="picture-card"
              className="avatar-uploader"
              showUploadList={false}
              action={`//localhost:3333/backend/image`}
              beforeUpload={beforeUpload}
              onChange={handleChange}
            >
              {imageUrl ? <img src={imageUrl} alt="avatar" style={{ width: '100%' }} /> : uploadButton}
            </Upload>
          </div>
  );
}

需要注意的是这里设置的上传地址,会涉及到跨域问题(从localhost:8000跨域到localhost:3333)那么需要配置一下跨域的代理

三、后端实现

现在来写后端入口文件app.js,首先搭建基础的node服务端监听3333端口

var express = require('express');
var fs = require('fs');
var http = require('http');
const router = require('./router/index')  //  引入路由
var app = express();

//设置跨域访问                                    
app.all('*', (req, res, next) => {
  res.header("Access-Control-Allow-Origin", "*");
  res.header("Access-Control-Allow-Headers", "X-Requested-With");
  res.header("Access-Control-Allow-Methods", "PUT,POST,GET,DELETE,OPTIONS");
  res.header("X-Powered-By", ' 3.2.1');
  res.header("Content-Type", "application/json;charset=utf-8");
  next();
});

//  使用路由 /index 是路由指向名称
app.use(`/backend`, router)

var httpsServer = http.createServer(app);
httpsServer.listen(3333, function () {
  console.log('正在监听3333接口')
});

这段代码有三部分需要解释一下:

(1)

这一段是设置跨域,对于所有的服务,都会在前面加上额外的请求头,设置Access-Control-Allow-Origin为*即所有域都可以访问

 (2)

这一段是我们的后端服务,当需要访问backend这个应用下的某个服务的时候,node会在router里面找(router在代码第四行已经定义,在另一个文件夹,这样是为了把后端backend这个应用给区分开来)

 (3)

最近监听当前所在域的3333端口,在本地跑就是localhost:3333

之后我们在当前文件夹下面新建一个文件夹名叫router,并在下面新增一个index.js和另一个文件夹files(files原来存取前端上传上来的图片)

router                                                  后端服务文件夹

├─files                                                存放我们图片的文件夹

├─index.js                                          后端服务

utils                                                     一些全局方法(不用管)

app.js                                                  后端入口文件

(app.js即我们刚刚的入口文件,router即我们的后端backend服务)

再打开router文件夹,在其中的index.js中写我们的后端服务

const express = require(`express`)
const router = express.Router()

router.post("/image", function (req, res) {
  res.json({
    success: true
  })
})


module.exports = router

当我们发送post服务 http://localhost:3333/backend/image 之后,会返回给我们一个success:true

试了一下,服务是跑通了,但是我们怎么去获取到这个图片?怎么去保存下来呢?服务是通过二进制流传输的图片,我们怎么去解析这个二进制并保存下来呢?

这就需要请出我们本文的主角了formidable

formidable简而言之是一个可以在node使用的表单处理器,而且他支持直接将二进制转换成实际的文件
(使用前,需要执行npm install formidable,并且在index.js头部增加这一行

var formidable = require('formidable');

之后,我们创建一个formidable的表单解析对象,并对这个对象的储存路径进行变更

const express = require(`express`)
const router = express.Router()

router.post("/image", function (req, res) {
  const form = new formidable.IncomingForm();//创建解析对象
  form.uploadDir = __dirname + "/files/";// 指定解析对象(图片)存放的目录
  form.keepExtensions = true//保留后缀名

  form.parse(req, function (err, fileds, files) {
    res.json({
      success: true
    })
  })
})


module.exports = router

这里的form.parse就是对表单数据进行处理,但是这里只有一个表单项(就是我们刚刚上传的图片)form.parse中的files就是我们的图片数据了

这样的话,文件会保存在当前目录下的files文件夹中,并保留后缀

我们来实操一下,首先上传一张菈妮的图片,可以看到服务是ok的,返回了一个success:true

但是我们查看文件夹,会发现是这样的

既没有名字,也没有后缀名(如果现在手动在文件后面加.jpg发现是可以的)

这是因为文件是二进制传输,没有告诉后端这个文件的名字和扩展名,所以才有这样的问题,但文件本身是没有问题的,只是命名的问题

那么我们需要给文件重命名,并且在命名后面加上对应的文件扩展名

 我们首先看一下form的file信息

 我们可以发现在form.parse后files中的avatar有文件的所有属性(包括本身的名字等等,和文件的扩展名)

那么我们可以通过node中的文件读写fs来进行一次文件重命名(可以扩展阅读一下fs的rename函数)

    //通过fs更改文件名
    fs.rename(files.avatar.filepath, generateFilename(oldFilename, files.avatar.originalFilename), err => {
      if (err) {
        console.log("重命名失败");
        console.log(err);
      } else {
        console.log(`已经保存为${generateFilename(oldFilename, files.avatar.originalFilename, files.avatar.filepath)}`);
      }
    })

 这里第一个参数是文件的位置,第二个参数是通过我们自定义的一个函数generateFilename来将文件重命名,第三个是rename之后的回调

对于generateFilename,需要注意的是从命名后需要在前面加上路径,否则fs会自动将文件重新移回当前文件夹,而不是我们想要的files文件夹

  const generateFilename = (oldFilename, originalFilename, path) => {
    let names = originalFilename.split(".");
    path = path.replace('invalid-name', '')
    return `${path}${names[0]}_${id}.${names[1]}`;
  }

 再试一次,好了,而且也加上扩展名了

四、通过url参数详细命名我们的文件

但是这样肯定是不完美的,首先是我们的这些图片没有用户信息,这样很难分辨是谁上传的图片...而且上传的图片只是有一个图片名,我们想要图片名按某种格式(用户id-用户name-日期)这样的命名,应该怎么做呢?

(1)解决方法一:在前端创建一个表单

因为formidable本身就是一个表单处理器,我们可以创建多个表单之后一起从前端通过一个服务传过来,让用户在表单中填写id信息等等,这也是一个大多数的解决方案(特别是不需要用户注册的问卷类型的页面)

之后,在后端通过form.parse处理表单项,form.parse中有三个参数err、fileds、files

 对于普通的表单项,我们就读fileds就可以了,对于文件格式的表单项(比如我们刚刚的files)也可以单独处理

这个解决方法相信大家都轻车熟路了,表单谁不会写啊,举个手让大家笑话一下

(2)解决方法二:通过url传递

这里是通过在服务url中包含用户的信息,之后在后端解析出来,并重命名,这个适合只传一张图,而不想麻烦用户传一个表单的情况,服务还是传递的图片的二进制流,其他信息就包含在url中,本文以这个为例

首先在前端将服务的发送url改一下

<Upload
              accept=".jpg,.jpeg,.png,.JPG,.JPEG,.PNG"
              name="avatar"
              method='post'
              listType="picture-card"
              className="avatar-uploader"
              showUploadList={false}
              action={`//localhost:3333/backend/image?id=${userData.id}&name=${userData.name}`}
              beforeUpload={beforeUpload}
              onChange={handleChange}
            >
              {imageUrl ? <img src={imageUrl} alt="avatar" style={{ width: '100%' }} /> : uploadButton}
            </Upload>

(userData即用户信息) 

之后我们在刚刚的服务中获取到这几个信息

router.post("/image", function (req, res) {
  console.log(req.url)
  let id = geturlparam(req.url, 'id')
  let name = geturlparam(req.url, 'name')
...
});

 geturlparam是一个全局方法

//获取url参数
const geturlparam = (url, name) => {
  let p = url.split('?')[1]
  let keyValue = p.split('&');
  let obj = {}
  for (let i = 0; i < keyValue.length; i++) {
    let item = keyValue[i].split('=');
    let key = item[0];
    let value = item[1];
    obj[key] = value;
  }
  return decodeURIComponent(obj[name])
}

module.exports = geturlparam

 (这里的decodeURIComponent(obj[name])是一个解码函数,我们其实可以发现发送的服务编码不是utf-8,这是因为编码格式只生效于我们的页面body,对于发送出去的请求我们无法编码,那么url参数想要传递中文的话,需要在后端这边重新解码一下)

之后,我们在更改的rename函数中,将generateFilename函数加入id和name这两个参数

    fs.rename(files.avatar.filepath, generateFilename(oldFilename, files.avatar.originalFilename, files.avatar.filepath, id, name), err => {
      if (err) {
        console.log("重命名失败");
        console.log(err);
      } else {
        console.log(`已经保存为${generateFilename(oldFilename, files.avatar.originalFilename, files.avatar.filepath, id, name)}`);
      }
    })
  const generateFilename = (oldFilename, originalFilename, path, id, name) => {
    let d = new Date();
    let names = originalFilename.split(".");
    path = path.replace('invalid-name', '')
    return `${path}${name}_${id}_${"" + d.getFullYear() + (d.getMonth() + 1) + d.getDate() + '_' + d.getHours() + d.getMinutes() + d.getSeconds()}.${names[1]}`;
  }

 最后运行一小,效果如下

 第四张图的文件名中就包含了用户信息

五、前端获取到我们服务器上的图片

假如我想写一个get服务,获取某一个用户上传的文件怎么办?其实很简单,这里直接讲思路

(1)文件名已知

那么直接访问域下面的文件就可以了,后端读取参数,然后返回对应的url就可以了(但是需要提前将对应的文件集合路径挂载到服务上,不然访问不到)

(2)文件名未知

假如我只知道一个用户的名字,我想拿到对应的所有包含这个名字的所有图片该怎么做呢?方法也很简单:

1.前端发送一个get请求,请求中包含对应的参数

2.后端解析参数,通过fs.readdir方法获取某个文件夹的所有文件,这个方法会返回一个数组,数组中全是文件夹中的所有文件名字

3.遍历并找到要输出的文件

4.后端返回对应的url数组(这也需要提前将对应的文件集合路径挂载到服务上)

总结:

一般而言,还是把图片的base64码通过数据库持久化保存好一点,数据库不光可以保存更多数据,而且数据库可以自定义其他字段来保存数据。

但是有的时候,我们不想写复杂的sql,或者我们想快速拿到上传的文件,这个方法显然好的多:

这个解决方案适合轻量级的网站,而且是能直接用户上传图片就能直接拿到图片文件的,相对于数据库来说也简单快捷,甚至于可以直接对excel表格进行读写。

我们是通过formidable来处理文件二进制流,formidable是一个表单处理器,可以处理表单数据或者表单文件,并保存在特定的路径。

之后通过fs的rename方法重命名文件,或者直接扩展前端表单来获取其他数据。

需要注意的是,如果一个表单既包含文件又包含数据,需要在form的属性中配置enctype=“multipart/form-data”,因为表单数据即包含普通数据,又包含文件二进制流

  • 3
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值