- 作者:老汪软件技巧
- 发表时间: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);
}
方法逻辑分为三步: