前言

在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 axiosnpm 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

PDF

CSV

HTML

PNG

写在最初

除了上述的导出性能外,GcExcel还能够实现其余性能,如迷你图,数据透视表、自定义函数等,欢送大家拜访:https://demo.grapecity.com.cn/documents-api-excel-java/demos/


扩大链接:

Spring Boot框架下实现Excel服务端导入导出

我的项目实战:在线报价洽购零碎(React +SpreadJS+Echarts)

Svelte 框架联合 SpreadJS 实现纯前端类 Excel 在线报表设计