Node.js<二十二>——项目实战-文件管理

281 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

头像上传

  1. router/file.router.js

头像上传其实也是文件管理的一部分,所以我们直接将其归类于文件上传即可;因为头像上传也是登录过后才能进行的操作,所以当用户上传头像之后经过的第一个中间件就是verifyAuth,其次是用于将用户上传的文件存到服务器中的avatarHandler中间件,光存到服务器中还不够,我们还需要利用saveAvatarInfo中间件将文件和用户之间的映射信息存储到数据表中才行,下次用户想要获取他上次上传的文件时我们才知道服务器中的哪个文件是他的

const Router = require('koa-router')

const { verifyAuth } = require('../middleware/auth.middleware')
const { avatarHandler } = require('../middleware/file.middleware')
const { saveAvatarInfo } = require('../controller/file.controller')

const fileRouter = new Router({ prefix: '/upload' })

fileRouter.post('/avatar', verifyAuth, avatarHandler, saveAvatarInfo)
// 通过用户id查看头像
userRouter.get('/:userId/avatar', getAvatarInfo)

module.exports = fileRouter
  1. middleware/file.middleware.js

有关文件读取的操作我们之前讲过一个好用的库——koa-multer,跟在express框架中使用multer第三方库一样,需要指定用户上传来的文件存储在哪个目录下,然后利用Multer创建出来的对象中的single方法来将上传的图片文件信息放到ctx.req.file

const Multer = require('koa-multer')

const { AVATAR_PATH } = require('../constants/file-path')

const avatarUpload = Multer({
  dest: AVATAR_PATH
})

const avatarHandler = avatarUpload.single('avatar')

module.exports = {
  avatarHandler
}
  1. controller/file.controller.js

经历过上一个中间件后,我们已经将用户上传的图片文件存放到了指定的文件中,但是我们想想,如果用户下次想获取到上次它上传的文件,我们应该怎么帮它找到呢?所以我们还需要在数据库中建一张文件与用户信息对应的一张表,用户每上传一个文件,我们就需要在表中添加一个对应的信息才行

const fs = require('fs')

const fileService = require("../service/file.service")
const userService = require("../service/user.service")
const {
  APP_HOST,
  APP_PORT
} = require('../app/config')
const { AVATAR_PATH } = require('../constants/file-path')

class FileController {
  async saveAvatarInfo(ctx, next) {
    const { id: userId } = ctx.user
    // 获取用户上传的文件信息
    const {
      filename,
      mimetype,
      size
    } = ctx.req.file
    // 去数据库中查询用户有没有上传过头像
    const results = await fileService.getAvatarByUserId(userId, filename, mimetype, size)
    // 如果没有,则直接将用户此次上传的文件信息添加到表中即可
    if (!results) {
      await fileService.create(userId, filename, mimetype, size)
      /**
      * 因为一个用户只对应一个头像,所以用户如果重新上传了头像,则需要将其之前上传的头像文件在服务器中删除
      * 但表中的数据其实可以不用删,我们只需要将上个头像的字段改成最近上传的那张头像的信息即可
      * 这样就只需要对数据表做一次操作,就不需要先删除再添加了
      */
    } else {
      // 利用fs对象上的unlink方法根据results对象删除用户上次存储的文件
      fs.unlink(`${AVATAR_PATH}/${results.filename}`, err => {
        if (err) console.log(err);
      })
      // 将用户上次存储在数据表中的文件信息更改为这一次的
      await fileService.update(filename, mimetype, size, userId)
    }
    // 去用户表中将最新的头像url更新上去
    await userService.updateAvatar(`${APP_HOST}:${APP_PORT}/users/${userId}/avatar`, userId)
    ctx.body = '上传头像成功~'
  }
}

// 将UserController这个实例返回出去,这样外部文件就可以通过该实例取出对应的函数了
module.exports = new FileController()
  1. service/file.service.js

该文件内部包含对数据库进行真正操作的函数,里面有在数据表中添加、更改文件信息等等的函数

const connection = require('../app/database')

class FileService {
  // 在数据表中添加文件信息
  async create(userId, filename, mimetype, size) {
    const statement = `INSERT INTO avatar (user_id, filename, mimetype, size) VALUES (?, ?, ?, ?);`
    const [results] = await connection.execute(statement, [userId, filename, mimetype, size])
    return results
  }
  
  // 更新数据表中的文件信息
  async update(filename, mimetype, size, userId) {
    const statement = `UPDATE avatar SET filename = ?, mimetype = ?, size = ? WHERE user_id = ?;`
    const [results] = await connection.execute(statement, [filename, mimetype, size, userId])
    return results
  }

  // 通过用户id获取到其上传的文件信息
  async getAvatarByUserId(userId) {
    const statement = `SELECT * FROM avatar WHERE user_id = ?;`
    const [results] = await connection.execute(statement, [userId])
    return results[0]
  }
}

