koa-body完美替换koa-bodyparser

2,183 阅读4分钟

前言

在node项目中,一开始使用了koa-bodyparser中间件用来处理post请求,但是后来发现还需要上传文件,而koa-bodyparser中间件不能够用于处理上传文件,需要使用koa-body,但是这两个中间件并不能同时使用,同时使用会报错,所以需要将koa-bodyparser换成koa-body

这两者的简单区别

  • koa-bodyparser:只能处理post请求的数据,但是不能够处理文件类型的上传

  • koa-body:不仅能处理post请求的数据,还能处理文件类型的上传

使用koa-bodyparser结合koa-multer进行图片上传

  • 安装模块
npm install koa-bodyparser
npm install koa-multer
  • koa2项目中,index.js文件下
const Koa = require('koa');
// 导入bodyParser中间件,解析json
const bodyParser = require('koa-bodyparser');
// 导入错误信息处置方法
const errorHandler = require('./error-handle');

const swagger = require('../utils/swagger'); // 存放swagger.js的位置
const { koaSwagger } = require('koa2-swagger-ui');

const koaStatic = require('koa-static');

const cors = require('koa2-cors');
const path = require('path');

// 导入路由 动态加载路由
const useRoutes = require('../router');

const app = new Koa();

const logger = require('../utils/logs');

// 遍历存储图片的路径
const FindPath = require('../utils/get-file-path');
FindPath.fileDisplay('./file/picture', (arr) => {
  console.log(arr, 'path');
});

app.use(async (ctx, next) => {
  const start = new Date();
  await next();
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  logger.resLogger(ctx, ms);
});

// error-handling
app.on('error', (err, ctx) => {
  logger.errorLogger(ctx, err);
  console.error('server error', err);
});

app.use(bodyParser());

app.use(koaStatic('../../file'));
app.use(cors());
// 接口文档配置
app.use(swagger.routes(), swagger.allowedMethods());
app.use(
  koaSwagger({
    routePrefix: '/swagger', // 接口文档访问地址
    swaggerOptions: {
      url: '/swagger.json' // example path to json 其实就是之后swagger-jsdoc生成的文档地址
    }
  })
);

useRoutes(app);

app.on('error', errorHandler);

module.exports = app;
  • 在文件上传路由中:file.router.js,具有图片上传以及查看图片的API
const Router = require('koa-router');

// 具体的处理逻辑
const {
  pictureHandler
} = require('../middleware/file.middleware');

const {
  savePictureInfo,
  getPictureInfo,
} = require('../controller/file.controller');

const fileRouter = new Router({
  prefix: '/api/file'
});

fileRouter.post('/picture',pictureHandler, savePictureInfo);

fileRouter.get('/picture/:filename', getPictureInfo);

module.exports = fileRouter;
  • 在中间件中使用koa-multer,在这个中间件中定义了图片上传后的存储地址以及使用时间戳对图片进行重新命名:file.middleware.js
const Multer = require('koa-multer');

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

// 存储图片
const storagePicture = Multer.diskStorage({
  destination: function (req, file, cb) {
    // 上传文件存储地址
    cb(null, PICTURE_PATH);
  },
  filename: function (req, file, cb) {
    // 自定义上传的文件名字
    const singFileArray = file.originalname.split('.');
    const fileExtension = singFileArray[singFileArray.length - 1]; // 文件后缀名
    const newFilename = Date.now() + '.' + fileExtension;
    cb(null, newFilename);
  }
});

const pictureUpload = Multer({ storage: storagePicture });

const pictureHandler = pictureUpload.single('picture');

module.exports = {
  pictureHandler
};

  • file.controller.js
// 读取文件信息
const fs = require('fs');
const fileService = require('../service/file.service');

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

const config = require('../app/config');

class FileController {
  async savePictureInfo(ctx, next) {
    // 1.获取图像相关信息
    const { filename, mimetype, size, path } = ctx.request.files.picture;

    const url = `${config.HOST}:${APP_PORT}/api${path}`;
    // const url = `${ctx.origin}/api${path}`;

    // 2.将图像信息保存到数据库
    await fileService.createPicture(filename, mimetype, path, size, url);

    // 4.返回数据
    ctx.body = {
      statusCode: 200,
      imageUrl: url,
      message: '上传图片成功!'
    };
  }

  async getPictureInfo(ctx, next) {
    const { filename } = ctx.params;

    const pictureInfo = await fileService.getPictureByFilename(filename);

    // 2.提供图像信息
    ctx.response.set('content-type', pictureInfo.mimetype); // 需要查看图片就这样设置,如要下载可注释

    ctx.body = fs.createReadStream(`${PICTURE_PATH}/${pictureInfo.filename}`);
  }
}

module.exports = new FileController();
  • file.server.js
const connection = require('../app/database');

