一、场景

1、业务

客户提出前端的导出功能目前只支持页面数据导出,需要导出查询出的所有数据。
预计导出数据量较大,需要重新设计后台导出接口。

2、技术

  • 前端:vue2
  • 后端:kotlin + EasyExcel

    二、代码

    1、后端

    1)导出工具类

    ```java import com.alibaba.excel.EasyExcel import javax.servlet.http.HttpServletResponse

/**

  • @author: 三味
  • @createTime: 2021/12/2 19:10
  • @description: */ open class ExcelUtil {

    /**

    • @Author: 三味
    • @CreateTime:2021/12/2 19:10
    • @Description: 导出Excel(单页)
    • @param response http响应对象
    • @param list 打印数据集合
    • @param fileName 文件名
    • @param className 打印数据的实体类
    • @param excludeColumnFiledNames 自定义排除的列(没有传null) */ open fun export2Excel(response: HttpServletResponse,

      1. list: List<T>,
      2. fileName: String,
      3. className: Class<T>,
      4. excludeColumnFiledNames: MutableSet<String>?

      ){ // 设置格式 response.contentType = “application/vnd.ms-excel” response.characterEncoding = “utf-8”

      // 需要设置Access-Control-Expose-Headers,否则即使network中存在,js也无法拿到Content-Disposition response.setHeader(“Access-Control-Expose-Headers”, “Content-Disposition”) response.setHeader(“Content-Disposition”, “attachment;filename=” + fileName)

      EasyExcel.write(response.outputStream, className)

      1. .excludeColumnFiledNames(excludeColumnFiledNames)
      2. .sheet("sheet1") // 单页
      3. .doWrite(list)

      } } ```

      2)实体类

      ```java import com.alibaba.excel.annotation.ExcelProperty import com.alibaba.excel.annotation.format.DateTimeFormat import com.alibaba.excel.annotation.write.style.ColumnWidth import com.alibaba.excel.converters.date.DateStringConverter import com.baomidou.mybatisplus.annotation.IdType import com.baomidou.mybatisplus.annotation.TableId import com.baomidou.mybatisplus.annotation.TableName import java.sql.Date

@TableName(“customer”) @ColumnWidth(12) // 默认宽度 class Customer {

  1. var id: Int? = null
  2. @ExcelProperty("id",index = 0) // value:数据头显示的字 index:数据显示顺序
  3. var userId: Int? = null
  4. @ExcelProperty("country",index = 1)
  5. @ColumnWidth(15)
  6. var country: String? = null
  7. @ExcelProperty("phone",index = 2)
  8. @ColumnWidth(20)
  9. var phone: String? = null
  10. var remark: String? = null
  11. @ExcelProperty("Batch",index = 3)
  12. var batch: String? = null
  13. @ExcelProperty("Seller",index = 4)
  14. @ColumnWidth(20)
  15. var saleName: String? = null
  16. @ExcelProperty("time",
  17. index = 5,
  18. converter = DateStringConverter::class // 内置的日期转换converter
  19. )
  20. @DateTimeFormat("yyyy-MM-dd") // 导出的日期格式
  21. @ColumnWidth(15)
  22. var cts: Date? = null

}

  1. <a name="mFR6U"></a>
  2. #### 3)客户端调用
  3. ```java
  4. val list = record // 查询到的记录
  5. val fDate = SimpleDateFormat("yyyy-MM-dd")
  6. // 设置文件名,URLEncoder.encode可以防止中文乱码
  7. val fileName = URLEncoder.encode("数据表" + fDate.format(Date()) + ".xlsx", "UTF-8")
  8. // 数据列中排除remark字段
  9. val excludeColumnFiledNames: MutableSet<String> = HashSet()
  10. excludeColumnFiledNames.add("remark")
  11. ExcelUtil().export2Excel(response, list, fileName, Customer::class.java, excludeColumnFiledNames)

2、前端

