Angular与PDF之四: 反思代码与模板的复用

news2024/11/26 3:48:11

在我们前面关于Angular与PDF的几篇博客中分别讲了如何在在如何在客户端渲染PDF(Angular与PDF之一:如何在客户端渲染PDF_angular pdf_KenkoTech的博客-CSDN博客) 和预览(Angular 与PDF之二:打印预览的实现_angular pdf预览_KenkoTech的博客-CSDN博客) 然后是服务器端渲染(Angular与PDF之三: 服务器端渲染PDF_KenkoTech的博客-CSDN博客)

这一次我们分析一个实现预览和服务器渲染PDF的例子和其中关于代码和模板服用的一些思考。

对于这个需求也许有人会提出问题既然可以实现预览为什么不直接在客户端渲染PDF还要在服务器端渲染?其实如果复杂或者专业化的需求往往对于PDF的美观度和可定制化的页眉页脚是有需求的,目前对于单纯的客户端浏览器来说还无法做到定制复杂的页眉页脚等元素。以Chrome内置的PDF导出工具页面页脚是这样:

 

所以我想要定制页面页脚还是要借助服务器端的能力.也因此我们不能直接使用浏览器的浏览窗口,所以我们需要自定义预览界面。不同于浏览器动态可交互的特性,我们的PDF是固定好的尺寸和字体大小等相关元素。因此这里的预览需要有不同于实际页面的样式要求。

main-page.component.html

<div class="main-page">
  <h3>Main Page list</h3>
  <div class="main-page-list">
      <div *ngFor="let item of listItems; let i = index;">
          {{item.name}}, index {{i}}
      </div>
  </div>
  <button mat-raised-button color="primary" (click)="showPreviewModal()">Click to Export PDF</button>
</div>

 main-page.component.ts

import { Component } from '@angular/core';
import { MatDialog } from '@angular/material/dialog'
import { PdfPreviewModalComponent } from '../pdf-preview-modal/pdf-preview-modal.component';
import { HttpClient } from '@angular/common/http';

@Component({
  selector: 'app-main-page',
  templateUrl: './main-page.component.html',
  styleUrls: ['./main-page.component.css']
})
export class MainPageComponent {
  listItems = [
    { name: 'Item 1' }, { name: 'Item 2' }, { name: 'Item 3' }, { name: 'Item 4' },
    { name: 'Item 5' }, { name: 'Item 6' }, { name: 'Item 7' }, { name: 'Item 8' },
    { name: 'Item 9' }, { name: 'Item 10' }, { name: 'Item 11' }, { name: 'Item 12' },
    { name: 'Item 13' }, { name: 'Item 14' }, { name: 'Item 15' }, { name: 'Item 16' },
    { name: 'Item 17' }, { name: 'Item 18' }, { name: 'Item 19' }, { name: 'Item 20' }
  ];
  constructor(public dialog: MatDialog, public http: HttpClient) {

  }
  showPreviewModal() {
    this.dialog.open(PdfPreviewModalComponent).afterClosed().subscribe((pages) => {
      this.http.post('api/pdfRender', {
        pages
      }).subscribe((pdf: any) => {
        const pdfUrl = URL.createObjectURL(pdf);
        const download = document.createElement('a');
        download.href = pdfUrl;
        download.download = 'demo.pdf';
        download.click();
      });
    });
  }
}

main-page.component.css

* {
  box-sizing: border-box;
  margin: 0;
}
.main-page  {
  width: 300px;
  height: 300px;
  border: 1px solid;
}

h3 {
  height: 60px;
}

.main-page-list {
  height: 240px;
  width: 100%;
  overflow: auto;
}

实际结果是如下图:

我们看到在这里右边会出现一个滚动条,但是这个在PDF中不能交互所以我们需要单独设置样式隐藏它。

下面是关于预览窗口的具体代码:

pdf-preview-modal.component.html

<h2 mat-dialog-title>Preview</h2>
<mat-dialog-content class="mat-typography" #previewContent>
  <app-main-page [previewMode]="true"></app-main-page>
</mat-dialog-content>
<mat-dialog-actions align="end">
  <button mat-button mat-dialog-close>Cancel</button>
  <button mat-button (click)="confirmExport()">Export</button>
