想让你的工作轻松高效吗?揭秘Java + React导出Excel/PDF的绝妙技巧!
前言
在B/S架构中,服务端导出是一种高效的方式。它将导出的逻辑放在服务端,前端仅需发起请求即可。通过在服务端完成导出后,前端再下载文件完成整个导出过程。服务端导出具有许多优点,如数据安全、适用于大规模数据场景以及不受前端性能影响等。
本文将使用前端框架React和服务端框架Spring Boot搭建一个演示的Demo,展示如何在服务端导出Excel和PDF文件。当然,对于前端框架,如Vue、Angular等也可以采用类似的原理来实现相同的功能。
在服务端导出过程中,需要依赖额外的组件来处理Excel和PDF文件。对于Excel相关操作,可以选择POI库,而对于PDF文件,可以选择IText库。为了方便起见,本方案选择了GcExcel,它原生支持Excel、PDF、HTML和图片等多种格式的导出功能。这样一来,在实现导出功能的同时,也提供了更多的灵活性和互操作性。
实践
本文将演示如何创建一个简单的表单,其中包括姓名和电子邮箱字段,这些字段将作为导出数据。同时,前端将提供一个下拉选择器和一个导出按钮,通过下拉选择器选择导出的格式,然后点击导出按钮发送请求。等待服务端处理完成后,前端将下载导出的文件。
在服务端,我们需要实现相应的API来处理提交数据的请求和导出请求。我们可以定义一个对象,在内存中保存提交的数据。然后利用GcExcel库构建Excel对象,并将数据导出为不同的格式。
前端 React
1.创建React工程
新建一个文件夹,如ExportSolution,进入文件夹,在资源管理器的地址栏里输入cmd,然后回车,打开命令行窗口。
使用下面的代码创建名为client-app的react app。
npx create-react-app client-app
进入创建的client-app文件夹,使用IDE,比如VisualStudio Code打开它。
2.设置表单部分
更新Src/App.js的代码,创建React app时,脚手架会创建示例代码,需要删除它们。如下图(红色部分删除,绿色部分添加)。

在Src目录下,添加一个名为FormComponent.js的文件,在App.js中添加引用。
在FormComponent.js中添加如下代码。其中定义了三个state, formData和exportType,count用来存储页面上的值。与服务端交互的方法,仅做了定义。
import React, { useEffect, useState } from 'react';
export const FormComponent = () => {
const [formData, setFormData] = useState({
name: '',
email: '',
});
const [exportType, setExportType] = useState('0');
const [count, setCount] = useState(0);
useEffect(() => {
fetchCount();
},[]);
const fetchCount = async () => {
//TODO
}
const formDataHandleChange = (e) => {
setFormData({
...formData,
[e.target.name]: e.target.value
});
};
const exportDataHandleChange = (e) => {
setExportType(e.target.value);
};
const handleSubmit = async (e) => {
//TODO
};
const download = async (e) => {
//TODO
}
return (
<div class="form-container">
<label>信息提交</label>
<br></br>
<label>已有<span class="submission-count">{count}</span>次提交</label>
<hr></hr>
<form class="form" onSubmit={handleSubmit}>
<label>
姓名:
<input type="text" name="name" value={formData.name} onChange={formDataHandleChange} />
</label>
<br />
<label>
邮箱:
<input type="email" name="email" value={formData.email} onChange={formDataHandleChange} />
</label>
<button type="submit">提交</button>
</form>
<hr />
<div className='export'>
<label>
导出类型:
<select class="export-select" name="exportType" value={exportType} onChange={exportDataHandleChange}>
<option value='0'>Xlsx</option>
<option value='1'>CSV</option>
<option value='2'>PDF</option>
<option value='3'>HTML</option>
<option value='4'>PNG</option>
</select>
</label>
<br />
<button class="export-button" onClick={download}>导出并下载</button>
</div>
</div>
);
}
CSS的代码如下:
.form-container {
margin: 20px;
padding: 20px;
border: 1px solid #ccc;
width: 300px;
font-family: Arial, sans-serif;
min-width: 40vw;
}
.submission-count {
font-weight: bold;
}
.form{
text-align: left;
}
.form label {
display: block;
margin-bottom: 10px;
font-weight: bold;
}
.form input[type="text"],
.form input[type="email"] {
width: 100%;
padding: 5px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
}
.form button {
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border-radius: 4px;
cursor: pointer;
width: 100%;
}
.export{
text-align: left;
}
.export-select {
padding: 5px;
margin-bottom: 10px;
border: 1px solid #ccc;
border-radius: 4px;
width: 10vw;
}
.export-button {
padding: 10px 20px;
background-color: #007bff;
color: #fff;
border-radius: 4px;
cursor: pointer;
width: 100%;
}
hr {
margin-top: 20px;
margin-bottom: 20px;
border: none;
border-top: 1px solid #ccc;
}
试着运行起来,效果应该如下图:

