当前位置: 首页 > news >正文

前端实现网页转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');});
http://www.jsqmd.com/news/489397/

相关文章:

  • 我用 OpenClaw 7 天,砍掉了 80% 的重复沟通
  • 骑砍2霸主MOD开发(10)-游戏实例GameEntity
  • 为什么说数字化转型非常重要?2026企业级AI Agent与RPA选型实测指南
  • 前端面试基础知识整理【Day-11】
  • 贡献者必读:如何参与SIMP开源项目并提交高质量代码
  • 【雷达干扰】基于CFastICA交叉极化干扰对消-独立成分分析附Matlab代码
  • leetcode副产品:Deepseek老师讲HashSet(待阅读)
  • 【状态估计】基于卡尔曼滤波器实现月球陨石坑导航附Matlab代码
  • Spring Boot配置优先级详解
  • 【亲测免费】 探索知识图谱的力量: rahulnyk/knowledge_graph 项目详解
  • 基于微信小程序实现学生购电小程序管理系统【附项目源码】计算机毕业设计
  • 【数字信号调制】基于8相移键控8-PSK调制数字通信系统(含模拟噪声信道上的信号传输,包括调制、噪声添加、解调以及符号和比特错误率的性能评估)附Matlab代码
  • OpenCode 的 skills 网站相关信息
  • 好用的软件、网站、插件记录
  • JavaScript性能优化实战冶懒
  • 【资源分配】基于强化学习Q-Learning实现DSA认知无线网络资源分配附Matlab代码
  • 推荐:Jib — 容器化你的Java应用的新选择!
  • Spring全家桶框架篇
  • sebastian/code-unit核心组件解析:从ClassUnit到TraitMethodUnit
  • 粒子群算法PSO-AHP模型在综合评价中的构建及应用附Matlab代码
  • 2026年热门的高校就业指导中心方案厂家推荐:高校就业指导中心方案设备/高校就业指导中心方案开发/高校就业指导中心方案采购优质公司推荐 - 行业平台推荐
  • 华为eNSP三层交换机实验全解析
  • 消息队列篇
  • sql2o配置与实战:5分钟上手的数据库结果映射工具
  • 基于深度置信网络(DBN)与模糊神经网络(FNN)分类附Matlab代码
  • 猜数字小游戏来了~(冲冲冲!)
  • 基于决策树RGB图像分类附Matlab代码
  • SAP Fiori 图标体系实战:用 Icon Explorer、Virtual Element 与 Fiori Elements 提升业务识别效率
  • Nginx常见问题解决
  • PHing vs Make:PHP开发者必知的构建工具对比分析