</mat-dialog-actions>

pdf-preview-modal.component.ts

import { Component, ViewChild, ElementRef } from '@angular/core';
import { MatDialogRef } from '@angular/material/dialog';

@Component({
  selector: 'app-pdf-preview-modal',
  templateUrl: './pdf-preview-modal.component.html',
  styleUrls: ['./pdf-preview-modal.component.css']
})
export class PdfPreviewModalComponent {
  @ViewChild('previewContent') previewContent!: ElementRef;

  constructor(private dialogRef: MatDialogRef<PdfPreviewModalComponent>) {

  }

  confirmExport() {
    const reportEls: Element[] = this.previewContent.nativeElement.querySelectorAll('.main-page');
    this.dialogRef.close(reportEls.map(o => o.innerHTML));
  }
}

pdf-preview-modal.component.css

:host ::ng-deep .main-page {
  transform: scale(0.8, 0.8);
}

:host ::ng-deep .main-page-list {
  overflow: hidden;
}

实际预览结果如下:

关于服务器端的实现这里比较复杂,我们需要借助于一个工具库jsreport,它可以让我们把数据动态绑定到模板并实现PDF合并的操作。我们不在这里讨论其具体的功能步骤代码,我们这里讨论的是简略的流程。

下面是服务器端的handlebar模板的实现。

main.handlebars

<link rel="stylesheet" href="{#asset main-page.component.css @encoding=link}" type="text/css" />
<link rel="stylesheet" href="{#asset pdf-preview-modal.component.css @encoding=link}" type="text/css" />
<div id="page-content"></div>
<script>
  var dataset = {{{ toJSON dataset }}};
  if (dataset && dataset.pages) {
    var contentEl = document.getElementById('page-content');
    if (contentEl) {
      contentEl.innerHTML = dataset.pages;
    }
  }
</script>

headerFooter.handlebars

<head>
    <title>header-footer</title>
    <link rel="stylesheet" href="{#asset headerFooter.css @encoding=link}" type="text/css" />
</head>

<body>
    <div style="page-break-before: always;"></div>
    {{/if}}
    <main class="main">
        <header class="header">
          <div class="icon">
            <img src="some-customer-brand-image.svg" alt="header-image">
          </div>
        </header>
        <footer class="footer">
          <div class="icon">
            <img src="some-customer-brand-image.svg" alt="header-image">
          </div>
          <div class="legal-info">
            some text here
          </div>
        </footer>
    </main>
</body>

简略时序图:

这里可以看到我们直接将客户端的采集的DOM结构数据绑定到了服务器端的模板并且复用了所有相关的样式文件。这个是这个实现的核心,在以往的实现中我们通常需要准备数据并书写PDF模板,然后才是绑定数据渲染。这种实现直接帮数据准备和模板书写和绑定数据三个步骤直接在客户端完成了,所以我们直接拿最终的DOM数据给到服务器端来做最后的渲染工作。

理想情况是最终我们不需要维护客户端和服务端的两套代码,只需要维护客户端的页面和样式文件就能直接在服务器端服用。

但是现实情况是受限于PDF与客户端页面的性质差异,最终我们还是要做一些差异化的处理。对于可能的PDF页面尺寸限制,我们会需要增加单独的样式。

因为最终的PDF内容直接来自预览窗口,所以原始的main-page组件里添加了针对预览页面的适配。

综合以上的措施,最终我们实现了PDF内容和样式的一次维护两端复用(PDF端,客户端), 避免了可能的复杂的维护情况。

优势:

  • PDF内容直接复用, 也就是数据的生产和绑定的逻辑直接复用了。
  • 客户端的样式直接复用。
  • 由于采用直接复用DOM,对于PDF模板没有单独的书写要求可以采用通用的PDF模板,并且这里的模板几乎可以免维护。

劣势:

  • 客户端实际上同时维护了一份针对PDF的样式。
  • 任何关于PDF端新增的修改,需要同时在客户端component上操作,增加逻辑分叉。

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

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

相关文章

Easeui 02 tree组件.

