赞
踩
前言
在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();
}
在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/
扩展链接:
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。