1)导出方法

  1. /**
  2. * 包装后台导出的xlsx文件流
  3. * @param res
  4. */
  5. export function export2Excel(res) {
  6. const { headers, data } = res
  7. // 从响应头中获取文件名,*如果network中能看到content-disposition,代码无法获取,需后端配置
  8. const filename = decodeURI(headers['content-disposition'].split('filename=')[1])
  9. const blob = new Blob([data], { type: headers['content-type'] })
  10. const eleA = document.createElement('a')
  11. eleA.download = filename
  12. eleA.style.display = 'none'
  13. eleA.href = URL.createObjectURL(blob)
  14. document.body.appendChild(eleA)
  15. eleA.click()
  16. URL.revokeObjectURL(eleA.href) // 释放URL对象
  17. document.body.removeChild(eleA)
  18. }

2)axios(客户端包装了axios)

  1. import request from '@/utils/request'
  2. // 导出
  3. export function customerExport(query) {
  4. return request({
  5. url: '/customer/export',
  6. method: 'post',
  7. data: query,
  8. responseType: 'blob'
  9. })
  10. }

3)页面调用

  1. handleDownload() {
  2. customerExport({
  3. id: id,
  4. page: 1,
  5. pageSize: -1
  6. }).then(res => {
  7. export2Excel(res)
  8. })
  9. }

三、问题处理

1、后端导出报错

com.alibaba.excel.exception.ExcelDataConvertException: Can not find ‘Converter’ support class Date.

此报错为实体类中的字段Date类型EasyExcel无法做转换,需要自定义转换器或者在实体类中引用EasyExcel内置的转换器:

  1. @ExcelProperty("time",
  2. index = 5,
  3. converter = DateStringConverter::class // 内置的日期转换converter
  4. )
  5. var cts: Date? = null

其中的 converter = DateStringConverter::class 就是使用了内置的转换器,不加则报错。

提供好的转换器列表:
image.png

2、后端日期列数据格式

日期类型导出后,数据表中显示的格式不正确。同样需要在实体类中添加注解:

  1. @DateTimeFormat("yyyy-MM-dd") // 导出的日期格式
  2. var cts: Date? = null

注意,导入的是com.alibaba.excel.annotation.format.DateTimeFormat

3、前端接收数据流的方式

vue接收数据流做导出,思路就是包装blob后在页面添加一个链接触发点击下载事件。
感谢同事提供了思路与写法:

  1. function fileDownload(href, fileName = "") {
  2. return new Promise((resolve, reject) => {
  3. if (!href) return Promise.reject("herf文件下载地址不能为空");
  4. fetch(href, {method: "GET",}).then(response => {
  5. if (response.ok) {
  6. return response.blob(); //转bolb
  7. } else {
  8. return reject(response.statusText);
  9. }
  10. }).then(blob => {
  11. let a = document.createElement("a");
  12. a.href = URL.createObjectURL(blob);
  13. a.download = fileName;
  14. a.style.display = "none";
  15. document.body.appendChild(a);
  16. a.click();
  17. URL.revokeObjectURL(a.href); // 释放URL 对象
  18. a.remove();
  19. return resolve(true);
  20. }).catch(err => {
  21. return reject(err);
  22. });
  23. });
  24. };

但某些项目中,并不能用fetch抓取数据。
因为部分项目会对url做限制与包装(axios方式),所以改用了改了部分代码实现导出:

  1. customerExport({
  2. id: id,
  3. page: 1,
  4. pageSize: -1
  5. }).then(res => {
  6. export2Excel(res)
  7. })
  8. export function export2Excel(res) {
  9. const { headers, data } = res
  10. // 从响应头中获取文件名,*如果network中能看到content-disposition,代码无法获取,需后端配置
  11. const filename = decodeURI(headers['content-disposition'].split('filename=')[1])
  12. const blob = new Blob([data], { type: headers['content-type'] })
  13. const eleA = document.createElement('a')
  14. eleA.download = filename
  15. eleA.style.display = 'none'
  16. eleA.href = URL.createObjectURL(blob)
  17. document.body.appendChild(eleA)
  18. eleA.click()
  19. URL.revokeObjectURL(eleA.href) // 释放URL对象
  20. document.body.removeChild(eleA)
  21. }

参考资料: EasyExcel · 语雀 目前前端下载文件的最佳实践 axios下载后端返回的blob数据流 vue 下载excel文件方法,以及下载后的乱码问题 js无法获取响应header的Content-Disposition字段