文件上传
初始化准备
安装依赖
首先创建一个express-multer-upload工程项目,然后在项目中下好各种依赖包。
multer中间件
Multer 是一个 node.js 中间件,用于处理 multipart/form-data 类型的表单数据,它主要用于上传文件。
注意: Multer 不会处理任何非
multipart/form-data类型的表单数据。
下面是我下载的依赖以及版本。

项目结构划分
本着以比较规范的形式去完成这个项目,所以有必要进行合理的项目结构划分。如下:

multer上传逻辑
multer配置
在multer目录下创建 multerConfig.js 编写如下代码:
- 引入依赖
 - 封装处理路径函数
 - 设置 multer 的配置对象
 - 为 multer 添加配置
 
// 1. 引入依赖
const multer = require('multer')
const path = require('path') 
// 2. 封装处理路径函数
const handlePath = (dir) => {
  return path.join(__dirname, './', dir)
}
// 3. 设置 multer 的配置对象
const storage = multer.diskStorage({
  // 3.1 存储路径
  destination: function(req, file, cb) {
    if (file.mimetype === 'image/jpeg' || file.mimetype === 'image/png' || file.mimetype==='image/gif') {
      cb(null, handlePath('../../public'))
    } else {
      cb({ error: '仅支持 jpg/png/gif 格式的图片!' })
    }
  },
  //  3.2 存储名称
  filename: function (req, file, cb) {
    // 将图片名称分割伪数组,用于截取图片的后缀
    const fileFormat = file.originalname.split('.')
    // 自定义图片名称
    cb(null, Date.now() + '.' + fileFormat[fileFormat.length - 1])
  }
})
// 4. 为 multer 添加配置
const multerConfig = multer({
  storage: storage,
  limits: { fileSize: 2097152 } // 2M
})
module.exports = multerConfig
在该配置中可以设置文件保存的地址、文件名称、限制上传的文件格式、文件大小
upload上传逻辑
在 multer 目录下创建 upload.js,编写如下代码:
// 引入配置好的 multerConfig
const multerConfig = require('./multerConfig')
// 上传到服务器地址
const BaseURL = 'http://localhost:3001' 
// 上传到服务器的目录
const imgPath = '/public/'
// 封装上传图片的接口
function uploadAvatar(req, res) {
  return new Promise((resolve, reject) => {
    multerConfig.single('file')(req, res, function (err) {
      if (err) {
        // 传递的图片格式错误或者超出文件限制大小,就会reject出去
        reject(err)
      } else {
        // 拼接成完整的服务器静态资源图片路径
        resolve(BaseURL + imgPath + req.file.filename)
      }
    })
  })
}
module.exports = uploadAvatar
上述代码主要是封装了一个上传文件的方法,当图片上传成功时就将拼接好的图片链接 resolve 出去。该方法会在控制器中被调用。
注意:上面的
multerConfig.single('file')表示单文件上传,并且字段名为 “file”,后面上传图片的字段必须保持一致
编写控制器与定义路由
编写控制器
在 controllers 目录下创建 UserController.js,编写如下代码:
const uploadAvatar = require('../multer/upload')
// 用户的逻辑控制器
const UserController = {
  // 头像图片上传
  async upload(req, res) {
    try {
      const uploadRes = await uploadAvatar(req, res)
      res.send({
        meta: { code: 200, msg: '上传成功!' },
        data: { img_url: uploadRes}
      })
    } catch (error) {
      res.send(error)
    }
  }
}
module.exports = UserController
上述代码主要是编写了一个用户控制器类 UserController,以及一个图片上传的方法 upload。
在 upload 中调用了上传图片的接口 uploadAvatar,得到成功或失败的结果,再响应给客户端。
定义路由
- 
在 routers 目录下创建
index.js,编写如下代码:const express = require('express') // 导入用户逻辑 const userController = require('../controllers/UserController') // 创建路由对象 const router = express.Router() // 设置路由 router.post('/upload/avatar', userController.upload) // 导入路由对象 module.exports = router - 
定义了路由之后还需要在
app.js中注册路由,添加如下代码:// 导入定义的路由 const router = require('./src/routers/index') // 注册路由 app.use('/user', router)在
app.js中,增加上面两行代码即可完成路由注册 
上传图片
接下来进入测试环节,借助 postman 工具进行测试

可以看到,成功的拿到了响应的数据,里面也包含了图片的链接地址
注意:
- 表单必须是 form-data 格式
 - 文件的字段必须与后端保持一致
 
图片名称优化
由于这是一个用户上传头像图片的功能,当用户第二次上传头像时,需要将原先的图片删除掉,否则旧的图片会一直保存在服务器中。
一开始的想法是使用用户id作为图片名称,这样每一次上传图片,都会把原来的图片覆盖掉。但是这样会有两个问题
- 不同格式的图片会残留(jpg、png、gif),不会被覆盖掉
 - 如果可以覆盖,但是图片链接地址不会有变化,存入数据库时也是跟上一次的图片地址是相同的,这样会导致前端页面不会根据静态资源中头像图片变化而变化
 