module.exports = new FileService()
  1. service/user.service.js

当用户上传头像成功后,其实我们要做的事情还没有结束,我们还得让他们知道自己上传成功了,比如下次在请求用户信息的时候,就能够获取到已经被修改过的头像url,所以我们还有一个很关键的步骤没做,就是去用户表中将对应的头像url更改为用户新上传的

const connection = require('../app/database')

class UserService {
  // 用户上传头像成功之后更新用户表中对应用户的头像地址
  async updateAvatar(avatarUrl, userId) {
    try {
      const statement = `UPDATE user SET avatar_url = ? WHERE id = ?;`
      const [results] = await connection.execute(statement, [avatarUrl, userId])
      return results[0]
    } catch (err) {
      console.log(err);
    }
  }
}

module.exports = new UserService()
  1. controller/controller.js

我们目前为止确实已经实现了上传头像的功能,用户已经可以在上传头像成功之后查询到自己头像的url了,但是用户还不能够根据该url查看图片,因此,服务器端还需要在客户端通过头像url请求图片信息时时返回有关图片的数据流,这样图片才能真正的在客户端显示出来(注意:返回图片信息时一定要将content-type设置为对应的图片类型,负责浏览器会直接下载我们返回的文件而不是展示)

const fs = require('fs')

const fileService = require('../service/file.service')
const { AVATAR_PATH } = require('../constants/file-path')

class UserController {
  async getAvatarInfo(ctx, next) {
    const { userId } = ctx.params
    // 根据用户id去查询对应的头像信息
    const results = await fileService.getAvatarByUserId(userId)
    // 如果头像存在,说明用户已经上传过,直接返回即可
    if (results) {
      const { filename, mimetype } = results
      // 必须要将响应信息的content-type属性设置为图片类型,负责客户端不会显示图片,而是直接下载
      ctx.response.set('content-type', mimetype)
      // 以流的形式将图片信息返回给客户端
      ctx.body = fs.createReadStream(`${AVATAR_PATH}/${filename}`)
    // 如果头像不存在,则返回错误信息
    } else {
      ctx.body = '找不到对应的头像'
    }
  }
}

// 将UserController这个实例返回出去,这样外部文件就可以通过该实例取出对应的函数了
module.exports = new UserController()

文件上传

  1. router/file.router.js

我们现在除了上传头像之外,还想实现能给发表过的动态设置配图,而且一次可以配多张图,这个逻辑跟上传头像很相似,只不过现在文件信息是与动态信息绑定在一起的而已。但是在真正上传之前,我们依旧要检查用户的登录状态,然后再将用户上传的图片保存到服务器中,最后还需要将上传过来的文件信息与动态信息映射到数据表中

const Router = require('koa-router')

const { verifyAuth } = require('../middleware/auth.middleware')
const {
  avatarHandler,
  pictureHandler,
  pictureResize
} = require('../middleware/file.middleware')
const {
  saveAvatarInfo,
  savePictureInfo
} = require('../controller/file.controller')

const fileRouter = new Router({ prefix: '/upload' })

fileRouter.post('/avatar', verifyAuth, avatarHandler, saveAvatarInfo)
fileRouter.post('/picture', verifyAuth, pictureHandler, pictureResize, savePictureInfo)

module.exports = fileRouter
  1. middleware/file.middleware.js

对图片的处理我们还是需要用到koa-multer这个库,它可以帮助我们自动将用户上传的文件保存到服务器中并将文件信息放置到ctx.req.files中方便我们获取对应信息

从上一个代码中可以看到有一个名为pictureResize的中间件,这个中间件主要任务就是对用户上传的文件做一个尺寸重置,因为我们想让前端那边根据对应的场景去拉取合适大小的图片,比如在掘金的文章列表中,对应文章的封面图片我们可以拉取一个尺寸较小的的,在文章详情中再拉取一个比较大的,这样在一定程度上可以节省用户的流量以及提高请求速率

图片的裁剪我们可以借助jimp这个库,它比sharp更加轻量级,使用起来也非常的方便

const path = require('path')

const Multer = require('koa-multer')
const Jimp = require('jimp')

const {
  AVATAR_PATH,
  PICTURE_PATH
} = require('../constants/file-path')

const avatarUpload = Multer({
  dest: AVATAR_PATH
})

const storage = Multer.diskStorage({
  destination: (req, file, cb) => {
    cb(null, PICTURE_PATH)
  },
  filename: (req, file, cb) => {
    // 动态配图在服务器中存储的文件格式和头像有些不同,配图采用的是时间戳加后缀名的方式
    cb(null, Date.now() + path.extname(file.originalname))
  }
})

const pictureUpload = Multer({
  storage
})

const avatarHandler = avatarUpload.single('avatar')

