手摸手教你前端和后端是如何实现导出 Excel 的?

Java雪荷 2024-08-21 16:33:01 阅读 94

前言

大家好呀,我是雪荷。在上篇文章(EasyExcel 初使用—— Java 实现多种写入 Excel 功能-CSDN博客)中给大家介绍了 Java 是如何写入 Excel 的,那么这篇算是对上篇文章的拓展,主要介绍前端和后端分别是如何导出数据至 Excel 的。

前端导出 Excel

我就用之前比赛的项目给大家演示吧,其组件库为 Ant Design Vue,框架为 Vue3,使用的第三方库为 XLSX。整体的实现并不困难只需写两个函数即可,话不多说直接上代码。

安装命令

<code>npm install XLSX

引入 XLSX

import * as XLSX from 'xlsx';

vue 的 template 部分

<template>

 <a-table

     id="table-data"code>

     style="margin-top: 30px;"code>

     :columns="columns"code>

     :dataSource="data"code>

     class="antv-table"code>

 >

 </a-table>

 <a-button type="primary" @click="exportTableData">导出表格数据</a-button>code>

 <a-button type="primary" @click="downloadExcel">Excel下载</a-button>code>

 <a-button type="primary" @click="exportData">导出数据</a-button>code>

</template>

ts 导出 Excel 代码

// 将 Table 组件的数据转为 Excel 数据

const transData = (columns: any, tableList: any) => {

 const obj = columns.reduce((acc, cur) => {

   if (!acc.titles && !acc.keys) {

     acc.titles = [];

     acc.keys = [];

  }

   acc.titles.push(cur.title);

   acc.keys.push(cur.dataIndex);

   return acc;

}, {});

 const tableBody = tableList.map(item => {

   return obj.keys.map(key => item[key]);

});

 return [obj.titles, ...tableBody];

};

// 将数据写入 Excel 文件

const exportTableData = () => {

 const tableData = transData(

     columns,

     data.value

);

 // 将一组 JS 数据数组转换为工作表

 const ws = XLSX.utils.aoa_to_sheet(tableData);

 // 创建 workbook

 const wb = XLSX.utils.book_new();

 // 将 工作表 添加到 workbook

 XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');

 // 将 workbook 写入文件

 XLSX.writeFile(wb, '销售数据.xlsx');

};

ts 请求后端接口代码

watchEffect(async () => {

 const res: any = await myAxios.post('/sale/data/list',);

 if (res?.code === 0) {

   data.value = res.data.map((item: SaleData, index: number) => ({

     key: index,

     id: item.id,

     name: item.name,

     price: item.price,

     totalNum: item.totalNum,

     userId: item.userId,

     datetime: item.datetime,

  }));

} else {

   message.error('数据获取失败');

}

});

后端代码

Controller 层:

 @PostMapping("/data/list")

   public BaseResponse<List<SaleData>> listSaleData() {

       List<SaleData> list = new ArrayList<>();

       list.add(new SaleData(1L, "苹果", new BigDecimal("10.00"), 10, 1L,"2024-01-01 13:00:00"));

       list.add(new SaleData(2L, "梨子", new BigDecimal("12.00"), 10, 1L,"2025-01-01 13:00:00"));

       list.add(new SaleData(3L, "西瓜", new BigDecimal("5.00"), 10, 1L,"2024-03-01 13:00:00"));

       list.add(new SaleData(4L, "香蕉", new BigDecimal("7.00"), 10, 1L,"2024-02-01 13:00:00"));

       return ResultUtils.success(list);

  }

SaleData:

@Data

@AllArgsConstructor

public class SaleData implements Serializable {

   @ExcelProperty("订单号")

   private Long id;

   @ExcelProperty("品种")

   private String name;

   @ExcelProperty("价格")

   private BigDecimal price;

   @ExcelProperty("数量")

   private Integer totalNum;

   @ExcelProperty("交易对象")

   private Long userId;

