简介
后端根据HTML生成PDF的常见的计划首要有wkhtmltopdf,Headless Chrome, andLibreOffice 三种。
本项目运用 nuxt3 + puppeteer 在服务器建立一个服务, 作为客户端和后端之间的中间层, 担任烘托生成PDF的页面,发动一个Headless Chrome用于生成PDF。
之所以叫中间层,能够看以下流程对比:
在这里,nuxt3是一个在客户端和后端之间,专门用于生成PDF的服务,因此我称为中间层服务
代码和装置
代码仓库: nuxt3_puppeteer_pdf_app: nuxt3 + puppeteer 作为中间层用于生成PDF文件 (gitee.com)
运转:
# pnpm
pnpm install --shamefully-hoist
npm run dev
运转作用:
生成作用:
测验中,生成90+页的PDF大约需求7-12S不等, 不过我的电脑比较老旧了。以实践为准。
代码目录结构和详解
中心代码展现:
report.js
let handlingCount = 0; // 正在处理的恳求数
let global = {}; // 全局数据
import puppeteer from "puppeteer";
import fs from 'fs';
import dayjs from 'dayjs';
//import axios from "axios";
async function launchBrowser() {
try {
const browser = await puppeteer.launch({
/* args: ['--no-sandbox', '--disable-setuid-sandbox', '--enable-accelerated-2d-canvas', '--enable-aggressive-domstorage-flushing'], */
args: [
'–disable-gpu',
'–disable-dev-shm-usage',
'–disable-setuid-sandbox',
'–no-first-run',
'–no-sandbox',
'–no-zygote',
'–single-process'
],
ignoreHTTPSErrors: true,
headless: true,
timeout: 60000,
});
const wsAddress = browser.wsEndpoint();
const w_data = Buffer.from(wsAddress);
global.wsa = w_data;
global.browser = browser;
console.log('chrome内核发动成功')
} catch (e) {
console.log(e)
}
}
launchBrowser();
async function getNewPage() {
const browser = await puppeteer.connect({
browserWSEndpoint: global.wsa
});
console.log('open Page')
return browser.newPage();
}
export default eventHandler(async(req, res) => {
// 连接数 + 1
++handlingCount;
console.log(`正在处理的连接数${handlingCount}`)
// 未超越最大连接数,进行生成
if (handlingCount <= 12) {
const startTime = dayjs(); // 打印开始时刻
const page = await getNewPage(); // 敞开一个新的标签页
await page.goto('http://localhost:3000/pdf', { waitUntil: 'networkidle0' }) // 等待响应加载
// 页头模板, 概况检查 http://puppeteerjs.com/#?product=Puppeteer&version=v19.2.2&show=api-pagepdfoptions
const header = `
<div>
<div>
<span> 我是页头 </span>
<span>
作者: <a href="https:///user/4332493267283560" type="primary"> Damon </a>
</span>
</div>
</div>`
// 页尾模板, 概况检查 http://puppeteerjs.com/#?product=Puppeteer&version=v19.2.2&show=api-pagepdfoptions
const footer = `
<div>
<div>
<span>
<span> 我是左页脚 </span>
</span>
<span>
<span> 我是右页脚 </span>
</span>
</div>
<div>
第 <span class="pageNumber"></span> 页
</div>
</div>`
// PDF尺度设置
const pdfConfig = {
//纸张尺度
format: 'A4',
//打印背景,默以为false
//printBackground: true,
/* width: '1300px',
height: '1848px', */
//不展现页眉
margin: {
top: '110px',
bottom: '150px',
left: '20px',
right: '30px',
},
/* format: {
width: 550
}, */
// scale: 0.99,
displayHeaderFooter: true, // 是否展现页头页脚
preferCSSPageSize: true, // 页面优先级声明CSS
printBackground: true, // 是否打印背景,CSS
footerTemplate: footer, // 页尾模板
headerTemplate: header // 页眉模板 /* (await axios.get('http://localhost:3000/header')).data */
}
await page.addStyleTag({
content: '@page { size: 1588px 2246px; }', // 设置页面高度和宽度
})
const result = await page.pdf(pdfConfig); // 生成 PDF
const endTime = dayjs(); // 获取完毕时刻
console.log(`生成时刻: ${ endTime.diff(startTime, 'second', true) }s`); // 生成耗时
fs.writeFileSync(`./${dayjs().format('YYYY_MM_DD_HH_mm_ss')}.pdf`, result) // 写入文件
await page.close(); // 封闭标签页
--handlingCount;
return {
code: 200,
message: '回来成功',
blob: result
}
}
// 超越最大处理任务则回来提示
else {
--handlingCount;
console.log('当前人数过多,请稍后再测验')
return {
code: 202,
message: '当前人数过多,请稍后再测验'
}
}
})
发动了一个Headless Chrome, 每次恳求该接口,判断是否超越最大的窗口数,否则新建一个标签页page
,再去恳求 /pdf 页面,/pdf 烘托响应前先去恳求 /api/data的接口(模仿后端数据接口),获取数据烘托回来页面,page
再经过page.pdf
生成PDF文件流回来。
/api/data 接口 这个接口模仿了后端接口回来的数据,
export default eventHandler(async (req, res) => {
return {
tableCount: 10, // table组件个数
hasCover: true, // 是否有封面
htmlCount: 10, //富文本组件个数
waterMark: '我是水印'
}
})
关于代码的目录详解检查nuxt3的详细文档: Nuxt 3 – 中文文档 (nuxtjs.org.cn)
关于生成pdf的详细参数能够检查 puppeteer 的详细文档: Page (puppeteerjs.com)
在这一接口中定义了一个全局变量,用于控制Headless Chrome能够一起打开的标签页(即一起生成PDF的恳求数, 毕竟是比较耗费内存的操作)的数量,超出时提示请稍等。
在pdf.vue中烘托的页面,首要经过CSS特点page-break-after
,page-break-before
,page-break-inside
。来控制特点。
在puppeteer抓取页面的时候,经过设置@page { size: 1588px 2246px; }
来规则了烘托页面的基础宽高。
关于水印组件waterMark.vue
:
则是经过fixed
布局来完成水印的作用。
<template>
<div v-if="content" style="position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%) rotate(-45deg); font-size: 84px; color: #666; font-weight: bold; opacity: 0.6;">
{{ content }}
</div>
</template>
<script setup>
const props = defineProps(['content']);
</script>
总体来说是个挺简易的demo,模仿了客户端恳求和恳求后端, 烘托页面, 生成PDF这一套流程。假如有更多的问题,欢迎谈论区交流。
关于HTML生成PDF的前后端计划的分析
纯前端计划:
纯前端的计划,存在着浏览器环境依靠或一定的局限性, 一定程度上难以做到多端导出一致(也许能够,但是过于繁复)。 以下是测验过的计划
- printjs/window.print()
经过调取浏览器原生的打印功用进行打印,缺陷: 关于自定义页眉页脚的自定义不友爱。 需求用户手动打印/进行微调,对用户不行友爱。APP端可能无法打印。
-
jsPDF + html2Canvas
关于这个操作,我上一篇文章有详细测验:jsPDF + html2canvas A4分页切断 完美解决计划(含代码 + 案例) – ()
根据这两个库较为成熟的解决计划有: hiprint.io
完成是经过html2canvas将HTML元素 转化 canvas 再转化成 JPED/PNG, 再经过jsPDF生成PDF文件。
长处: 生成契合完好契合样式的PDF,所见即所得,页头页尾自定义化高, 水印能够经过fixed布局元素生成。
缺陷:需求手动核算分页点。 由所以经过转化成图片来生成PDF, 关于a标签的链接元素,需求手动记载坐标、链接,再调用jsPDF的API去进行标记。 操作繁琐,并且各类浏览器关于canvas的高度存在约束,当转化为canvas的HTML元素过高/宽,canvas超出浏览器约束后,无法转化图片,生成的PDF便是一个空白/纯黑的文档。 能够经过切割HTML元从来躲避这个问题,但是操作会更加繁复。
后端计划:
运用后端生成PDF再传输到客户端,能够确保生成的PDF与客户端的环境无关,能够完成多端一致。 我了解到比较常用的是以下三种: wkhtmltopdf,Headless Chrome, LibreOffice 。
-
wkhtmltopdf:
这个是后端生成HTML的经典解决计划, 能够HTML文本/HTML链接生成PDF文件, 页眉页脚也同理。 网上各种言语的二次开发也有,完成概况能够查找自行了解。
首要讲一下缺陷:
因为过于经典, wkhtmltopdf 关于一些比较 “新” 的CSS3特点并不支撑, 例如flex布局就不支撑, HTML内要运用float布局代替。
-
puppeteerjs: (Headless Chrome)
Puppeteer v19.2.0 (puppeteerjs.com)
puppeteerjs 在服务端发动了一个无头(headless)的chrome内核,能够进行页面操作和生成PDF。 经过发动一个标签页,数据加载选然后经过pdf接口进行生成。
比起wkhtmlpdf长处是,关于CSS样式的支撑与你启用的chrome内核相关联,能够对页面进行操作。等
缺陷是可能存在很大的内存耗费 (实践出产环境中没有测验过)。 测验中,生成100页pdf所需大约3s,内存占用最大飙升至200M,假如并发数过大,不知道会不会存在问题。
-
LibreOffice
还没有用过,详细不太了解, 有用过的朋友能够分享一下。