Angular与PDF之三: 服务器端渲染PDF

news2024/10/6 20:35:43

一、Angular PDf server 端渲染

1. 环境准备

    _                     _                 ____ _     ___
  / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
  / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |   | |
/ ___ \| | | | (_| | |_| | | (_| | |     | |___| |___ | |
/_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
              |___/
Angular CLI: 16.0.0
Node: 16.14.0
Package Manager: npm 8.3.1
OS: darwin arm64
Angular: 
Package                     Version
------------------------------------------------------
@angular-devkit/architect    0.1600.0 (cli-only)
@angular-devkit/core         16.0.0 (cli-only)
@angular-devkit/schematics   16.0.0 (cli-only)
@schematics/angular          16.0.0 (cli-only)


"express": "^4.18.2",
"handlebars": "^4.7.7",
"puppeteer": "^20.1.2",

2. 新建一个 angular 项目并在项目下启动一个 express 服务,用于数据的获取和 PDF 的渲染导出

1. 创建一个新项目

ng new server-pdf

2. 修改 app.component.htmlapp.component.ts

添加一个 到处按钮, 导出当前在后端获取回来的list

app.component.html
<div>
   <h1>angular PDF server 渲染</h1>
   <button (click)="downloadPdf()"> Export to PDF</button>
   <div class="line">

   </div>
   <table id="list">
       <thead>
           <tr>
               <th *ngFor="let item of titleList">{{item}}</th>
           </tr>
       </thead>
       <tbody>
           <tr *ngFor="let item of data">
               <td *ngFor="let item of data; let i = index">{{ getcloumName(i, item) }}</td>
           </tr>
       </tbody>
   </table>
</div>
app.component.ts

init 时,从后端获取数据,并渲染到页面

  import { Component, OnInit } from '@angular/core';
  import { HttpClient } from '@angular/common/http';
  @Component({
      selector: 'app-root',
      templateUrl: './app.component.html',
      styleUrls: ['./app.component.less']
  })
  export class AppComponent implements OnInit {
      data: {
          name: string;
          age: string;
          gender: string;
      }[] = []
      constructor(private http: HttpClient) { }
      ngOnInit() {
          this.getData();
      }
      getData(): void {
          this.http.get('/api/data').subscribe((data: any) => {
              this.data = data.users;
          });
      }
      downloadPdf(): void {
          this.http.get('/api/pdf', {
            responseType: 'blob'
          }).subscribe((pdfBlob: Blob) => {
            const pdfUrl = URL.createObjectURL(pdfBlob);
            const downloadLink = document.createElement('a');
            downloadLink.href = pdfUrl;
            downloadLink.download = 'angular-pdf.pdf';
            downloadLink.click();
          });
        }
  }

3. 在项目的根目录下 创建 server文件夹

创建 template.hbs template

创建要转换为 PDF HTML 元素

<div>
   <h1>angular PDF server 渲染</h1>
   <div class="line">
   </div>
   <table id="list">
       <thead>
           <tr>
              {{#each cloumKey}}
               <th>{{this}}</th>
              {{/each}}
           </tr>
       </thead>
       <tbody>
          {{#each users}}
           <tr>
              {{#each this}}
               <td>{{this}} {{[`cloum--${@index}`]}}</td>
              {{/each}}
           </tr>
          {{/each}}
       </tbody>
   </table>
</div>

创建 server.js

当前端在请求/api/pdf 这个接口时,后端拿到当前页面渲染的数据,使用 handlebars 定义的 template 将数据转换为 HTML 格式,然后再使用 puppeteer 创建一个无头浏览器,创建空白页,将 handlebars 创建的 HTML 放进 浏览器中转换为 PDF Buffer 的格式传输给前端,传输完毕关闭 虚拟浏览器

导入 handlebars express fs 和创建好的 handlebars template

const express = require('express');
const handlebars = require('handlebars');
const fs = require('fs');
const templatePath = './server/template.hbs';
// 创建一个Express应用程序
const app = express();
const port = 3000;
// 创建一个 50 * 50 的 table 表格数据
const json = [];
for (var i = 0; i < 50; i++) {
 var row = {};
 for (var j = 0; j < 50; j++) {
   var cellKey = 'cloum--' + j;  // 列的属性名
   var cellValue = 'Cell ' + i + '-' + j;  // 列的值
   row[cellKey] = cellValue;
}
 json.push(row);
}
const data = {
   users: json
}
// 设置路由处理程序
app.get('/api/data', (req, res) => {
   res.json(data);
});
app.get('/api/pdf', async (req, res) => {
   // 读取模板文件
   let html;
   fs.readFile(templatePath, 'utf8', (err, template) => {
       if (err) {
           console.error('Error reading template:', err);
           // 处理错误
           return;
      }
       // 编译模板
       const compiledTemplate = handlebars.compile(template);
       // 应用数据到模板
       html = compiledTemplate(data);
  });
   const puppeteer = require('puppeteer');
   // 创建一个无头浏览器
   const browser = await puppeteer.launch();
   // 创建一个新页面
   const page = await browser.newPage();
   // 将 handlebars 生成的 html 放入浏览器中
   await page.setContent(html);
   // 将当前页面 转化成 PDF buffer
   const pdfBuffer = await page.pdf({ format: 'A4' });
   res.setHeader('Content-Type', 'application/pdf');
   res.setHeader('Content-Disposition', 'attachment; filename="example.pdf"');
   // 发送给前端
   res.send(pdfBuffer);
   // 关闭浏览器实例
   await browser.close();
});
// 启动服务器
app.listen(port, () => {
   console.log(`Server listening on port ${port}`);
});

​​​​​​​创建 proxy.conf.json 将前端的请求都转发到 server.js 中

{
   "/api/*": {
   "target": "http://localhost:3000",
   "secure": false,
   "logLevel": "debug"
  }
}

在 package.json 中修改前端启动命令,增加server 启动命令

   "start": "ng serve --proxy-config proxy.conf.json",
   "server": "node ./server/server.js"

以上就是angular PDF server 渲染完整流程

二、row 行数过多时如何分页

这里有一个 5*100 table 表格,规定每页只能显示 20 条数据通过分页处理

1. 将 100 * 100 的 table data 切成 五个 100 * 20 数据

  function cutData(data) {
        const pageSize = 20;
        const listData = [];
        const len = data.length;
        const count = Math.ceil(len / pageSize);
        const cloumKey = [];
        for (let i = 0; i < count; i++) {
          const start = i * pageSize;
          const end = start + pageSize;
          const arr = data.slice(start, end);
          cloumKey.push(Object.keys(arr[0]))
          listData.push(arr);
        }
        return { listData, cloumKey };
  }

​​​​​​​2. 修改 get('/api/pdf') 方法

使用 cutData 方法将原有的 100 * 100 的数据分割成 五个 100 * 20 然后循环生成的数据依次放入 handlebars template ,去生成每页的 html 拿到 html 使用 puppeteer 将其转化为 PDF 并保存在本地 创建一个数据将当前创建的 PDF name保存下来供后面PDF合并使用 将分页的 PDF 全部生成完之后 使用 pdf-lib 把刚才生成的 PDF 合并成一个

app.get('/api/pdf', async (req, res) => {
   // 读取模板文件
   let html;
   const puppeteer = require('puppeteer');
   const browser = await puppeteer.launch();
   const page = await browser.newPage();
   const pdfFiles=[];
   const tableData = cutData(data.users);
   console.log('%c [ tableData ]-39', 'font-size:13px; background:pink; color:#bf2c9f;', tableData)
   for (let i = 0; i < tableData.cloumKey.length + 1; i++) {
     fs.readFile(templatePath, 'utf8', (err, template) => {
         if (err) {
             console.error('Error reading template:', err);
             // 处理错误
             return;
        }
         // 编译模板
         const compiledTemplate = handlebars.compile(template);
         // 应用数据到模板
         const pdfData = {
           users: tableData.listData[i],
           cloumKey: tableData.cloumKey[i]
        }
         html = compiledTemplate(pdfData);
    });
     await page.setContent(html);
     var pdfFileName =  'sample'+(i)+'.pdf';
     await page.pdf({path: __dirname + pdfFileName,format: 'A4' });
     pdfFiles.push(pdfFileName);
  }
   res.setHeader('Content-Type', 'application/pdf');
   res.setHeader('Content-Disposition', 'attachment; filename="example.pdf"');
   // 关闭浏览器实例
   await browser.close();
   const pdfBytes = await mergePDF(pdfFiles);
   res.send(pdfBytes);

});


const mergePDF = async (sourceFiles) => {
 const pdfDoc = await PDFDocument.create()
 for(let i = 0;i<sourceFiles.length;i++) {
   const localPath = __dirname + sourceFiles[i]
   const PDFItem = await PDFDocument.load(fs.readFileSync(localPath))
   for(let j = 0;j<PDFItem.getPageCount();j++) {
     const [PDFPageItem] = await pdfDoc.copyPages(PDFItem, [j])
     pdfDoc.addPage(PDFPageItem)
  }
}
 const pdfBytes = await pdfDoc.save()
 fs.writeFileSync('merge.pdf', pdfBytes)
 return pdfBytes;
}

前端页面

PDF 导出页面

三、column 列数过多时数据如何切分

一共 20列, 将每十列切成一个数组,使用上面的 get('/api/pdf') 方法进行渲染传输

function cutData(data) {
     const pageSize = 10;
     const listData = [];
     const columKey = Object.keys(data[0]);
     const len = columKey.length;
     const count = Math.ceil(len / pageSize);
     const cloumKey = [];
     for (let i = 0; i < count; i++) {
       const start = i * pageSize;
       const end = start + pageSize;
       const arr = data.slice(start, end);
       for (let j = 0; j < arr.length; j++) {
         arr[j] = _.pick(arr[j], columKey.slice(start, end));
      }
       cloumKey.push(columKey.slice(start, end));
       listData.push(arr);
    }
     return { listData, cloumKey };
  }

前端页面

PDF 导出页面

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/528569.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

RK 平台MIPI 点屏注意事项

转自&#xff1a;https://www.cnblogs.com/chorm590/p/11658360.html rk 平台关于 MIPI 屏幕的点屏流程已经非常完善了&#xff0c;基本上只要确定了硬件没问题、接线没问题、屏幕没问题&#xff0c;再稍稍配置一下 dtsi 里的参数就可以的了。 MIPI 点屏流程大致可以概括为以下…

一步一步教你写kubernetes sidecar

什么是sidecar&#xff1f; sidecar&#xff0c;直译为边车。 如上图所示&#xff0c;边车就是加装在摩托车旁来达到拓展功能的目的&#xff0c;比如行驶更加稳定&#xff0c;可以拉更多的人和货物&#xff0c;坐在边车上的人可以给驾驶员指路等。边车模式通过给应用服务加装一…

Python命名空间和作用域

命名空间定义了在某个作用域内变量名和绑定值之间的对应关系&#xff0c;命名空间是键值对的集合&#xff0c;变量名与值是一一对应关系。作用域定义了命名空间中的变量能够在多大范围内起作用。 命名空间在python解释器中是以字典的形式存在的&#xff0c;是以一种可以看得见…

OCPC系列三 - 展开说说广告业务及算法介绍

系列分享&#xff1a; OCPC系列 - OCPC介绍扫盲贴来啦_高阳很捷迅的博客-CSDN博客 OCPC系列 - PID算法&#xff08;理解PID算法&#xff09;-比例控制算法、积分控制算法、微分控制算法_高阳很捷迅的博客-CSDN博客 名词解释 先简单介绍下以下名称解释&#xff0c;方便下面阅…

Flink基础介绍-1 概述

Flink基础介绍-1 概述 一、Flink介绍1.1 批处理计算引擎1.2 流式计算引擎1.3 批处理和流处理 一、Flink介绍 Flink 是为分布式、高性能、随时可用以及准确的流处理应用程序打造的开源流处理框架。Flink 是一个框架和分布式处理引擎&#xff0c;用于对无界和有界数据流进行有状…

PDF转HTML格式怎么弄?将PDF转换为HTML的三种简便方法

PDF和HTML是两种常见的文档格式&#xff0c;它们在用途和外观上有很大的差异。然而&#xff0c;令人惊讶的是&#xff0c;这两种看似毫不相关的格式实际上可以相互转换。 一些网页编辑人员在更新网站内容时&#xff0c;通常会先将内容保存为PDF文件&#xff0c;然后在发布时将…

软件测试——性能测试

性能测试基础 为什么要进行性能测试&#xff08;WHY&#xff09;&#xff08;最重要&#xff09; 应用程序是否能够很快的响应用户的要求&#xff1f;应用程序是否能处理预期的用户负载并有盈余能力&#xff1f;应用程序是否能处理业务所需要的事务数量&#xff1f;在预期和非…

全网最详细,性能测试各种测试场景分析+性能测试基准测(超细总结)

目录&#xff1a;导读 前言一、Python编程入门到精通二、接口自动化项目实战三、Web自动化项目实战四、App自动化项目实战五、一线大厂简历六、测试开发DevOps体系七、常用自动化测试工具八、JMeter性能测试九、总结&#xff08;尾部小惊喜&#xff09; 前言 面对日益复杂的业…

【JAVA程序设计】(C00135)基于Servlet+jsp的旅游管理系统

基于Servletjsp的旅游管理系统 项目简介项目获取开发环境项目技术运行截图 项目简介 本项目为基于Servletjsp的旅游管理系统:本项目分为二种角色&#xff1a; 管理员&#xff1a; 用户管理&#xff08;增删改查&#xff09;、线路管理&#xff08;增删改查&#xff09;、景点管…

对SRC并发漏洞挖掘的思考

对SRC并发漏洞挖掘的思考 1.burpsuite Turbo插件使用2.并发点赞测试3.并发验证码测试4.某代金券逻辑测试5.有限制的并发验证码绕过6.对于并发漏洞的思考 1.burpsuite Turbo插件使用 Turbo Intruder是一个用于发送大量HTTP请求并会分析其结果的Burp Suite扩展。它旨在补充Burp …

实景三维浪潮翻涌,新技术“席卷”石家庄!

5月11日&#xff0c;“全自主、全流程、全覆盖”2023实景三维新技术研讨会石家庄站暨航测与遥感学术交流会在石家庄凯旋金悦大酒店圆满举行。 本次会议由中国测绘学会、中国地理信息产业协会指导&#xff0c;河北省测绘学会、河北省地理信息产业协会主办&#xff0c;武汉大势智…

Grafana安装、升级与备份(02)

一、安装Grafana软件包 Grafana部署非常简单,直接使用yum命令从官网拉到安装再启动就可以了,本次使用的grafana版本为9.5.0 官网下载地址:Download Grafana | Grafana Labs # wget yum install -y https://dl.grafana.com/oss/release/grafana-9.5.0-1.x86_64.rpm # yum …

js:正则表达式常用方法总结test、exec、match、matchAll、replace、replaceAll、search

文章目录 正则使用testmatch/matchAll不加g加ggroup 的使用 matchAll不加g加g exec不加g加g searchreplace 正则使用 常用的几种方法有&#xff1a;test、exec、match、matchAll、replace、replaceAll、search test // 匹配返回true&#xff0c;不匹配false /e/.test("…

高通摄像头打不开报错SOF Freeze!

目录 报错日志 代码分析 报错日志 E/mm-camera( 647): <MCT ><ERROR> 95: mct_bus_sof_thread_run: Session 3: Hinting SOF freeze to happen. Sending event to dump infoE/mm-camera( 647): <MCT ><ERROR> 57: server_debug_dump_dat…

大模型来了,自动驾驶还远吗?关键看“眼睛”

感知系统是自动驾驶最重要的模块之一&#xff0c;被视为智能车的“眼睛”&#xff0c;对理解周围环境起到至关重要的作用。随着深度学习以及传感器技术的发展&#xff0c;感知系统呈现出迅猛的发展趋势&#xff0c;涌现出各种新技术&#xff0c;性能指标不断提升。本文将围绕感…

手撕机器学习算法--一步步推导-------NFL(没有免费午餐定理)

文章目录 前言一、NFL是什么&#xff1f;二、表现形式三、介绍四、手动推导 前言 其实机器学习也好&#xff0c;深度学习也罢&#xff0c;在我看来&#xff0c;代码编程终究是不重要的&#xff0c;因为现成的库&#xff0c;其数学原理&#xff0c;其公式推导才是我们需要理解的…

bind查找用法

inclue中的root 也取了名字 引用的时候应该是 引用外面的名字再引用里面的名字&#xff0c;包括rootview也是 binding.errorView.errorView.visibility View.GONE binding.errorView.statusHintIcon?.visibility View.GONE

绩效管理常见的7大误区,越用企业越走下坡路!

绩效管理是企业中非常重要的一个环节&#xff0c;但是很多企业在实施过程中常常会犯一些误区&#xff0c;导致绩效管理的实际效果和预期效果相差甚远。下面我们一起来看看企业中常见的七个绩效管理误区。 1、公司战略和绩效没有关联 绩效管理需要与企业战略相结合&#xff0c;…

【大数据学习篇7】小试牛刀统计并且分析天猫数据

本项目基于搭建大数据环境&#xff0c;通过将数据存放在HDFS上&#xff0c;从HDFS中获取数据&#xff0c;然后根据实际需求通过Spark或Spark SQL对数据进行读取分析&#xff0c;将分析结果存储到HBase表中&#xff0c;最终通过 ECharts数据可视化工具基于Python Web平台实现数据…

【深入浅出】条件概率的链式法则:定义、公式与应用

前言 在概率论的研究中&#xff0c;条件概率是一种非常重要的概念。当多个随机事件发生时&#xff0c;我们有时需要考虑它们同时发生的概率。条件概率的链式法则就是一种用于计算多个随机事件同时发生的概率的方法。本文将会介绍条件概率的链式法则的定义、公式以及应用。 定…