   @ExcelProperty("交易时间")

   private String datetime;

}

前端导出主要是通过 XLSX 这个库实现的,其根据 Ant Design 的 Table 组件的 columns 属性和后端返回的 List 构建成了一个 Excel 文件,最后利用 writeFile 方法导出 Excel 文件。

点击“导出表格数据”按钮,可以看到数据已导出至 Excel 中了。

image-20240729195452336

d0b3d3225e8f16c4acde85a471d64f6

优点:简单,便捷

缺点:

不灵活,不适合非固定表头和复杂的表头

前端导出不适合数据量大的场景,因为页面会卡死

后端导出 Excel

据我了解的后端实现导出 Excel 的功能主要有两种。一种是将 Excel 写入流中,前端拿到文件流再转为 blob 后进行下载,另一种是将 Excel 文件转为 base64 编码,前端将 base64 编码转为 blob 再下载。由此可见最终都是要转为 blob 的,拿到 blob 就好搞了。

后端将 Excel 写入流中

后端代码

Controller 层:

<code>@PostMapping("/data/download")

   public void exportSaleDetails(HttpServletResponse response) throws UnsupportedEncodingException {

       List<SaleData> list = new ArrayList<>();

       list.add(new SaleData(1L, "苹果", new BigDecimal("10.00"), 10, 1L, "2024-01-01 13:00:00"));

       list.add(new SaleData(2L, "梨子", new BigDecimal("12.00"), 10, 1L, "2025-01-01 13:00:00"));

       list.add(new SaleData(3L, "西瓜", new BigDecimal("5.00"), 10, 1L, "2024-03-01 13:00:00"));

       list.add(new SaleData(4L, "香蕉", new BigDecimal("7.00"), 10, 1L, "2024-02-01 13:00:00"));

       // 设置响应头信息

       response.setContentType("application/vnd.ms-excel");

       response.setCharacterEncoding("utf-8");

       String fileName = "销售数据1.xlsx";

       response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);

       // 写出Excel文件到响应

       try {

           EasyExcel.write(response.getOutputStream(), SaleData.class)

                  .sheet("合伙人业务订单")

                  .doWrite(list);

      } catch (IOException e) {

           e.printStackTrace();

      }

  }

ts 代码

const downloadExcel = () => {

 const response = myAxios.post('/sale/data/download', null, {

   responseType: 'blob',

}).then((res) => { // 处理返回的文件流

   const content = res

   const blob = new Blob([content])

   console.log(content)

   const fileName = '销售数据.xlsx';

   if ('download' in document.createElement('a')) { // 非IE下载

     const elink = document.createElement('a')

     elink.download = fileName

     elink.style.display = 'none'

     elink.href = URL.createObjectURL(blob)

     document.body.appendChild(elink)

     elink.click()

     URL.revokeObjectURL(elink.href) // 释放URL 对象

     document.body.removeChild(elink)

  } else { // IE10+下载

     navigator.msSaveBlob(blob, fileName)

  }

});

};

点击“Excel 下载”按钮导出数据。

219d096f29327fc8e8e9f4c17dabf0d

image-20240729200907785

优点:

传输效率高,网络负载小

内存占用,适合传输大文件

缺点:

会有跨域问题

后端返回 base 64 编码,前端再转为 blob

后端代码

<code> @PostMapping("/data/export")

   public BaseResponse<String> exportSaleData() throws UnsupportedEncodingException {

       List<SaleData> list = new ArrayList<>();

       list.add(new SaleData(1L, "苹果", new BigDecimal("10.00"), 10, 1L, "2024-01-01 13:00:00"));

       list.add(new SaleData(2L, "梨子", new BigDecimal("12.00"), 10, 1L, "2025-01-01 13:00:00"));

       list.add(new SaleData(3L, "西瓜", new BigDecimal("5.00"), 10, 1L, "2024-03-01 13:00:00"));

       list.add(new SaleData(4L, "香蕉", new BigDecimal("7.00"), 10, 1L, "2024-02-01 13:00:00"));

       // 生成 Excel 文件并转换为 Base64 编码

       ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

       EasyExcel.write(outputStream, SaleData.class).sheet("Sheet1").doWrite(list);

       byte[] bytes = outputStream.toByteArray();

       String excelBase64 = Base64.getEncoder().encodeToString(bytes);

       // 返回 Base64 编码的 Excel 内容给前端

       return ResultUtils.success(excelBase64);

  }