1.添加tree组件. tree组件的位置&#xff1a;DataGrid and Tree(表格和树) → tree(树)&#xff1b; 复制 tree组件到 "菜单管理"的div里面&#xff0c;如&#xff1a; 这里要动态绑定数据&#xff0c;所以把死数据删除&#xff0c;只留下一个 ul&#xff0c;如&am…

HCIA-动态路由

目录 动态路由&#xff1a; 动态路由的分类 按工作区域分类&#xff1a; 按算法和工作机制分类&#xff1a; 距离矢量路由协议&#xff1a; 链路状态路由协议&#xff1a; OSPF协议计算路由步骤&#xff1a; OSPF协议 OSPF协议报文&#xff1a; OSPF三张表 OSPF路由…

算法修炼之筑基篇——筑基一层后期(解决KMP算法,KMP算法模板)

✨博主&#xff1a;命运之光​​​​​​ &#x1f984;专栏&#xff1a;算法修炼之练气篇​​​​​ &#x1f353;专栏&#xff1a;算法修炼之筑基篇 ✨博主的其他文章&#xff1a;点击进入博主的主页​​​​​​ 前言&#xff1a;学习了算法修炼之练气篇想必各位蒟蒻们的基…

kafka 四 Kafka读写流程、LEO log end offset、物理存储 稠密索引 稀疏索引 、Kafka物理存储、深入了解读数据流程、删除消息

目录 Kafka读写流程 LEO log end offset 物理存储 稠密索引 稀疏索引 Kafka物理存储 深入了解读数据流程 删除消息 Kafka读写流程 写流程&#xff1a; 通过zookeeper 找leader分配开始读写Isr中的副本同步数据&#xff0c;并返回给leader ack返回给 分片ack 读流程&…

2023高考语文,用ChatGPT挑战全国卷作文,已达到双一流高校学生水平?

前言 2023年高考语文结束啦&#xff0c;今天我们用ChatGPT来挑战高考作文&#xff0c;一起来看看它的表现如何&#xff1f;ChatGPT突然爆火网络&#xff0c;它真的会取代人类的工作吗&#xff1f; 什么是ChatGPT&#xff1f; ChatGPT是由OpenAI开发的&#xff0c;OpenAI是一家…

BBA EDI 项目数据库方案开源介绍

近期为了帮助广大用户更好地使用 EDI 系统&#xff0c;我们根据以往的项目实施经验&#xff0c;将成熟的 EDI 项目进行开源。用户安装好知行之桥EDI系统之后&#xff0c;只需要下载我们整理好的示例代码&#xff0c;并放置在知行之桥指定的工作区中&#xff0c;即可开始使用。 …

排序算法的复杂度及稳定性详解(内含记忆小窍门)

排序算法的复杂度及稳定性 一、排序算法分类二、概念2.1 时间复杂度2.2 空间复杂度2.3 稳定性 三、表格比较注意 四、部分排序分析4.1 直接插入排序图示代码 4.2 冒泡排序图示代码 4.3 快速排序图示代码 五、结构化记忆&#xff08;小窍门&#xff09;5.1 结构化5.2 我的结构化…

2023 如何备考系统架构师?

高级系统架构设计师难度还是有的&#xff0c;所以一般千万不要裸考&#xff01;&#xff01;要时间充足&#xff0c;至少要接触过&#xff0c;反正没有基础的尽量还是不要去裸考了&#xff01; 一、系统架构设计师考试题型 考试科目分为综合题&#xff08;选择题&#xff09;&a…

Stable Diffusion最全保姆级安装教程(建议收藏)

Midjourney 因细致的画图风格备受大家的欢迎&#xff0c;但由于其网络环境以及会员费&#xff0c;导致入门门槛过高&#xff0c;拦住了很多对AIGC感兴趣的小伙伴。 今天阿良就教大家&#xff0c;不需要魔法&#xff0c;也不用交会员费&#xff0c;尽情玩转AI出图的保姆级安装教…

力扣算法系统刷题详细题解记录二(字符串、双指针法、栈与队列)

力扣算法系统刷题题解记录二&#xff08;字符串、双指针法、栈与队列&#xff09; 前言 参考顺序和资料&#xff1a;《代码随想录》 二刷要认真做笔记啦&#xff0c;加油&#xff01; 笔记模板&#xff1a; #### 解题思路#### 示意图#### 代码四、字符串 344.字符串反转 编…

