前端实现网页转PDF矢量文件,高清还原网页内容
前端:Vue3
后端:Node.js + Express 接口
核心 PDF 引擎:Puppeteer(谷歌 Chrome 官方无头浏览器)
中文 100% 不乱码
图片 100% 显示
样式 1:1 还原
A4 自动分页,完美排版
文字可选中,矢量高清
✅ 实现逻辑
前端把整个网页 HTML的Dom元素传给 Node.js 后端
后端用 Puppeteer 打开这个 HTML
直接输出 标准 A4、无乱码、完整内容 PDF
返回给前端下载
依赖安装:
npm i puppeteer npm i express前端核心代码:别问其他的,照抄就行,样式不对,你就自己调,调不了问AI
consthandleExportPDF=async()=>{try{awaitnextTick();awaitnewPromise(resolve=>setTimeout(resolve,800));constdom=pdfContent.value;if(!dom)returnalert("内容未渲染完成");// ==============================================// ✅ 1. Echarts转图片// ==============================================constcanvases=dom.querySelectorAll("canvas");canvases.forEach((canvas)=>{try{constimg=newImage();img.src=canvas.toDataURL("image/png");img.style.width=canvas.offsetWidth+"px";img.style.height=canvas.offsetHeight+"px";if(canvas.parentNode){canvas.parentNode.replaceChild(img,canvas);}}catch(e){}});// ==============================================// ✅ 2. 修复所有<img>路径 + 转base64// ==============================================constimgs=dom.querySelectorAll("img");for(constimgofimgs){try{// 把 @/assets 转成可访问路径if(img.src.includes('@/assets/')){img.src=img.src.replace('@/assets/','http://localhost:5173/src/assets/');}// 转base64constblob=awaitfetch(img.src).then(res=>res.blob());constbase64=awaitnewPromise<string>((resolve)=>{constreader=newFileReader();reader.onload=()=>resolve(reader.resultasstring);reader.readAsDataURL(blob);});img.src=base64;img.style.display='block';img.style.visibility='visible';}catch(e){}}// ==============================================// ✅ 3. 【关键修复】把 CSS 背景图全部转 base64// ==============================================constelementsWithBg=dom.querySelectorAll('*[style*="background"], *[class*="one"] .header');for(constelofelementsWithBg){constcomputed=window.getComputedStyle(el);constbgImage=computed.backgroundImage;if(!bgImage||bgImage==='none')continue;// 提取图片URLconsturlMatch=bgImage.match(/url\(["']?([^"']+)["']?\)/);if(!urlMatch||!urlMatch[1])continue;letimgUrl=urlMatch[1];// 本地路径修复if(imgUrl.includes('/src/assets/')||imgUrl.includes('@/assets/')){imgUrl=imgUrl.replace('@/assets/','http://localhost:5173/src/assets/');}// 转 base64 并替换try{constblob=awaitfetch(imgUrl).then(res=>res.blob());constbase64=awaitnewPromise<string>((resolve)=>{constreader=newFileReader();reader.onload=()=>resolve(reader.resultasstring);reader.readAsDataURL(blob);});(elasHTMLElement).style.backgroundImage=`url(${base64})`;}catch(e){}}// ==============================================// ✅ 4. 收集样式// ==============================================conststyleSheets=Array.from(document.styleSheets).map(sheet=>{try{returnArray.from(sheet.cssRules).map(r=>r.cssText).join('');}catch(e){return'';}}).join('');consthtml=`<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <style>${styleSheets}* { font-family: "Microsoft YaHei", sans-serif !important; } body { background: #fff; margin:0; padding:0; } </style> </head> <body>${dom.outerHTML}</body> </html>`;// ==============================================// 请求后端生成PDF// ==============================================const{data}=awaitaxios.post("http://localhost:3001/export-pdf",{html,filename:"学习报告"},{responseType:"blob"});constblob=newBlob([data],{type:"application/pdf"});consturl=URL.createObjectURL(blob);consta=document.createElement("a");a.href=url;a.download="学习报告.pdf";a.click();URL.revokeObjectURL(url);message.success("PDF导出成功")}catch(err){console.error("导出失败:",err);message.error("导出失败")}};nodejs后端:安装好依赖后,复制这段代码到你创建的文件中,启动后端服务就行了
// const express = require('express');// const puppeteer = require('puppeteer');importexpressfrom'express'importpuppeteerfrom'puppeteer'constapp=express();app.use(express.json({limit:'50mb'}));// 跨域配置app.use((req,res,next)=>{res.setHeader('Access-Control-Allow-Origin','*');res.setHeader('Access-Control-Allow-Methods','POST,OPTIONS');res.setHeader('Access-Control-Allow-Headers','Content-Type');if(req.method==='OPTIONS')returnres.sendStatus(200);next();});app.post('/export-pdf',async(req,res)=>{try{const{html,filename}=req.body;// 🔥 1. 关键修复:中文文件名必须编码,否则 Header 报错constencodedFilename=encodeURIComponent(`${filename}.pdf`);// 🔥 2. 启动 Chrome,使用 Mac 本地 Chrome 路径,避免下载失败constbrowser=awaitpuppeteer.launch({headless:true,// 不需要可视化界面executablePath:'/Applications/Google Chrome.app/Contents/MacOS/Google Chrome',// Mac 本地 Chrome 路径args:['--no-sandbox','--disable-dev-shm-usage','--disable-setuid-sandbox','--disable-gpu']});constpage=awaitbrowser.newPage();// await page.setContent(html, {// waitUntil: "networkidle0", // 👈 必须加!等所有样式加载完// });// await page.emulateMedia("print"); // 👈 必须加!让PDF用打印样式awaitpage.waitForFunction(()=>{returnArray.from(document.querySelectorAll('img')).every(img=>img.complete);},{timeout:10000});// 最多等10秒,确保所有图片加载// 🔥 3. 设置超时时间,延长等待awaitpage.setContent(html,{waitUntil:'networkidle0',timeout:60000});awaitpage.addStyleTag({content:`table, tr, td, th { page-break-inside: avoid !important; break-inside: avoid !important; } * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } .header { display: flex !important; align-items: center !important; } .ml-2 { margin-left: 8px !important; } .page-title { line-height: 120px !important; font-size: 36px !important; } .info { display: grid !important; grid-template-columns: repeat(3, 1fr) !important; } /* 强制图片显示,防止加载失败后隐藏 */ img { display: block !important; visibility: visible !important; width: auto !important; height: auto !important; }`});// 生成 PDFconstpdfBuffer=awaitpage.pdf({width:'210mm',// A4 宽度不变height:'297mm',// 从 297mm 加大到 330mm,多装内容format:'A4',printBackground:true,// 必须打印背景margin:{top:'10mm',bottom:'10mm',left:'10mm',right:'10mm'}});awaitbrowser.close();// 🔥 4. 返回响应,使用编码后的文件名res.setHeader('Content-Type','application/pdf');res.setHeader('Content-Disposition',`attachment; filename*=UTF-8''${encodedFilename}`);// 这种写法支持中文下载res.send(pdfBuffer);}catch(err){console.error('PDF 生成失败:',err);res.status(500).send('PDF 生成失败,请检查页面内容或服务状态。');}});app.listen(3001,()=>{console.log('✅ PDF 服务已启动:http://localhost:3001');});