ts 代码

const exportData = async () => {

 const response: any = await myAxios.post('/sale/data/export');

 if (response?.code === 0) {

   const excelBase64 = response.data; // 接收后端返回的 Base64 编码字符串

   console.log(excelBase64)

   const blob = b64toBlob(excelBase64, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');

   const fileName = '销售数据.xlsx';

   if ('download' in document.createElement('a')) { // 非IE下载

     const elink = document.createElement('a')

     elink.download = fileName

     elink.style.display = 'none'

     elink.href = URL.createObjectURL(blob)

     document.body.appendChild(elink)

     elink.click()

     URL.revokeObjectURL(elink.href) // 释放URL 对象

     document.body.removeChild(elink)

  } else { // IE10+下载

     navigator.msSaveBlob(blob, fileName)

  }

}

}

// 将 Base64 字符串转换为 Blob 对象

function b64toBlob(b64Data, contentType = '', sliceSize = 512) {

 const byteCharacters = atob(b64Data);

 const byteArrays = [];

 for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {

   const slice = byteCharacters.slice(offset, offset + sliceSize);

   const byteNumbers = new Array(slice.length);

   for (let i = 0; i < slice.length; i++) {

     byteNumbers[i] = slice.charCodeAt(i);

  }

   const byteArray = new Uint8Array(byteNumbers);

   byteArrays.push(byteArray);

}

 return new Blob(byteArrays, {type: contentType});

}

主要分为三个阶段,首先后端将文件写入流中再将流转为 base64 编码返回给前端,第二步前端拿到 base64 编码将其转为 blob,最后根据 blob 进行下载。

点击“导出数据”按钮导出数据。

image-20240729201558898

e3d3341edc8985fd103c87c9dd764cf

优点:

传输数据是 base64 字符串便于调试

跨域问题少

缺点:

内存占用大,适合传输小文件

传输效率低

全部代码

前端

<code><!-- eslint-disable vue/multi-word-component-names -->

<template>

 <a-table

     id="table-data"code>

     style="margin-top: 30px;"code>

     :columns="columns"code>

     :dataSource="data"code>

     class="antv-table"code>

 >

 </a-table>

 <a-button type="primary" @click="exportTableData">导出表格数据</a-button>code>

 <a-button type="primary" @click="downloadExcel">Excel下载</a-button>code>

 <a-button type="primary" @click="exportData">导出数据</a-button>code>

</template>

<script setup lang="ts">code>

import {ref, watchEffect} from "vue";

import myAxios from "@/plugins/myAxios";

import {message, TableColumnsType} from "ant-design-vue";

import * as XLSX from 'xlsx';

const columns: TableColumnsType = [

{title: '订单号', width: 100, dataIndex: 'id', key: 'id', fixed: 'left'},

{title: '品种', dataIndex: 'name', key: 'name', width: 150},

{title: '价格', dataIndex: 'price', key: 'price', width: 150},

{title: '数量', dataIndex: 'totalNum', key: 'totalNum', width: 150},

{title: '交易对象', dataIndex: 'userId', key: 'userId', width: 150},

{title: '交易时间', dataIndex: 'datetime', key: 'datetime', width: 150},

];

interface SaleData {

 id: number;

 name: string;

 price: number;

 totalNum: number;

 userId: number;

 datetime: string;

}

interface DataItem {

 key: number;

 id: number;

 name: string;

 price: number;

 totalNum: number;

 userId: number;

 datetime: string;

}

const data = ref<DataItem[]>([]);

watchEffect(async () => {

 const res: any = await myAxios.post('/sale/data/list',);

 if (res?.code === 0) {

   data.value = res.data.map((item: SaleData, index: number) => ({

     key: index,

     id: item.id,

     name: item.name,

     price: item.price,

     totalNum: item.totalNum,

     userId: item.userId,

     datetime: item.datetime,

  }));

} else {

   message.error('数据获取失败');

}

});

const transData = (columns: any, tableList: any) => {

 const obj = columns.reduce((acc, cur) => {

   if (!acc.titles && !acc.keys) {

     acc.titles = [];

     acc.keys = [];

  }

   acc.titles.push(cur.title);

   acc.keys.push(cur.dataIndex);

   return acc;

}, {});

 const tableBody = tableList.map(item => {

   return obj.keys.map(key => item[key]);

});

 return [obj.titles, ...tableBody];

};

const exportTableData = () => {

 const tableData = transData(

     columns,

     data.value

);

 // 将一组 JS 数据数组转换为工作表

 const ws = XLSX.utils.aoa_to_sheet(tableData);

 // 创建 workbook

 const wb = XLSX.utils.book_new();

 // 将 工作表 添加到 workbook

 XLSX.utils.book_append_sheet(wb, ws, 'Sheet1');

 // 将 workbook 写入文件

 XLSX.writeFile(wb, '销售数据.xlsx');

};

const downloadExcel = () => {

 const response = myAxios.post('/sale/data/download', null, {

   responseType: 'blob',

}).then((res) => { // 处理返回的文件流

   const content = res

   const blob = new Blob([content])

   console.log(content)

   const fileName = '销售数据.xlsx';

   if ('download' in document.createElement('a')) { // 非IE下载

     const elink = document.createElement('a')

     elink.download = fileName

     elink.style.display = 'none'

     elink.href = URL.createObjectURL(blob)

     document.body.appendChild(elink)

     elink.click()

     URL.revokeObjectURL(elink.href) // 释放URL 对象

     document.body.removeChild(elink)

  } else { // IE10+下载

     navigator.msSaveBlob(blob, fileName)

  }

});

};

const exportData = async () => {

 const response: any = await myAxios.post('/sale/data/export');

 if (response?.code === 0) {

   const excelBase64 = response.data; // 接收后端返回的 Base64 编码字符串

   console.log(excelBase64)

   const blob = b64toBlob(excelBase64, 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet');

   const fileName = '销售数据.xlsx';

   if ('download' in document.createElement('a')) { // 非IE下载

     const elink = document.createElement('a')

     elink.download = fileName

     elink.style.display = 'none'

     elink.href = URL.createObjectURL(blob)

     document.body.appendChild(elink)

     elink.click()

     URL.revokeObjectURL(elink.href) // 释放URL 对象

     document.body.removeChild(elink)

  } else { // IE10+下载

     navigator.msSaveBlob(blob, fileName)

  }

}

}

// 将 Base64 字符串转换为 Blob 对象

function b64toBlob(b64Data, contentType = '', sliceSize = 512) {

 const byteCharacters = atob(b64Data);

 const byteArrays = [];

 for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {

   const slice = byteCharacters.slice(offset, offset + sliceSize);

   const byteNumbers = new Array(slice.length);

   for (let i = 0; i < slice.length; i++) {

     byteNumbers[i] = slice.charCodeAt(i);

  }

   const byteArray = new Uint8Array(byteNumbers);

   byteArrays.push(byteArray);

}

 return new Blob(byteArrays, {type: contentType});

}

</script>

<style scoped>

</style>

后端

Controller 层:

@Slf4j

@RestController

@RequestMapping("/sale")

public class SaleController {    

@PostMapping("/data/download")

   public void exportSaleDetails(HttpServletResponse response) throws UnsupportedEncodingException {

       List<SaleData> list = new ArrayList<>();

       list.add(new SaleData(1L, "苹果", new BigDecimal("10.00"), 10, 1L, "2024-01-01 13:00:00"));

       list.add(new SaleData(2L, "梨子", new BigDecimal("12.00"), 10, 1L, "2025-01-01 13:00:00"));

       list.add(new SaleData(3L, "西瓜", new BigDecimal("5.00"), 10, 1L, "2024-03-01 13:00:00"));

       list.add(new SaleData(4L, "香蕉", new BigDecimal("7.00"), 10, 1L, "2024-02-01 13:00:00"));

       // 设置响应头信息

       response.setContentType("application/vnd.ms-excel");

       response.setCharacterEncoding("utf-8");

       String fileName = "销售数据1.xlsx";

       response.setHeader("Content-disposition", "attachment;filename*=utf-8''" + fileName);

       // 写出Excel文件到响应

       try {

           EasyExcel.write(response.getOutputStream(), SaleData.class)

                  .sheet("合伙人业务订单")

                  .doWrite(list);

      } catch (IOException e) {

           e.printStackTrace();

      }

  }

   @PostMapping("/data/export")

   public BaseResponse<String> exportSaleData() throws UnsupportedEncodingException {

       List<SaleData> list = new ArrayList<>();

       list.add(new SaleData(1L, "苹果", new BigDecimal("10.00"), 10, 1L, "2024-01-01 13:00:00"));

       list.add(new SaleData(2L, "梨子", new BigDecimal("12.00"), 10, 1L, "2025-01-01 13:00:00"));

       list.add(new SaleData(3L, "西瓜", new BigDecimal("5.00"), 10, 1L, "2024-03-01 13:00:00"));

       list.add(new SaleData(4L, "香蕉", new BigDecimal("7.00"), 10, 1L, "2024-02-01 13:00:00"));

       // 生成 Excel 文件并转换为 Base64 编码

       ByteArrayOutputStream outputStream = new ByteArrayOutputStream();

       EasyExcel.write(outputStream, SaleData.class).sheet("Sheet1").doWrite(list);

       byte[] bytes = outputStream.toByteArray();

       String excelBase64 = Base64.getEncoder().encodeToString(bytes);

       // 返回 Base64 编码的 Excel 内容给前端

       return ResultUtils.success(excelBase64);

  }

   

   @PostMapping("/data/list")

   public BaseResponse<List<SaleData>> listSaleData() {

       List<SaleData> list = new ArrayList<>();

       list.add(new SaleData(1L, "苹果", new BigDecimal("10.00"), 10, 1L,"2024-01-01 13:00:00"));

       list.add(new SaleData(2L, "梨子", new BigDecimal("12.00"), 10, 1L,"2025-01-01 13:00:00"));

       list.add(new SaleData(3L, "西瓜", new BigDecimal("5.00"), 10, 1L,"2024-03-01 13:00:00"));

       list.add(new SaleData(4L, "香蕉", new BigDecimal("7.00"), 10, 1L,"2024-02-01 13:00:00"));

       return ResultUtils.success(list);

  }

}

SaleData:

@Data

@AllArgsConstructor

public class SaleData implements Serializable {

   @ExcelProperty("订单号")

   private Long id;

   @ExcelProperty("品种")

   private String name;

   @ExcelProperty("价格")

   private BigDecimal price;

   @ExcelProperty("数量")

   private Integer totalNum;

   @ExcelProperty("交易对象")

   private Long userId;

   @ExcelProperty("交易时间")

   private String datetime;

}

总结

相信大家看完也能学会导出 Excel,可以根据具体的开发需求选择不同的方案,如果有更好的方案欢迎探讨哈。



声明

本文内容仅代表作者观点,或转载于其他网站,本站不以此文作为商业用途
如有涉及侵权,请联系本站进行删除
转载本站原创文章,请注明来源及作者。