先说结论:现在前端真可以一行代码,把几千页的 PDF 直接在浏览器里干出来,而且是可选中、可搜索的那种,不是截图糊糊的一张大图片,靠的就是这货——dompdf.js 新增的分页能力。
你肯定遇到过这种场景:产品突然拉个需求,要导出「几十页报表 PDF」,还要样式跟页面一样,页眉页脚有 logo、有页码,表格不能被切断,图片不能糊,文字还能复制。
传统做法大概就这几种:
html2canvas + jsPDF:先截图,再塞进 PDF,一放大就糊,文字不是真文字;jsPDF 手写排版:定位全靠算坐标,复杂布局写起来要怀疑人生;尤其是那种几十、上百页的大报表,你会发现前端能做的,要么糊,要么丑,要么写死人。
所以我第一次看到 dompdf.js 的分页功能的时候,内心 OS 就一句:这才像 202x 年的前端库干的事啊。
简单粗暴一点讲:
dompdf.js= 在浏览器里,把一个 DOM 节点「排版 ➜ 渲染 ➜ 导出」成矢量 PDF的库,支持分页、页眉页脚、加密压缩这些高级操作。
重点几个点:
它对外暴露的核心就是一个函数:dompdf(targetDom, options) => Promise<Blob>。
所以真的可以写出这种一行:
import dompdf from'dompdf.js';dompdf(document.querySelector('#report'), { pagination: true });这行就是「把 #report 这个 DOM 节点按分页规则变成 PDF Blob」的完整入口。
下载、上传、预览怎么搞,你再接在后面就行。
光说一行有点耍流氓,我们写一个最常见的「A4 报表导出」场景,你可以直接抄去项目里改一改就用。
HTML 里先准备一个容器,宽度一定要和纸张尺寸对应,这是分页能不能算准的关键:([codelove.tw][1])
<!-- 注意这个宽度:A4 宽度对应 794px --><divid="report"style="width: 794px; margin: 0 auto;"><!-- 你的表格 / 报表 / 合同内容都塞这里 --></div><buttonid="exportBtn">导出 PDF</button>JS 里这么写:
import dompdf from'dompdf.js';const btn = document.querySelector('#exportBtn');btn.addEventListener('click', async () => {const el = document.querySelector('#report');// 真正干活的就这一行:const blob = await dompdf(el, {pagination: true, // 开启分页format: 'a4', // 纸张大小compress: true, // 压缩一下体积(可选)pageConfig: { // 页眉页脚(可选)header: {content: '某某系统报表',height: 40,contentFontSize: 10,contentPosition: 'center', },footer: {content: '第${currentPage}页 / 共${totalPages}页',height: 40,contentFontSize: 10,contentPosition: 'center', }, }, });// 后面这些是常规的 blob 下载套路const url = URL.createObjectURL(blob);const a = document.createElement('a'); a.href = url; a.download = `report-${Date.now()}.pdf`;document.body.appendChild(a); a.click(); a.remove(); URL.revokeObjectURL(url);});你看,核心逻辑就那一行 dompdf(el, { ... }),其他都是打工仔给它收拾烂摊子。
dompdf.js 内部其实干了三大件事(为了方便理解,我稍微意译了一下官方的描述):
先把 DOM 解析成一个「带位置信息的树」
它会遍历你的页面,把每个元素的 width/height/left/top、样式、文本节点都算出来,弄成类似这样的结构:
{bounds: { left: 8, top: 0, width: 794, height: 1300 },elements: [ {bounds: { left: 8, top: 1000, width: 794, height: 300 },textNodes: [ {text: '这是一个文本节点',bounds: { left: 16, top: 1115, width: 300, height: 24 } } ], }, ],}按页高切片:该翻页就翻页
假设你 A4 的可用高度(扣掉页眉页脚)是 1123px,那它就会按类似的逻辑走一遍(伪代码):
functionshouldBreak(bounds, pageHeight) {return bounds.top + bounds.height > pageHeight;}div 整块高度超过一页,就拆成两块;top 重新减掉前面占的高度,保证每一页自己的坐标系是从 0 开始。按页循环,交给底层 PDF 引擎绘制
dompdf.js 底下是用 jsPDF 之类的东西去画图的(文本、矩形、图片等),分页之后就是:
pages.forEach((page, index) => {if (index > 0) jspdf.addPage();// 画页眉// 画内容// 画页脚});你不需要关心这些细节,真正有用的是:它已经帮你想好了大部分分页规则,能把复杂布局切得比较自然。
比如合同里的一个签名区域、一个完整的卡片、一张大图,你不希望它被拆成半张上一页半张下一页,这种时候可以在 DOM 上加一个标记属性,告诉 dompdf.js:这块别拆,宁可整体挪到下一页。
比如这样:
<divclass="card"divisionDisable><h3>用户信息</h3><p>姓名:XXX</p><p>证件号:YYYY</p><p>签名区:</p><!-- ... --></div>它在分割的时候会检测这个属性,发现你不让拆,就整块算一个元素,顶多把前面那一页空出一块,整体丢到下一页去,看起来会比「拦腰斩断」舒服很多。
说一千道一万,你真要搞「数千页」那种超大 PDF,还是得考虑几个现实问题。
内容越「图文并茂」,页数就越少
纯文本的 PDF,几千页没什么问题;如果每页都塞满大图,几十页可能就已经很大了。dompdf.js 自己也提到,这个跟文件总大小绑死的。
图片一定要配好跨域 & CORS
如果你的图片是 CDN / 其他域名的,记得:
dompdf(el, {pagination: true,useCORS: true,});同时服务端得配好 Access-Control-Allow-Origin,不然 PDF 里那块就是一片空白。
别忘了压缩 & 加密这些高级选项
dompdf.js 还支持一些高级参数,比如:
dompdf(el, {pagination: true,compress: true, // 压缩 PDF 体积encryption: {userPassword: '123456',ownerPassword: 'admin-xxx',userPermissions: ['print', 'copy'], // 限制打印 / 拷贝等 },});压一下体积,对几百页以上的 PDF 很有用。
导出时给个 loading,别让用户以为卡死了
生成几百页 PDF 还是挺吃 CPU 的,动辄好几秒,建议包一层:
asyncfunctionexportPdf() { showLoading('正在生成 PDF,请稍候...');try {const blob = await dompdf(/* ... */);// 下载逻辑 } catch (e) {console.error(e); alert('导出失败,请稍后重试'); } finally { hideLoading(); }}dompdf.js 这种库,本质上就是把你脑子里「排版」这件事自动化了:你给它 DOM,它帮你算坐标、控制分页、处理页眉页脚,前端这边只要关注页面长什么样就行,这一点跟之前自己手撸坐标的时代已经完全不是一个世界。
我自己在写这类东西的习惯,也是先从小 demo 开始,搞清楚几个最关键的问题:宽度怎么配、分页规则会不会坑、图片和字体能不能正常出来,然后再慢慢往项目里砌。风格上和之前写数据库那些技术踩坑记录是一脉相承的。
总之,如果你现在项目里还在用截图方式搞 PDF,真心可以抽半天,把 dompdf.js 的分页功能拉下来玩一玩,起码以后再遇到「全前端生成多页 PDF」这种需求,不至于第一反应就是「我能不能跟产品吵一架先」。
-END-
我为大家打造了一份RPA教程,完全免费:songshuhezi.com/rpa.html
🔥私藏精品🔥
虎哥作为一名老码农,整理了全网最全《前端资料合集》。总量高达650GB