3.Axios请求及文件下载
前端与服务端交互,一共有三种请求:
- 页面加载时,获取服务端有多少次数据已经被提交
- 提交数据,并且获取一共有多少次数据已经被提交
- 发送导出请求,并根据结果下载文件。
通过npm添加两个依赖,Axios用于发送请求,file-saver用于下载文件。
npm install axios
npm install file-saver
在FormComponent.js中添加引用
import axios from 'axios';
import { saveAs } from 'file-saver';
三个请求方法的代码如下:
const fetchCount = async () => {
let res = await axios.post("api/getListCount");
if (res !== null) {
setCount(res.data);
}
}
const handleSubmit = async (e) => {
e.preventDefault();
let res = await axios.post("api/commitData", {...formData});
if (res !== null) {
setCount(res.data);
}
};
const download = async (e) => {
let headers = {
'Content-Type': 'application/json',
'Access-Control-Allow-Headers': 'Content-Disposition'
};
let data = { exportType: exportType };
let res = await axios.post('/api/exportDataList', data, { headers: headers, responseType: 'blob' });
if (res !== null) {
let contentDisposition = res.headers['content-disposition']
let filename = contentDisposition.substring(contentDisposition.indexOf('"') + 1, contentDisposition.length - 1);
saveAs(res.data, filename);
}
}
三个请求都是同步的,使用了await等待返回结果。三个请求,会分别向已定义的api发送请求,其中fetchCount,仅会在页面第一次完成加载时执行。其他两个请求方法会在点击按钮时触发。
4.配置请求转发中间件
因为React的程序会默认使用3000端口号,而Springboot默认使用8080端口。如果在Axios直接向服务端发送请求时(比如:localhost:8080/api/getListCount ),会出现跨域的问题。因此需要添加一个中间件来转发请求,避免前端跨域访问的问题。
在src文件夹下面添加文件,名为setupProxy.js,代码如下:
const { createProxyMiddleware } = require('http-proxy-middleware');
module.exports = function(app) {
app.use(
'/api',
createProxyMiddleware({
target: 'http://localhost:8080',
changeOrigin: true,
})
);
};
OK,至此前端代码基本完成,但还暂时不能运行测试,因为服务端代码没有完成。
服务端 Springboot
1.创建Springboot工程
使用IDEA创建一个Springboot工程,如果使用的是社区(community)版本,不能直接创建Springboot项目,那可以先创建一个空项目,idea创建project的过程,就跳过了,这里我们以创建了一个gradle项目为例。
plugins {
id 'org.springframework.boot' version '3.0.0'
id 'io.spring.dependency-management' version '1.1.0'
id 'java'
id 'war'
}
group = 'org.example'
version = '1.0-SNAPSHOT'
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'com.grapecity.documents:gcexcel:6.2.0'
implementation 'javax.json:javax.json-api:1.1.4'
providedRuntime 'org.springframework.boot:spring-boot-starter-tomcat'
testImplementation('org.springframework.boot:spring-boot-starter-test')
}
test {
useJUnitPlatform()
}
在dependencies 中,我们除了依赖springboot之外,还添加了GcExcel的依赖,后面导出时会用到GcExcel,目前的版本是6.2.0。
2.添加SpringBootApplication
完成依赖的添加后,删除原有的main.java,并新创建一个ExportServerApplication.java,然后添加以下代码。
@SpringBootApplication
@RestController
@RequestMapping("/api")
public class ExportServerApplication {
public static void main(String[] args) {
SpringApplication.run(ExportServerApplication.class, args);
}
}
3.添加 getListCount 和 commitData API
继续在ExportServerApplication.java中添加一个ArraryList用来临时存储提交的数据,commitData把数据添加进ArraryList中,getListCount从ArraryList中获取数据数量。
private static ArrayList<CommitParameter> dataList = new ArrayList<>();
@PostMapping("/commitData")
public int commitData(@RequestBody CommitParameter par) {
dataList.add(par);
return dataList.size();
}
@PostMapping("/getListCount")
public int getCount() {
return dataList.size();
}
4.添加导出API
在React app中,我们使用selector允许选择导出的类型,selector提供了,Xlsx, CSV, PDF, HTML, PNG, 5种导出格式。在导出的API中,需要用GcExcel构建Excel文件,把提交的数据填入到Excel的工作簿中。之后,根据前端传递的导出类型来生成文件,最后给前端返回,进行下载。
在GcExcel,可以直接通过workbook.save把工作簿保存为Xlsx, CSV, PDF 以及HTML。但是在导出HTML时,因为会导出为多个文件,因此我们需要对HTML和PNG进行特殊处理。
@PostMapping("/exportDataList")
public ResponseEntity<FileSystemResource> exportPDF(@RequestBody ExportParameter par) throws IOException {
var workbook = new Workbook();
copyDataToWorkbook(workbook);
String responseFilePath = "";
switch (par.exportType) {
case Html -> {
responseFilePath = exportToHtml(workbook);
}
case Png -> {
responseFilePath = exportToImage(workbook);
}
default -> {
responseFilePath = "download." + par.exportType.toString().toLowerCase();
workbook.save(responseFilePath, Enum.valueOf(SaveFileFormat.class, par.exportType.toString()));
}
}
FileSystemResource file = new FileSystemResource(responseFilePath);
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getFilename() + "\"");
return ResponseEntity.ok()
.headers(headers)
.contentLength(file.contentLength())
.body(file);
}
private static void copyDataToWorkbook(Workbook workbook) {
Object[][] data = new Object[dataList.size() + 1][2];
data[0][0] = "name";
data[0][1] = "email";
for (int i = 0; i < dataList.size(); i++) {
data[i + 1][0] = dataList.get(i).name;
data[i + 1][1] = dataList.get(i).email;
}
workbook.getActiveSheet().getRange("A1:B" + dataList.size() + 1).setValue((Object) data);
}
对于HTML,可以直接通过FileOutputStream的方式,把HTML输出成为zip。
private String exportToHtml(Workbook workbook) {
String outPutFileName = "SaveWorkbookToHtml.zip";
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(outPutFileName);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
workbook.save(outputStream, SaveFileFormat.Html);
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
return outPutFileName;
}
对于PNG类型,GcExcel可以导出多种图片格式,这里通过ImageType.PNG来选择导出为PNG,以文件流的方式导出为图片。
另外,我们需要单独准备model的类,代码如下:
private String exportToImage(Workbook workbook) {
String outPutFileName = "ExportSheetToImage.png";
FileOutputStream outputStream = null;
try {
outputStream = new FileOutputStream(outPutFileName);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
IWorksheet worksheet = workbook.getWorksheets().get(0);
worksheet.toImage(outputStream, ImageType.PNG);
try {
outputStream.close();
} catch (IOException e) {
e.printStackTrace();
}
return outPutFileName;
}
CommitParameter.java:
package org.example;
public class CommitParameter {
public String name;
public String email;
}
ExportParameter.java:
package org.example;
public class ExportParameter {
public ExportType exportType;
}
ExportType.java:
package org.example;
public enum ExportType {
Xlsx,
Csv,
Pdf,
Html,
Png;
}
至此我们就完成了服务端的代码。
最终效果
通过表单添加一些数据,同时导出不同类型的文件。

打开这些文件,看看导出的数据是否正确。
Excel


CSV

HTML


PNG

写在最后
除了上述的导出功能外,GcExcel还可以实现其他功能,如迷你图,数据透视表、自定义函数等,欢迎大家访问:https://demo.grapecity.com.cn/documents-api-excel-java/demos/
扩展链接:
项目实战:在线报价采购系统(React +SpreadJS+Echarts)
Svelte 框架结合 SpreadJS 实现纯前端类 Excel 在线报表设计
想让你的工作轻松高效吗?揭秘Java + React导出Excel/PDF的绝妙技巧!的更多相关文章
- 想要更高效地找到信息,你需要掌握这些搜索技巧 (google or baidu)
想要更高效地找到信息,你需要掌握这些搜索技巧 (google or baidu) 转载:https://tingtalk.me/search-tips/ 在大型局域网(互联网)的今天,你以为搜索是一门 ...
- ZT 头文件包含其实是一想很烦琐的工作 第一个原则应该是,如果可以不包含头文件
当出现访问类的函数或者需要确定类大小的时候,才需要用头文件(使用其类定义) http://blog.csdn.net/clever101/article/details/4751717 看到这个 ...
- Groovy轻松入门——通过与Java的比较,迅速掌握Groovy (更新于2008.10.18)
摘自: http://www.blogjava.net/BlueSUN/archive/2007/03/10/103014.html Groovy轻松入门--通过与Java的比较,迅速掌握Groovy ...
- Groovy轻松入门——通过与Java的比较,迅速掌握Groovy
转自 :Groovy轻松入门——通过与Java的比较,迅速掌握Groovy (更新于2008.10.18) 在前几篇文章中,我已经向大家介绍了Groovy是什么,学习Groovy的重要性等内容,还不了 ...
- Python编程快速上手 让繁琐工作自动化 - 专业程序员的养成完整版PDF免费下载_百度云盘
Python编程快速上手 让繁琐工作自动化 - 专业程序员的养成完整版PDF免费下载_百度云盘 提取码:7qm4 有关本书 累计销售超过10万册 零基础编程者的不二之选 基于Python3编写 通用 ...
- java生成简单Excel工作薄
前言: 代码都是建立在实际需求上的,上周做完一个调外部电影券接口的项目,这周产品又要excel表格,大致内容为:券所属影院.图片URL.等信息制作为excel表格,把每次同步过来的数据给他分析. jx ...
- 工作了3年的JAVA程序员应该具备什么技能?(转)
工作了3年的JAVA程序员应该具备什么技能? 因为和同事有约定再加上LZ自己也喜欢做完一件事之后进行总结,因此有了这篇文章.这篇文章大部分内容都是面向整个程序员群体的,当然因为LZ本身是做Java开发 ...
- 高效并发一 Java内存模型与Java线程(绝对干货)
高效并发一 Java内存模型与Java线程 本篇文章,首先了解虚拟机Java 内存模型的结构及操作,然后讲解原子性,可见性,有序性在 Java 内存模型中的体现,最后介绍先行发生原则的规则和使用. 在 ...
- 工作中常用到的Java集合类有哪些?
前言 只有光头才能变强. 文本已收录至我的GitHub精选文章,欢迎Star:https://github.com/ZhongFuCheng3y/3y Java集合是我认为在Java基础中最最重要的知 ...
- NPOI导出Excel表功能实现(多个工作簿)(备用)
Excel生成操作类: 代码 using System; using System.Collections.Generic; using System.Text; using System.IO; u ...
随机推荐
- 搭建自动化 Web 页面性能检测系统 —— 设计篇
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品.我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值.. 本文作者:琉易 liuxianyu.cn 页面性能对于用户体验.用 ...
- 技术招聘漫谈 | 正在招Golang工程师的你,赶快收藏这份识人秘籍!
各位技术面试官,欢迎来到新一期的技术招聘漫谈专栏. 在前两期的专栏中,我们解析了前端工程师以及 Java 工程师 这两个常见技术岗位的招聘技巧. 今天,我们想把目光聚焦在一个前景与"钱&qu ...
- selenium4-获取页面元素相关信息
本小节我们简单说下如何使用selenium4-获取页面元素相关信息,以及获取页面元素的相关信息后可以做什么. 获取页面元素的主要目的:(1)执行完步骤后进行断言:(2)获取前一步骤的响应结果作为后续步 ...
- 驱动开发:内核ShellCode线程注入
还记得<驱动开发:内核LoadLibrary实现DLL注入>中所使用的注入技术吗,我们通过RtlCreateUserThread函数调用实现了注入DLL到应用层并执行,本章将继续探索一个简 ...
- 分享6个SQL小技巧
原创:扣钉日记(微信公众号ID:codelogs),欢迎分享,非公众号转载保留此声明. 简介 经常有小哥发出疑问,SQL还能这么写?我经常笑着回应,SQL确实可以这么写.其实SQL学起来简单,用起来也 ...
- 前端Vue自定义简单实用中国省市区三级联动选择器
前端Vue自定义简单实用中国省市区三级联动选择器, 请访问uni-app插件市场地址:https://ext.dcloud.net.cn/plugin?id=13118 效果图如下: 使用方法 < ...
- Java使用redis-Redis是并发安全的吗?
大家都清楚,Redis 是一个开源的高性能键值对存储系统,被开发者广泛应用于缓存.消息队列.排行榜.计数器等场景.由于其高效的读写性能和丰富的数据类型,Redis 受到了越来越多开发者的青睐.然而,在 ...
- python学习笔记:第九章异常
1.1 异常是什么 python使用异常对象来表示异常状态,并在遇到错误时引发异常.异常对象未被处理,程序将终止并显示一条错误信息. 我们可以通过各种方法引发和捕获错误,并采取对应措施. 1.2 将& ...
- 如何根据oops函数偏移快速定位源码?
如何根据函数偏移快速定位源码? 在内核栈的输出中,你一定注意到每一个函数的输出格式都是函数名+偏移量,而这儿的偏移就是调用下一个函数的位置.那么,能不能根据函数名+偏移量直接定位源码的位置呢? 答案是 ...
- K8S | Deployment应用编排
目录 一.背景 二.Deployment组件 1.简介 2.语法说明 三.基础用例 1.创建操作 2.查看信息 3.更新操作 4.删除操作 四.进阶用例 1.回滚操作 2.伸缩操作 3.暂停与恢复 五 ...