需求
好久之前做的两个都是表格数据的页面,客户突然提了个新需求说想要页面能实现下载这两个表格的功能... 实现功能很简单,但是遇到了一个很坑的问题,就是文字被分页截断。所以想总结一下,同时在看一看代码有没有什么可以优化的地方。
我们的项目使用的框架是 Angular, 我就以在 Angular 中的使用举列子。
html2canvas
html2canvas 能够实现在用户浏览器端对真整个或者部分页面进行截屏。html2canvas 脚本将当前页面渲染成一个 canvas 图片。通过读取 DOM 并将不同的样式应用到这些元素上面实现。他不需要来自服务器任何渲染,整张图片都是在客户端浏览器创建。 html2canvas 可以通过获取 HTML 的某个 DOM ,然后生成 Canvas, 能让用户保存为图片。这个脚本主要是生成 canvas,那么如果我们需要生成图片还需要将他转化为图片格式.
- 安装
npm install html2canvas
- 引用
import html2canvas from 'html2canvas';
html2canvas(downloadElement, options?).then(canvas => {})
options 的值请参考官网
jspdf
- 安装
npm install jspdf
- 引用
import jspdf from 'jspdf';
let pdf = new jspdf('p', 'pt', 'a4'); // A4 size page of PDF
const imageData = canvas.toDataURL('image/jpeg', 1.0);
pdf.addImage(contentDataURL, 'JPEG', 0, 0, imgWidth, imgHeight)
new jspdf(): 第一个参数: 'l' 横向, 'p' 纵向 第二个参数: 测量单位 ("pt","mm", "cm", "m", "in" or "px") 第三个参数: 默认 "A4" 或者 [width, heignt] 还有其他格式。
pdf.addPage() 在 PDF 文档中添加新页面 pdf.addImage(imageData, format, x, y, width, height, alias, compression, rotation) 将图像添加到 PDf
imageData: string | HTMLImageElement | HTMLCanvasElement | Unit8Array format: 'JEPG' | 'PNG' x: x 坐标轴位置 y: y 坐标轴位置 width: 图片长度 height: 图片宽度 alias: 图片别名 compression: 压缩 'FAST' | 'MEDIUM' | 'SLOW' | 'NONE' rotation: 旋转度 (0-359)
删除某页pdf:
let targetPage = pdf.internal.getNumberOfPages();
pdf.deletePage(targetPage)
保存 pdf 文档: pdf.save('name.pdf')
两个插件的使用介绍完了,下面放上我整个实现的 demo 代码
首先,创建一个 dowload pdf 按钮的公共组件 DownloadPDFButtonComponent
down-pdf-button.ts downloadElement 参数为想要下载成 pdf 的 DOM 节点, 可以用 getElementById("idName") 或者 querySelector("#id") 来获取
import { Component, Input, OnInit, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NzMessageService, NzButtonModule, NzIconModule } from 'ng-zorro-antd';
import jspdf from 'jspdf';
import html2canvas from 'html2canvas';
@Component({
selector: 'download-pdf-button',
template:
`<button class="app-button right" nz-button [nzType]="'primary'">
<i nz-icon nzType="download" nzTheme="outline"></i>下载 PDF
</button>`,
})
export class DownloadPDFButtonComponent implements OnInit {
@Input() pdfName: string = "";
constructor(
private _message: NzMessageService
) { }
ngOnInit(): void { }
downloadPDF(downloadElement) {
if (!downloadElement) {
return;
}
const options = {
useCORS: true,
allowTaint: true
};
const A4_WIDTH = 592.28;
const A4_HEIGHT = 841;
html2canvas(downloadElement, options).then(canvas => {
const contentWidth = canvas.width;
const contentHeight = canvas.height;
// 一页pdf显示html页面生成的canvas高度;(A4)
const pageHeight = contentWidth / A4_WIDTH * A4_HEIGHT;
// 未生成pdf的html页面高度
let leftHeight = contentHeight;
let position = 0;
const imgWidth = A4_WIDTH;
const imgHeight = A4_WIDTH / contentWidth * contentHeight;
const contentDataURL = canvas.toDataURL('image/jpeg', 1.0);
let pdf = new jspdf('p', 'pt', 'a4'); // A4 size page of PDF
if (leftHeight < pageHeight) {
pdf.addImage(contentDataURL, 'JPEG', 0, 0, imgWidth, imgHeight);
} else {
while (leftHeight > 0) {
pdf.addImage(contentDataURL, 'JPEG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight;
position -= A4_HEIGHT;
//避免添加空白页
if (leftHeight > 0) {
pdf.addPage();
}
}
}
pdf.save(this.pdfName + '.pdf'); // Generated PDF
}).catch((e) => { });
}
}
@NgModule({
imports: [CommonModule, NzButtonModule, NzIconModule],
exports: [DownloadPDFButtonComponent],
declarations: [DownloadPDFButtonComponent]
})
export class DownloadPDFModule { }
父组件中引用子组件, 父组件中调用子组件的方法 downloadPDF
parentPdf.html
<download-pdf-button [pdfName] = "'报告的名字'" (click)="downloadPDF('domToPdfId')">
</download-pdf-button>
<div nz-row id = "domToPdfId">
//balabalabala
<table>
//balabalabala
</table>
</div>
parentPdf.ts
import { Component, OnInit, ElementRef, ViewChild } from '@angular/core';
// 引用子组件
import { DownloadPDFButtonComponent } from '../../../common/download/download-pdf-button';
export class ParentPdfComponent implements OnInit {
@ViewChild(DownloadPDFButtonComponent)
public downloadPdfComponent!: DownloadPDFButtonComponent;
downloadPDF(id) {
const downloadElement = this.element.nativeElement.querySelector("#" + id);
this.downloadPdfComponent.downloadPDF(downloadElement);
}
}
分页截断文字解决方法
上面的代码就能实现你想要的 DOM 节点下载,很简单。但是,页面数据很多的时候,超过 A4 大小,他就会分页,分页有时候会在有文字的地方截断,这样肯定是不行的。我其实觉得行,但是测试觉得不行,那就不行。我参考了资下面这个博主的思路,根据我们页面的布局又改了一点点。 参考:blog.csdn.net/Sandy_zhi/a… 整个思路大概就是:
- 在每个不可以被截断的节点添加一个标识类,然后遍历每一个标识类的节点。
- 根据 DOM 的长度和 A4 长度的比例 * A4 高度就得到 DOM 对应一页 A4 的高度。
- 然后在遍历节点的时候,判断该节点的头部和尾部是否在一个页面
const topPageNum = Math.ceil((wholeNodes[i].offsetTop + moreHeight) / pageHeight);
const bottomPageNum = Math.ceil((wholeNodes[i].offsetTop + moreHeight + wholeNodes[i].offsetHeight) / pageHeight);
if (bottomPageNum !== topPageNum)
博主的代码里面是没有 moreHeigt 的,但是因为我的页面是由一部分的栅格布局 和 下面4 个 table 组成的,标识类是加到每个 tr 上的, offsetTop 不是到整个 DOM 的高度而是到其父元素的高度, 所以计算topPageNum和bottomPageNum 的时候得加上它上面DOM 的高度。 4. 不在一个页面的话就在改节点前增加一个空白的 tr。
最终的down-pdf-button.ts代码
import { Component, Input, OnInit, NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { NzMessageService, NzButtonModule, NzIconModule } from 'ng-zorro-antd';
import jspdf from 'jspdf';
import html2canvas from 'html2canvas';
@Component({
selector: 'download-pdf-button',
template:
`<button class="app-button right" nz-button [nzType]="'primary'">
<i nz-icon nzType="download" nzTheme="outline"></i>下载 PDF
</button>`,
})
export class DownloadPDFButtonComponent implements OnInit {
@Input() pdfName: string = "";
constructor(
private _message: NzMessageService
) { }
ngOnInit(): void { }
downloadPDF(downloadElement, moreHeight) {
if (!downloadElement) {
return;
}
const options = {
useCORS: true,
allowTaint: true
};
// 避免笔下误 灯下黑 统一写
const A4_WIDTH = 592.28;
const A4_HEIGHT = 841;
let pageHeight = (downloadElement.offsetWidth / A4_WIDTH) * A4_HEIGHT;
// 将所有不允许被截断的元素进行处理
//let wholeNodes: any = document.querySelectorAll('.whole-node');
let wholeNodes: any = document.querySelectorAll('[class*=whole-node]');
for (let i = 0; i < wholeNodes.length; i++) {
let className= wholeNodes[i].className.split(" ")[0];
//两个页面都用到了这个子组件并且两个 pdf 的布局不一样
//page Two我用ng-for遍历实现的 table,每个table长度不一样,所以他们的标识类用 whole-node-1 ... whole-node-4 来表示方便处理
if (this.pdfName.indexOf("pageTwo") > -1 && className.split("-")[2] > 1) {
if (className !== wholeNodes[i-1].className.split(" ")[0]) {
moreHeight = moreHeight + wholeNodes[i-1].offsetTop + 97;
if (className.split("-")[2] < 4) {
moreHeight = moreHeight + wholeNodes[i-1].offsetHeight
}
}
}
const topPageNum = Math.ceil((wholeNodes[i].offsetTop + moreHeight) / pageHeight);
const bottomPageNum = Math.ceil((wholeNodes[i].offsetTop + moreHeight + wholeNodes[i].offsetHeight) / pageHeight);
if (bottomPageNum !== topPageNum) {
//说明该dom会被截断
// 2、插入空白块使被截断元素下移
let divParent = wholeNodes[i].parentNode;
let newBlock = document.createElement('tr')
newBlock.className = 'emptyDiv'
// 3、计算插入空白块的高度 可以适当流出空间使得内容太靠边,根据自己需求而定
let _H = topPageNum * pageHeight - wholeNodes[i].offsetTop - moreHeight;
newBlock.style.height = _H + 50 + 'px'
divParent.insertBefore(newBlock, wholeNodes[i])
}
}
html2canvas(downloadElement, options).then(canvas => {
let emptyDivs = document.querySelectorAll('.emptyDiv');
for (let i = 0; i < emptyDivs.length; i++) {
emptyDivs[i].parentNode.removeChild(emptyDivs[i])
}
const contentWidth = canvas.width;
const contentHeight = canvas.height;
// 一页pdf显示html页面生成的canvas高度;(A4)
const pageHeight = contentWidth / A4_WIDTH * A4_HEIGHT;
// 未生成pdf的html页面高度
let leftHeight = contentHeight;
let position = 0;
const imgWidth = A4_WIDTH;
const imgHeight = A4_WIDTH / contentWidth * contentHeight;
const contentDataURL = canvas.toDataURL('image/jpeg', 1.0);
let pdf = new jspdf('p', 'pt', 'a4'); // A4 size page of PDF
if (leftHeight < pageHeight) {
pdf.addImage(contentDataURL, 'JPEG', 0, 0, imgWidth, imgHeight);
} else {
while (leftHeight > 0) {
pdf.addImage(contentDataURL, 'JPEG', 0, position, imgWidth, imgHeight)
leftHeight -= pageHeight;
position -= A4_HEIGHT;
//避免添加空白页
if (leftHeight > 0) {
pdf.addPage();
}
}
}
pdf.save(this.pdfName + '.pdf'); // Generated PDF
}).catch((e) => { });
}
}
@NgModule({
imports: [CommonModule, NzButtonModule, NzIconModule],
exports: [DownloadPDFButtonComponent],
declarations: [DownloadPDFButtonComponent]
})
export class DownloadPDFModule { }
放上 page two 的部分 html 代码
<div *ngFor="let table of tableInfo">
<div nz-col [nzLg]="{span:24}" [nzXl]="{span: 24}" class = "outline">
<div [class] = "table.pdfClass" ><h3>{{table.title}}</h3></div>
</div>
<div nz-col [nzLg]="{span:24}" [nzXl]="{span: 24}" class = "outline" >
<div *ngIf="table['tableData'].length > 0; else noTableData" class = "bottom">
<nz-table
[nzLoading]="loading"
[nzShowPagination]="'false'"
[nzFrontPagination]="false"
[nzData]="table.tableData"
class = "cursor">
<thead>
<tr [class] = "table.pdfClass">
<th *ngFor="let header of table.tableHeader">{{header}}</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let data of table.tableData" [class] = "table.pdfClass">
<td *ngFor="let name of table.tableName">{{data[name] | undefinedNullTransform}}</td>
</tr>
</tbody>
</nz-table>
</div>
<ng-template #noTableData>
<div class = "bottom">
<p>{{table.noInfo}}</p>
</div>
</ng-template>
</div>
</div>
关于这部分逻辑的代码由于着急提测,所以写的更差劲。如果有更好的解决办法或者代码优化的方案麻烦指教~