• 作者:老汪软件技巧
  • 发表时间:2024-11-02 11:01
  • 浏览量:

新书全栈实战项目:数字门店管理平台开源啦

GitHub地址(持续更新NestJS企业级实践):欢迎star⭐️⭐️⭐️

前端React+TypeScript+Vite

后端Nest+MySQL+Redis+Docker

大家好,我是元兮。

就在这个月,我发布了新书《NestJS全栈开发解析:快速上手与实践》并开源了书中的实战项目代码。其中需要特别说明的是,项目面向的是快速上手的基础人群,当然还有需要持续迭代的地方,比如MySQL事务篇、使用MQ进行异步和流量削峰、如何使用Nest实现爬虫服务、数据统计、实现商品数据的Excel导入导出、Nest集成飞书服务解析多维表格、Nest集成飞书实现每日数据统计等等,细水长流,我们一步步来。

背景

Execel的导入导出在实际的业务中很常见,例如导入Excel商品数据、列表数据导出等。

接下来在图书的实战项目中store-web-frontend和store-web-backend实现一下,没有clone过的同学可以根据项目README.md指示来初始化即可。

集成excel服务

在Node中集成Excel服务可以使用exceljs这个包,使用pnpm add exceljs -S安装。

先来认识exceljs有几种概念:

工作簿指的就是整个Excel表格,被称为Workbook,可以设置Excel的表名、创建者、创建时间和更新时间等属性。

const workbook = new ExcelJS.Workbook();
workbook.creator = 'Me';
workbook.title = '商品信息';
workbook.created = new Date(1985, 8, 30);
workbook.modified = new Date();

里面包含有多个工作表,也称为Sheet,这个库提供了丰富的属性进行操作每个Sheet的标签颜色、冻结操作、显示隐藏等等。

Sheet下管理多行(row)、多列(colunm)数据,我们经常会对数据进行行列维度的增删改查操作。

当然了,更细一层的维度是单元格(cell),可以进行合并单元格、数据验证等。

有了这些基础概念后,我们再来写一个Excel服务。

业务中一般会把这种功能作为公共基础服务统一管理,在common目录下,创建一个excel模块集成相关导入导出服务,这里不涉及标准CRUD相关操作,入口选择no就行了。

当然了,这里也可以单独抽离成一个服务@Injectable()来管理,不需要controller这一层,这里我有可能需要暴露接口给外部调用,所以用模块管理。

导入Excel文件并解析数据

_导出应用是什么意思_导入导出软件

在excel.service.ts中定义importExcel方法,接受file对象作为入参,接着来解析表格数据,实现代码如下:

  async importExcel(
    file: Express.Multer.File,
  ): Promise<Record<string, string | number>[]> {
    const workBook: Workbook = new Workbook();
    await workBook.xlsx.load(file.buffer);
    const sheet = workBook.worksheets[0];
    const headers: string[] = [];
    const data = [];
    sheet.eachRow((row, rowNumber) => {
      if (rowNumber === 1) {
        // 假设第一行是标题行,我们将其存储起来用于映射
        row.eachCell((cell) => {
          headers.push(cell.value.toString());
        });
      } else {
        const obj = {};
        // 不是标题行
        row.eachCell((cell, colNumber) => {
          obj[headers[colNumber - 1]] = cell.value;
        });
        data.push(obj);
      }
    });
    return data;
  }

workBook.xlsx.load接收并加载一个文件buffer对象,之后就可以通过workBook来操作sheet了。

除了加载buffer之外,xlsx对象中还提供了这几个方法。

与load对应的是writeBuffer,一般是通过buffer的方式来响应请求,实现下载文件,待会我们就会用到。比如这样:

const buffer = await workbook.xlsx.writeBuffer(); 
// 例如,直接将 buffer 响应给客户端 
res.setHeader('Content-Disposition', 'attachment; filename="output_buffer.xlsx"');
res.setHeader('Content-Type', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'); 
res.send(buffer);

readFile和writeFile对应,前者是接收一个文件路径,将读取文件到Workbook中进行操作;后者就是导出文件到本地,也可以导出到指定位置。

最后就是读写流的方式了,对应read和write,比如通过读取文件流的方式来写入Excel表中,写完之后会自动下载,这样:

const fs = require('fs'); 
const workbook = await createAndFillWorkbook(); 
const stream = fs.createWriteStream('output_stream.xlsx'); 
await workbook.xlsx.write(stream); 
stream.on('finish', () => { 
    console.log('Workbook saved to output_stream.xlsx'); 
});

好,话说回来,load完文件后就可以通过遍历所有的行实现对数据的解析,注意中间区分一下标题行和内容行即可,最后返回一个解析后的JSON数据。

注意,这个方法里面不会涉及到持久化db的逻辑,持久化属于业务逻辑,需要在其他服务中自行处理。在这里我们旨在提供一个通用的Excel服务。

没错!导入Excel就是这么简单,再来看看导出文件。

  async exportExcel(colunms: ColunmDto[], data: unknown[], sheetName: string) {
    const workBook = new Workbook();
    const sheet = workBook.addWorksheet(sheetName);
    sheet.columns = colunms;
    data.forEach((row) => {
      sheet.addRow(row);
    });
    return await workBook.xlsx.writeBuffer();
  }

导出的逻辑也很简洁,接收Excel表格的columns配置、需要导出的数据data和导出sheet名,最后返回一个可以导出的buffer缓冲对象。

如何使用?

完成服务编写后再来看如何在具体的模块中使用。在prodcutController中定义import路由接口,实现逻辑如下:

  @Post('import')
  @UseInterceptors(FileInterceptor('file'))
  async importProducts(@UploadedFile() file: Express.Multer.File) {
    const data = await this.excelService.importExcel(file);
    await this.productService.importProducts(data);
    return {
      message: '上传成功',
      file: file.filename,
      data,
    };
  }

方法中首先会通过importExcel来解析导入的文件数据,然后通过productService服务中importProducts来实现持久化db。

  async importProducts(data: unknown[]) {
    const entities = data.map((item: any) => {
      const entity = new ProductEntity()
      Object.assign(entity, item)
      return entity
    })
    return this.productRepository.save(entities)
  }

importProducts服务方法也很简洁,调用实体的save方法将entity实体对象持久化。

同理,在prodcutController中定义export路由接口


  @Get('export')
  @AllowNoToken()
  async exportProducts(@Res() res: Response) {
    // 定义列
    const columns = [
      { header: 'ID', key: 'id', width: 10 },
      { header: 'Name', key: 'name', width: 32 },
      { header: 'Price', key: 'price', width: 32 },
      { header: 'Desc', key: 'desc', width: 32 },
      { header: 'CreateTime', key: 'createTime', width: 32 },
    ];
    // 查询数据库或获取数据
    const { list } = await this.productService.getProductList();
    // 导出数据到Excel
    const buffer = await this.excelService.exportExcel(columns, list, '产品');
    res.setHeader(
      'Content-Type',
      'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
    );
    res.setHeader(
      'Content-Disposition',
      'attachment; filename="产品数据.xlsx"',
    );
    return res.send(buffer);
  }

方法逻辑分为三步: