前言 官方文档
https://pptr.nodejs.cn/
添加依赖 1 npm install puppeteer@24.8.2 pdf-lib@1.17.1
其中
puppeteer 把HTML转为PDF
pdf-lib 把封面、目录、正文的PDF进行合并
安装慢可以使用
1 2 npm install -g cnpm --registry=https://registry.npmmirror.com cnpm install puppeteer@24.8.2
puppeteer和puppeteer-core 安装puppeteer,会下载Chrome可能时间比较长。
puppeteer-core 不会自动下载 Chromium,可以结合系统中已有的 Chrome浏览器使用。
puppeteer 1 2 3 4 5 6 7 8 9 10 11 const puppeteer = require ('puppeteer' );const browser = await puppeteer.launch ({ headless : "new" , timeout : 1000 * 120 , args : [ "--no-sandbox" , "--disable-setuid-sandbox" , "--disable-dev-shm-usage" , ], });
puppeteer-core 1 npm i puppeteer-core@24.8.2
这种初始化的时候要指定chrome.exe路径,使用edge不行。
1 2 3 4 5 6 7 8 9 10 11 12 13 const puppeteer = require ('puppeteer-core' );const chromePath = "C:\\Program Files\\Google\\Chrome\\Application\\chrome.exe" ;const browser = await puppeteer.launch ({ executablePath : chromePath, headless : "new" , timeout : 1000 * 120 , args : [ "--no-sandbox" , "--disable-setuid-sandbox" , "--disable-dev-shm-usage" , ], });
整体示例 页面样式 1 2 3 4 5 6 7 8 9 .page { width : 210mm ; height : 297mm ; page-break-after : always; background-color : white; margin : 0 auto 20px ; padding : 40px 20px ; position : relative; }
注意
A4是201mm*297mm
导出脚本 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 const puppeteer = require ("puppeteer" );const fs = require ("fs" ).promises ;(async () => { const browser = await puppeteer.launch ({ headless : "new" }); const page = await browser.newPage (); await page.goto ("http://localhost:5173/cover.html" , { waitUntil : "networkidle2" , }); await page.pdf ({ path : "temp_cover.pdf" , format : "A4" }); await page.goto ("http://localhost:5173/#/report_school" , { waitUntil : "networkidle0" , }); await page.waitForSelector (".page" ); await new Promise ((resolve ) => setTimeout (resolve, 1000 )); const headers = await page.evaluate (() => { const result = []; let pageNumber = 1 ; document .querySelectorAll (".page" ).forEach ((pageItem ) => { pageItem.querySelectorAll ("h1, h2, h3" ).forEach ((header ) => { const id = "header-" + Date .now () + "-" + result.length ; header.id = id; result.push ({ text : header.textContent , level : header.tagName , id : id, page : pageNumber, }); }); pageNumber++; }); return result; }); await page.pdf ({ path : "temp_content.pdf" , format : "A4" , printBackground : true , displayHeaderFooter : true , headerTemplate : ` <div style="position: absolute;top:0;z-index:999;font-size: 12px; width: 100%; height:40px;line-height:40px; text-align: left;padding:0 30px;"> 码客说 </div> ` , footerTemplate : ` <div style="position: absolute;bottom:0;z-index:999;font-size: 12px; width: 100%; height:40px;line-height:40px; text-align: center;"> <span class="pageNumber"></span> / <span class="totalPages"></span> </div> ` , margin : { top : "0" , bottom : "0" , }, }); await page.goto ( "data:text/html," + encodeURIComponent (` <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>目录</title> <style> body { font-family: Arial, sans-serif; padding: 2cm; } h1 { text-align: center; } .toc-item { margin: 0.5cm 0; } .toc-item a { text-decoration: none; color:#333; } .toc-level-1 { font-size: 16px; font-weight: bold; } .toc-level-2 { font-size: 14px; margin-left: 1cm; } .toc-level-3 { font-size: 12px; margin-left: 2cm; } .toc-page { float: right; } </style> </head> <body> <h1>目录</h1> ${headers .map( (header) => ` <div class="toc-item toc-level-${header.level.replace("H" , "" )} "> <a href="#${header.id} ">${header.text} </a> <span class="toc-page">${header.page} </span> </div> ` ) .join("" )} </body> </html> ` ), { waitUntil : "networkidle0" } ); await page.pdf ({ path : "temp_toc.pdf" , format : "A4" }); const { PDFDocument } = require ("pdf-lib" ); const coverPdf = await PDFDocument .load (await fs.readFile ("temp_cover.pdf" )); const tocPdf = await PDFDocument .load (await fs.readFile ("temp_toc.pdf" )); const contentPdf = await PDFDocument .load ( await fs.readFile ("temp_content.pdf" ) ); const mergedPdf = await PDFDocument .create (); const [coverPage] = await mergedPdf.copyPages (coverPdf, [0 ]); mergedPdf.addPage (coverPage); const tocPages = await mergedPdf.copyPages (tocPdf, tocPdf.getPageIndices ()); tocPages.forEach ((page ) => mergedPdf.addPage (page)); const contentPages = await mergedPdf.copyPages ( contentPdf, contentPdf.getPageIndices () ); contentPages.forEach ((page ) => mergedPdf.addPage (page)); const mergedPdfBytes = await mergedPdf.save (); await fs.writeFile ("final_document.pdf" , mergedPdfBytes); await fs.unlink ("temp_cover.pdf" ); await fs.unlink ("temp_toc.pdf" ); await fs.unlink ("temp_content.pdf" ); await browser.close (); console .log ("PDF生成完成!" ); })();
常用设置 初始化 1 2 3 4 5 6 7 8 9 10 const browser = await puppeteer.launch ({ headless : "new" , timeout : 1000 * 120 , args : [ "--no-sandbox" , "--disable-setuid-sandbox" , "--disable-dev-shm-usage" , ], }); const page = await browser.newPage ();
注意
上面的参数中的args建议添加,添加后在Docker容器中才能正常运行。并且也不影响在非容器内的运行。
加载页面 1 2 3 await page.goto ("http://localhost:5173/cover.html" , { waitUntil : "networkidle2" , });
waitUntil的值
"load" 等待页面的 load 事件触发(即 HTML 文档完全加载并解析完成)。此时可能仍有图片等资源在加载。
"domcontentloaded" 等待页面的 DOMContentLoaded 事件触发(即 HTML 解析完成,无需等待样式表、图片等资源)。
"networkidle0" 等待页面网络请求数量减少到 0 个 (即没有任何网络活动)并持续 500 毫秒 。这是最严格的策略,确保所有资源都加载完成。
"networkidle2" 如上所述,等待网络请求数量 ≤2 并持续 500 毫秒。
等待页面加载完成 等待某个元素完成渲染
1 2 await page.waitForSelector (".report_finish" );
延迟
1 2 await new Promise ((resolve ) => setTimeout (resolve, 1000 ));
下面的这种方式我这测试的无效,目前还是使用第一种方法,等加载完在页面上添加一个元素。
页面状态
1 2 3 await page.waitForFunction (() => { return window .loadingComplete === true ; });
控制生成PDF区域 你可以在页面中添加一个 <div> 容器,只包含你想导出为 PDF 的内容,并通过 CSS 的 @media print 规则隐藏其他部分。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <style> @media print { body * { visibility : hidden; } .page_box , .page_box * { visibility : visible; } } </style> <div class="page_box"> <!-- 你希望生成 PDF 的内容 --> </div >
设置后可以使用打印快捷键Ctrl + P查看效果。
换页控制 在使用 Puppeteer 生成 PDF 时,如果你希望在 HTML 中某个位置强制换页(分页) ,可以通过 CSS 的分页控制属性 来实现。这是最标准、最可靠的方式。
方法:使用 CSS page-break-* 或 break-* 属性
分页控制 现代浏览器(包括 Chromium,即 Puppeteer 使用的引擎)支持以下 CSS 属性来控制分页:
元素前换页 1 2 3 4 5 .page-break-before { break-before : page; page-break-before : always; }
元素后换页 1 2 3 4 5 .page-break-after { break-after : page; page-break-after : always; }
避免元素被分页打断 避免元素被分页打断(保持完整在一页)
1 2 3 4 .no-break { break-inside : avoid; page-break-inside : avoid; }
注意:
推荐同时使用新标准(break-*)和旧标准(page-break-*)以提高兼容性。
示例 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 <!DOCTYPE html > <html > <head > <meta charset ="utf-8" > <style > @media print { .page-break-before { break-before : page; page-break-before : always; } .page-break-after { break-after : page; page-break-after : always; } body { font-family : Arial; line-height : 1.6 ; } } </style > </head > <body > <h1 > 第一页内容</h1 > <p > 这里是第一页的内容...</p > <div class ="page-break-before" > </div > <h1 > 第二页内容</h1 > <p > 这会出现在新的一页上。</p > <p > 再加一段内容。</p > <div class ="page-break-after" > </div > <h1 > 第三页内容</h1 > <p > 这是第三页。</p > </body > </html >
注意事项
不要对行内元素(如 <span>)使用分页属性 ,应使用块级元素(<div>, <section>, <h1> 等)。
如果页面有复杂布局(如 Flex/Grid),某些分页行为可能被忽略,建议测试效果。
break-* 是 CSS Paged Media Module Level 3 的标准,Chromium 支持良好。
最佳实践 直接把分页类加在你希望“新开一页”的标题上:
1 <h2 class ="page-break-before" > 新章节标题</h2 >
脚本传参 调用脚本传参
test_para.js
1 2 3 4 5 const args = process.argv .slice (2 );console .log ("接收到的参数:" , args);
调用
1 node test_para.js config=/data/test/paras.json other=show
可以看到接收的数组
接收到的参数: [ ‘config=/data/test/paras.json’, ‘other=show’ ]
为了方便可以转为对象
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 const args = process.argv .slice (2 );let paras = {};for (let i = 0 ; i < args.length ; i++) { const paraStr = args[i]; const index = paraStr.indexOf ('=' ); if (index !== -1 ) { const leftPart = paraStr.substring (0 , index); const rightPart = paraStr.substring (index + 1 ); paras[leftPart] = rightPart } } console .log ("接收到的参数:" , paras);
访问
1 node .\index.js name="zhangsan" age=8
结果
接收到的参数: { name: ‘zhangsan’, age: ‘8’ }
服务器环境 在 Linux 上安装 Puppeteer 时,需要确保系统已安装必要的依赖库:
Debian/Ubuntu 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 sudo apt-get update && sudo apt-get install -y \ ca-certificates \ fonts-liberation \ libappindicator3-1 \ libasound2 \ libatk-bridge2.0-0 \ libatk1.0-0 \ libc6 \ libcairo2 \ libcups2 \ libdbus-1-3 \ libexpat1 \ libfontconfig1 \ libgbm1 \ libgcc1 \ libglib2.0-0 \ libgtk-3-0 \ libnspr4 \ libnss3 \ libpango-1.0-0 \ libpangocairo-1.0-0 \ libstdc++6 \ libx11-6 \ libx11-xcb1 \ libxcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxrender1 \ libxss1 \ libxtst6 \ lsb-release \ wget \ xdg-utils
CentOS/RHEL 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 sudo yum install -y \ pango.x86_64 \ libXcomposite.x86_64 \ libXcursor.x86_64 \ libXdamage.x86_64 \ libXext.x86_64 \ libXi.x86_64 \ libXtst.x86_64 \ cups-libs.x86_64 \ libXScrnSaver.x86_64 \ libXrandr.x86_64 \ GConf2.x86_64 \ alsa-lib.x86_64 \ atk.x86_64 \ gtk3.x86_64 \ ipa-gothic-fonts \ xorg-x11-fonts-100dpi \ xorg-x11-fonts-75dpi \ xorg-x11-fonts-cyrillic \ xorg-x11-fonts-misc \ xorg-x11-fonts-Type1 \ xorg-x11-utils
NodeJS 1 2 3 4 5 6 7 8 9 10 11 12 13 14 curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.3/install.sh | bash source ~/.nvm/nvm.shnvm install 18.20.8 nvm list nvm use 18.20.8
Docker方式 在Linux上安装依赖和NodeJS会需要别的依赖,比较麻烦,这里提供使用Docker的方式构建
脚本 Dockerfile
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 FROM node:18 -busterENV TZ=Asia/ShanghaiRUN sed -i 's/deb.debian.org/mirrors.tuna.tsinghua.edu.cn/g' /etc/apt/sources.list && \ sed -i 's|security.debian.org/debian-security|mirrors.tuna.tsinghua.edu.cn/debian-security|g' /etc/apt/sources.list ENV LANG=C.UTF-8 ENV DEBIAN_FRONTEND=noninteractiveRUN apt update && \ apt install -y fonts-wqy-zenhei fonts-wqy-microhei fonts-noto-cjk && \ apt clean && \ rm -rf /var/lib/apt/lists/* RUN apt-get update RUN apt-get install -y \ wget \ gnupg \ ca-certificates \ fonts-liberation \ libappindicator3-1 \ libasound2 \ libatk-bridge2.0-0 \ libatk1.0-0 \ libc6 \ libcairo2 \ libcups2 \ libdbus-1-3 \ libexpat1 \ libfontconfig1 \ libgbm1 \ libgcc1 \ libglib2.0-0 \ libgtk-3-0 \ libnspr4 \ libnss3 \ libpango-1.0-0 \ libpangocairo-1.0-0 \ libstdc++6 \ libx11-6 \ libx11-xcb1 \ libxcb1 \ libxcomposite1 \ libxcursor1 \ libxdamage1 \ libxext6 \ libxfixes3 \ libxi6 \ libxrandr2 \ libxrender1 \ libxss1 \ libxtst6 \ lsb-release \ xdg-utils \ && rm -rf /var/lib/apt/lists/* WORKDIR /app COPY package*.json ./ RUN echo "registry=https://registry.npmmirror.com" > ~/.npmrc && \ echo "puppeteer_download_host=https://registry.npmmirror.com" >> ~/.npmrc RUN npm install -g cnpm --registry=https://registry.npmmirror.com RUN cnpm install COPY ./*.js . CMD ["node" , "index.js" ]
构建 1 docker build -t puppeteer-docker .
测试 调用容器内的JS
1 2 docker run --rm puppeteer-docker node index.js docker run --rm puppeteer-docker node html2pdf.js
调用Docker外的JS
1 2 3 4 5 docker run --rm -v $(pwd ):/app puppeteer-docker node /app/index.js docker run --rm -v "%cd%:/app" puppeteer-docker node /app/html2pdf.js
复制字体 1 2 3 4 5 6 cd C:\Windows\Fontscopy msyh.ttc %USERPROFILE%\Downloads copy msyhbd.ttc %USERPROFILE%\Downloads copy msyhl.ttc %USERPROFILE%\Downloads copy simhei.ttf %USERPROFILE%\Downloads