class FileService {
  async createPicture(filename, mimetype, path, size, url) {
    const statement = `INSERT INTO picture (filename, mimetype, path, size, url) VALUES (?, ?, ?, ?, ?)`;
    try {
      const [result] = await connection.execute(statement, [
        filename,
        mimetype,
        path,
        size,
        url
      ]);
      return result;
    } catch (error) {
      console.log(error);
    }
  }

  async getPictureByFilename(filename) {
    const statement = `SELECT * FROM picture WHERE filename = ?;`;

    try {
      const [result] = await connection.execute(statement, [filename]);

      return result[0];
    } catch (error) {
      console.log(error);
    }
  }

}

module.exports = new FileService();

使用koa-body进行图片上传和文件上传

  • index.js,在koa-body中如果没有区分图片和文件的存储路径,那么图片和文件将会存储在同一个文件夹中,在以下代码中,笔者使用正则匹配上传文件的格式进行区分图片和文件的存储路径,以下的是图片的存储路径,而文件的存储路径在file.service.js中,采用流的方式进行存储文件
const Koa = require('koa');
// 导入bodyParser中间件,解析json
// const bodyParser = require('koa-bodyparser');
const koaBody = require('koa-body');
// 导入错误信息处置方法
const errorHandler = require('./error-handle');

const swagger = require('../utils/swagger'); // 存放swagger.js的位置
const { koaSwagger } = require('koa2-swagger-ui');

const koaStatic = require('koa-static');

const cors = require('koa2-cors');
const path = require('path');

// 导入路由 动态加载路由
const useRoutes = require('../router');

const app = new Koa();

const logger = require('../utils/logs');

// 遍历存储图片的路径
const FindPath = require('../utils/get-file-path');
FindPath.fileDisplay('./file/picture', (arr) => {
  console.log(arr, 'path');
});

app.use(async (ctx, next) => {
  const start = new Date();
  await next();
  const ms = new Date() - start;
  console.log(`${ctx.method} ${ctx.url} - ${ms}ms`);
  logger.resLogger(ctx, ms);
});

// error-handling
app.on('error', (err, ctx) => {
  logger.errorLogger(ctx, err);
  console.error('server error', err);
});

// app.use(bodyParser());
app.use(
  koaBody({
    multipart: true,
    formidable: {
      keepExtensions: true, // 保持文件的后缀
      maxFieldsSize: 20 * 1024 * 1024, // 文件上传大小
      onFileBegin: (name, file) => {
        const ext = file.originalFilename.split('.').pop();
        file.newFilename = `${Date.now()}.${ext}`;
        file.filename = file.newFilename;
        
        // 这边是为了将上传的图片和文件进行区分开,放入不同的文件夹
        const regex = /^image/;
        if (regex.test(file.mimetype)) {
          file.path = `/file/picture/${file.filename}`;
          file.filepath = path.join(
            __dirname,
            `../../file/picture/${file.filename}`
          );
        }
      }
    }
  })
);

app.use(koaStatic('../../file'));
app.use(cors());
// 接口文档配置
app.use(swagger.routes(), swagger.allowedMethods());
app.use(
  koaSwagger({
    routePrefix: '/swagger', // 接口文档访问地址
    swaggerOptions: {
      url: '/swagger.json' // example path to json 其实就是之后swagger-jsdoc生成的文档地址
    }
  })
);

useRoutes(app);

app.on('error', errorHandler);

module.exports = app;

  • file.router.js,这里有上传图片、查看图片、上传excelpdf文件以及下载的API,文件下载使用到了koa-send这个中间件
const Router = require('koa-router');

// 具体的处理逻辑
const {
  pictureHandler,
  excelHandler
} = require('../middleware/file.middleware');

const {
  savePictureInfo,
  getPictureInfo,
  saveFileInfo,
  savePdfInfo,
  downloadPdfInfo
} = require('../controller/file.controller');

const fileRouter = new Router({
  prefix: '/api/file'
});

fileRouter.post('/picture', savePictureInfo);
fileRouter.get('/picture/:filename', getPictureInfo);

fileRouter.post('/excel', saveFileInfo);
fileRouter.post('/pdf', savePdfInfo);
fileRouter.get('/download/:filename', downloadPdfInfo);

module.exports = fileRouter;
  • file.controller.js
// const xlsx = require('xlsx');
const path = require('path');
const koaSend = require('koa-send');

// 读取文件信息
const fs = require('fs');
const fileService = require('../service/file.service');

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

const config = require('../app/config');

class FileController {
  async savePictureInfo(ctx, next) {
    // 1.获取图像相关信息
    const { filename, mimetype, size, path } = ctx.request.files.picture;

    const url = `${config.HOST}:${APP_PORT}/api${path}`;
    // const url = `${ctx.origin}/api${path}`;

    // 2.将图像信息保存到数据库
    await fileService.createPicture(filename, mimetype, path, size, url);

    // 4.返回数据
    ctx.body = {
      statusCode: 200,
      imageUrl: url,
      message: '上传图片成功!'
    };
  }