所以这里采用的做法是先对图片的名称进行拼接优化,改为如下的形式:
时间戳.用户id.jpg
这样既能保证每一张图片都不重复,而且还附带了用户的id
注:可以使用 md5 对时间戳进行加密,以确保唯一性。这里为了方便就直接使用时间戳。
图片名称优化实现
这个过程其实就是删除旧图片,重命名新图片为规定的格式,可以编写一个函数来实现。
图片去重删除和重命名
- 查找指定路径下的所有图片文件,进行遍历
 - 先查询该id命名的文件是否存在,存在则删除
 - 根据新存入的文件名(时间戳.jpg),找到对应文件,然后重命名为: 
时间戳.id.jpg 
在upload.js,编写如下代码:
const fs = require('fs')
// 对图片进行去重删除和重命名
const hanldeImgDelAndRename = (id, filename, dirPath) => {
  // TODO 查找该路径下的所有图片文件
  fs.readdir(dirPath, (err, files) => {
    for (let i in files) {
      // 当前图片的名称
      const currentImgName = path.basename(files[i])
      // 图片的名称数组:[时间戳, id, 后缀]
      const imgNameArr = currentImgName.split('.')
      // TODO 先查询该id命名的文件是否存在,有则删除
      if (imgNameArr[1] === id) {
        const currentImgPath = dirPath + '/' + currentImgName
        fs.unlink(currentImgPath, (err) => { })
      }
      // TODO 根据新存入的文件名(时间戳.jpg),找到对应文件,然后重命名为: 时间戳.id.jpg
      if (currentImgName === filename) {
        const old_path = dirPath + '/' + currentImgName
        const new_path = dirPath + '/' + imgNameArr[0] + '.' + id  + path.extname(files[i])
        // 重命名该文件
        fs.rename(old_path, new_path, (err) => { })
      }
    }
  })
}
函数执行过程分析:
- 该函数主要调用了 
fs内置模块中的readdir进行指定路径查询文件,再进行遍历 - 将图片名称分割为数组,取出id与传入的id进行判断,符合条件则调用 
fs内置模块中的fs.unlink()方法删除文件 - 根据新存入的文件名(时间戳.jpg),找到对应文件,然后重命名为: 
时间戳.id.jpg。然后调用fs内置模块中的fs.rename()方法重命名文件 
修改 uploadAvatar 接口
完成图片去重删除和重命名 hanldeImgDelAndRename 方法后,还需要在 upload.js 中原先的上传接口方法 uploadAvatar 中进行调用,修改为如下代码:
const path = require('path')
// 封装处理路径函数
const handlePath = (dir) => {
  return path.join(__dirname, './', dir)
}
// 上传接口的 请求参数req  响应参数res
function uploadAvatar(req, res) {
  return new Promise((resolve, reject) => {
    multerConfig.single('file')(req, res, function (err) {
      if (err) {
        reject(err)
      } else {
        // 对图片进行去重删除和重命名
        hanldeImgDelAndRename(req.body.id, req.file.filename, handlePath('../../public'))
        const img = req.file.filename.split('.')
        resolve({
          id: req.body.id,
          // 重新返回符合规定的图片链接地址
          img_url: BaseURL + imgPath + img[0] + '.' + req.body.id + '.' + img[1]
        })
      }
    })
  })
}
注意:在上传文件时,必须携带 id 字段,这样
req.body.id才能获取到传入的 id。
最终测试
第一次上传

可以看到,图片上传成功,并且图片的名称也是按照我们的规定进行拼接,后端服务器也成功保存了上传的图片。

第二次上传

第二次上传,成功的将相同 id 的旧图片进行了删除,并且重命名了图片名称。
第三次上传

可以看到,不同 id 之间上传的图片是互不干扰的,只有当 id 匹配时才会进行替换和重命名。
最后只需要在控制器当中,把获取到的图片链接地址保存到数据库即可,这里可以根据用户 id 进行保存。
Ajax上传
<div class="ajax">
  <p>ajax上传</p>
  <form>
    <input type="text" name="username" />
    <input type="password" name="password" />
    <input type="file" name="avatar" />
    <button type="button">上传</button>
  </form>
  <img />
</div>
<script>
  let btn = document.querySelector('.ajax [type=button]');
  var username = document.querySelector('.ajax [name=username]');
  var password = document.querySelector('.ajax [name=password]');
  var avatar = document.querySelector('.ajax [name=avatar');
  avatar.addEventListener('change', () => {
    // 创建预览地址
    let httpUrl = window.webkitURL.createObjectURL(new Blob(avatar.files));
    document.querySelector('img').src = httpUrl;
  });
  btn.addEventListener('click', () => {
    // 要处理成表单对象上传
    const formsdata = new FormData();
    formsdata.append('username', username.value);
    formsdata.append('password', password.value);
    // 追加name值,和文件对象
    formsdata.append('avatar', avatar.files[0]);
    axios
      .post('/user/upload/avatar', formsdata, {
        headers: {
          'Content-Type': 'multipart/form-data',
        },
    })
      .then(res => {
      document.querySelector('img').src = res.data.imgPath;
    });
  });
</script>