求最小生成树(Kruskal算法和Prim算法)

目录 一、前言 二、相关概念 1、最小生成树 2、Prim算法&#xff08;对结点进行操作&#xff09; 3、kruskal 算法&#xff08;对边进行操作&#xff09; 三、例题 1、修建公路&#xff08;lanqiaoOJ题号1124&#xff09; 1、Prim算法题解 2、Kruskal算法 一、前言 很…

PyToch 深度学习 || 卷积神经网络分类

卷积神经网络分类 import torch import torch.nn as nn import torchvision import numpy as np from torch.autograd import Variable import matplotlib.pyplot as plt import torch.nn.functional as F import torch.utils.data as Data from torchvision import datasets,…

【业务功能篇20】Springboot java逻辑实现动态行转列需求

在此前&#xff0c;我也写过一个行转列的文章&#xff0c;是用存储过程sql处理的一个动态的逻辑 Mysql 存储过程\Mybatis框架call调用 实现动态行转列 那么后面我们同样又接收了业务的一个新需求&#xff0c;针对的是不同的业务数据&#xff0c;做的同样的一个展示数据报表&…

【C++11】 initializer_list | 右值引用 | 移动构造 | 完美转发

文章目录 1. 统一的列表初始化{ } 初始化initializer_list 2. 引用左值引用右值引用左值引用与右值引用的相互转换右值引用的真正使用场景移动构造 C98与C11传值返回问题注意事项总结 3. 完美转发 1. 统一的列表初始化 { } 初始化 C11 扩大了括号括起的列表(初始化列表)的使用…

使用PHP导出Excel时处理复杂表头的万能方法

使用PHP导出Excel时&#xff0c;如果是一级表头处理起来很简单&#xff0c;但如果碰到复杂一点的表头&#xff0c;比如二级、三级&#xff0c;甚至更多级别的表头要怎么办呢&#xff1f; 就像下面这个表头&#xff0c;有三层&#xff0c;并且每层都不太规则—— 难道我们每次处…

动态绑定v-model,并解决输入框无法输入和无法双向绑定问题

问题&#xff1a;在界面中想要动态获取数据库中返回的数据&#xff0c;作为下拉的值&#xff0c;每个下拉值中又包含不同的属性信息&#xff0c;给输入框动态绑定v-model&#xff0c;但是绑定成功后输入框内无法输入内容&#xff0c;且没有双向绑定 解决思路&#xff1a;1.双向…

SIM:基于搜索的用户终身行为序列建模

SIM&#xff1a;基于搜索的用户终身行为序列建模 论文&#xff1a;《Search-based User Interest Modeling with Lifelong Sequential Behavior Data for Click-Through Rate Prediction》 下载地址&#xff1a;https://arxiv.org/abs/2006.05639 1、用户行为序列建模回顾 1…

在 AWS 上使用 OpenText 实现业务关键型应用程序的现代化

通过在云中进行信息管理建立持久的竞争优势 创新在云中发生的速度比以往任何时候都快。 企业面临着数字经济快速转型的挑战&#xff0c;充分释放业务信息的能力对于建立持久的竞争优势至关重要。为分散的员工扩大安全可靠的协作范围将是生产力和创新的关键驱动力。 如今大多…

Web UI自动化测试之元素定位

目前&#xff0c;在自动化测试的实际应用中&#xff0c;接口自动化测试被广泛使用&#xff0c;但UI自动化测试也并不会被替代。让我们看看二者的对比&#xff1a; 接口自动化测试是跳过前端界面直接对服务端的测试&#xff0c;执行效率和覆盖率更高&#xff0c;维护成本更低&am…

【EtherCAT】一、入门基础

什么是EtherCAT&#xff1f; 介绍简介特点和优势EtherCAT系统组成主站从站 硬件EtherCAT主站芯片EtherCAT从站芯片 EtherCAT应用层协议 工具软件 介绍 简介 EtherCAT&#xff08;Ethernet Control Automation Technology&#xff09;是一种高性能实时以太网通信协议&#xff…