  async getPictureInfo(ctx, next) {
    const { filename } = ctx.params;

    const pictureInfo = await fileService.getPictureByFilename(filename);

    // 2.提供图像信息
    ctx.response.set('content-type', pictureInfo.mimetype); // 需要查看图片就这样设置,如要下载可注释

    ctx.body = fs.createReadStream(`${PICTURE_PATH}/${pictureInfo.filename}`);
  }

  async saveFileInfo(ctx, next) {
    // const file = ctx.request.files.file; // 获取上传文件 postman
    const file = ctx.request.files.raw; // 获取上传文件 项目中

    const getRes = await fileService.getExcelData(file);

    if (getRes.status) {
      if (getRes.datas.length > 1) {
        console.log('暂时不支持多个sheet存在');
      } else {
        //得到的是数组
        const objs = getRes.datas[0];
        ctx.body = {
          status: 200,
          data: objs,
          msg: '上传文件成功'
        };
      }
    } else {
      // errorResult.errorRes(ctx, getRes.msg);
      console.log(getRes.msg);
    }
    await next();
  }

  async savePdfInfo(ctx, next) {
    // const file = ctx.request.files.file; // 获取上传文件 postman
    const file = ctx.request.files.raw; // 获取上传文件 项目中

    const { originalFilename } = file;

    const url = `${config.HOST}:${APP_PORT}/api/file/download/${originalFilename}`;
    // const url = `${ctx.origin}/api/file/download/${originalFilename}`;

    await fileService.getPdfData(file);

    ctx.body = {
      status: 200,
      msg: '上传成功',
      downloadUrl: url
    };
  }

  async downloadPdfInfo(ctx, next) {
    const { filename } = ctx.params;

    const path = `/file/filePdf/${filename}`;
    ctx.attachment(path);
    const result = await koaSend(ctx, path);

    ctx.body = {
      status: 200,
      result: result
    };
  }
}

module.exports = new FileController();

  • file.service.js,在getExcelData方法中定义了解析excel文件的方法,采用前端上传excel文件,后端进行解析并将数据返回给前端展示的方法
const xlsx = require('xlsx');
const path = require('path');
const downPath = path.resolve(__dirname, '../../file/fileExcel');
const pdfPath = path.resolve(__dirname, '../../file/filePdf');
const fs = require('fs');
const connection = require('../app/database');

class FileService {
  async createPicture(filename, mimetype, path, size, url) {
    const statement = `INSERT INTO picture (filename, mimetype, path, size, url) VALUES (?, ?, ?, ?, ?)`;
    try {
      const [result] = await connection.execute(statement, [
        filename,
        mimetype,
        path,
        size,
        url
      ]);
      return result;
    } catch (error) {
      console.log(error);
    }
  }

  async getPictureByFilename(filename) {
    const statement = `SELECT * FROM picture WHERE filename = ?;`;

    try {
      const [result] = await connection.execute(statement, [filename]);

      return result[0];
    } catch (error) {
      console.log(error);
    }
  }

  async getExcelData(file) {
    const reader = fs.createReadStream(file.filepath); // 创建可读流

    const filePath = `${downPath}/${file.originalFilename}`;

    const upStream = fs.createWriteStream(filePath); // 创建可写流

    try {
      const getRes = await this.getFile(reader, upStream, filePath); //等待数据存储完成

      const datas = []; //可能存在多个sheet的情况
      if (!getRes) {
        //没有问题
        const workbook = xlsx.readFile(filePath);

        const sheetNames = workbook.SheetNames; // 返回 ['sheet1', ...]
        for (const sheetName of sheetNames) {
          const worksheet = workbook.Sheets[sheetName];
          const data = xlsx.utils.sheet_to_json(worksheet);
          datas.push(data);
        }
        return {
          status: true,
          datas
        };
      } else {
        return {
          status: false,
          msg: '上传文件失败'
        };
      }
    } catch (error) {
      console.log(error, 'error');
    }
  }

  async getPdfData(file) {
    const reader = fs.createReadStream(file.filepath); // 创建可读流

    const filePath = `${pdfPath}/${file.originalFilename}`;

    const upStream = fs.createWriteStream(filePath); // 创建可写流

    const result = await this.getFile(reader, upStream, filePath); //等待数据存储完成

    return result;
  }

  getFile(reader, upStream, filePath) {
    return new Promise(function (result) {
      let stream = reader.pipe(upStream); // 可读流通过管道写入可写流
      stream.on('finish', function (err) {
        result(err);
      });
    });
  }
}

module.exports = new FileService();