前言
在 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/
扩大链接:
Spring Boot 框架下实现 Excel 服务端导入导出
我的项目实战:在线报价洽购零碎(React +SpreadJS+Echarts)
Svelte 框架联合 SpreadJS 实现纯前端类 Excel 在线报表设计