const pictureHandler = pictureUpload.array('picture')
// 对图像进行处理
const pictureResize = async (ctx, next) => {
  // 对用户上传的每一个文件都拷贝大中小三分图片方便客户端选择合适的图片
  for (const file of ctx.req.files) {
    const { filename, destination, path: filePath } = file
    // Jimp对象上有一个read方法可以读取文件的信息,调整好对应的尺寸之后再写入到对应的文件夹中即可
    Jimp.read(filePath).then(image => {
      image.resize(1280, Jimp.AUTO).write(path.join(destination, `large-${filename}`))
      image.resize(640, Jimp.AUTO).write(path.join(destination, `middle-${filename}`))
      image.resize(320, Jimp.AUTO).write(path.join(destination, `small-${filename}`))
    })
  }
  await next()
}

module.exports = {
  avatarHandler,
  pictureHandler,
  pictureResize
}
  1. conroller/file.controller.js

现在我们已经可以将用户上传的多张动态配图保存到服务器中了,但是用户下次请求的时候我们并没有办法找到,所以还需要新建一张文件信息与动态信息映射的表,将用户上传的信息保存到这里面去

const fs = require('fs')

const fileService = require("../service/file.service")
const {
  APP_HOST,
  APP_PORT
} = require('../app/config')

class FileController {
  async savePictureInfo(ctx, next) {
    const { id: userId } = ctx.user
    const { dynamicId } = ctx.query
    // 获取用户上传的每个文件信息并保存到数据表中
    for (const file of ctx.req.files) {
      const {
        filename,
        mimetype,
        size
      } = file
      await fileService.createPicture(userId, filename, mimetype, size, dynamicId)
    }
    ctx.body = '上传图片成功~'
  }
}

module.exports = new FileController()
  1. service/file.service.js

这个文件中包含的是直接使用sql语句对数据库进行操作的函数,包括通过文件名在数据库中查找对应的文件信息以及在数据表中创建新的数据等等

const connection = require('../app/database')

class FileService {
  // 通过文件名称在数据表中寻找对应的配图信息
  async getPictureByName(filename) {
    const statement = `SELECT * FROM file WHERE filename = ?;`
    const [results] = await connection.execute(statement, [filename])
    return results[0]
  }

  // 在数据表中创建一条动态与文件的映射关系
  async createPicture(userId, filename, mimetype, size, dynamicId) {
    const statement = `INSERT INTO file (user_id, filename, mimetype, size, dynamic_id) VALUES (?, ?, ?, ?, ?);`
    const [results] = await connection.execute(statement, [userId, filename, mimetype, size, dynamicId])
    return results
  }
}

module.exports = new FileService()
  1. router.dynamic.router.js

上传动态配图是没问题了,我们现在希望的是用户可以通过动态配图的url在浏览器中看到这张图片。其实和处理头像的思路很像,只不过因为我们存储在服务端的文件名称发生了变化,所以在路由中注册中间件时也会有些区别,现在用户是通过文件名称来访问图片而不是用户id

const Router = require('koa-router')

const {
  fileInfo
} = require('../controller/dynamic.controller')

const dynamicRouter = new Router({ prefix: '/dynamics' })

dynamicRouter.get('/images/:filename', fileInfo)

module.exports = dynamicRouter
  1. controller/dynamic.controller.js

此前我们在存储用户上传的图片时,还对其做了一个重置大小操作,目的就是为了让前端在不同的场景可以请求不同大小的图片,减少资源的浪费,所以在用户现在想要获取图片资源的时候,我们可以通过query的方式让他们传入对应的尺寸信息,比如xxx?type=small,然后服务端解析之后为其返回对应尺寸的图片

const fs = require('fs')

const { PICTURE_PATH } = require('../constants/file-path')
const fileService = require('../service/file.service')

class DynamicController {
  async fileInfo(ctx, next) {
    const { filename } = ctx.params
    // 通过filename去数据库中先查找对应的数据
    const result = await fileService.getPictureByName(filename)
    // 如果对应的文件确实存在数据库中,那说明服务器中也有对应的文件,我们读取文件信息之后返回即可
    if (result) {
      /**
      * 我们想实现的效果是用户在正常图片的路径后面加一个query,比如xxx?type=small
      * 我们就知道前端想读取哪种大小的图片了
      */
      const { type } = ctx.query
      const types = ['small', 'middle', 'large']
      let { filename, mimetype } = result
      if (types.includes(type)) {
        filename = type + '-' + filename
      }
      ctx.response.set('content-type', mimetype)
      ctx.body = fs.createReadStream(`${PICTURE_PATH}/${filename}`)
    } else {
      ctx.body = '找不到对应的动态配图'
    }
  }
}

module.exports = new DynamicController()

至此,我们的文件管理工作已全部完成,大家可以根据自己的理解进行优化和扩展