目录
序章:当产品经理说 "我要在网页里看 PDF"
那天产品经理踩着风火轮冲进我工位,手里挥舞着一个 U 盘:"小顾啊,用户反馈说咱们系统里的 PDF 总让下载,体验太烂了!我要那种一点击就唰地出来,还能翻页、放大、搜文字的效果,今天就要!"
我看着他真诚(且带着一丝威胁)的眼神,默默打开了搜索引擎。彼时的我还不知道,这场 "网页看 PDF" 的修行,会让我从一个只会用window.open()的菜鸡,进化成能在面试里吹半小时牛的 "PDF 大师"。
本文就来复盘这场血与泪的战斗 —— 包含 3 种解决方案、10 行救命代码、8 个深坑预警,保证让你看完就能上手,顺便收获产品经理的膝盖。
一、史前时代:那些年我们用过的土办法
在正经解决方案出现前,前端 er 为了在网页里展示 PDF,简直把 "歪门邪道" 发挥到了极致。
1.1 iframe 直译法:简单到愚蠢
<iframe src="test.pdf" width="100%" height="800px">
您的浏览器不支持iframe,建议换个浏览器(比如扔掉IE)
</iframe>
这招的原理是利用浏览器自带的 PDF 渲染能力,就像把 PDF 扔给浏览器说 "哥,帮个忙"。优点是零代码,复制粘贴就能用;缺点是样式丑到爆,控制欲为零 —— 你既不能改背景色,也不能禁止下载,更别说加个水印了。
我司老系统用了三年这方案,直到有用户截图反馈:"你们网站的 PDF 查看器,跟我奶奶的老年机一样难用"。
1.2 转图片大法:舍本逐末的骚操作
有个后端大哥曾骄傲地跟我说:"我把 PDF 转成图片返回给你,你直接 img 标签不就完了?" 我当时居然觉得很有道理。
// 伪代码:后端转图片的悲惨世界
axios.get('/pdf-to-images?url=xxx').then(res => {
const images = res.data.images; // 一堆base64图片
images.forEach(img => {
document.body.innerHTML += `<img src="${img}" style="max-width:100%">`;
});
});
这方案在移动端适配时直接翻车:100 页的 PDF 转成 100 张图片,加载速度比蜗牛爬还慢,用户流量哗哗流,客服电话被打爆。更惨的是图片没法搜文字,产品经理看了我的实现,沉默着递给我一本《前端求生指南》。
二、PDF.js:前端渲染的救世主
就在我准备卷铺盖跑路时,隔壁工位的老前端扔给我一个链接:"试试 Mozilla 的 PDF.js,不好用你打我"。从此,我打开了新世界的大门。
2.1 什么是 PDF.js?
简单说,这是 Mozilla(火狐爸爸)开发的开源库,能让浏览器用纯 JS 解析 PDF—— 注意是纯 JS!也就是说,它不依赖浏览器自带的 PDF 引擎,从解析二进制流到渲染像素点,全靠自己硬刚。
就像一个自带厨师证的外卖员,不仅给你送餐,还能现场给你炒个菜。
2.2 入门三板斧:安装与基础使用
第一步:引库
可以直接用 CDN,也可以 npm 安装:
<!-- CDN引入 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.4.120/pdf.worker.min.js"></script>
这里有个坑:pdf.worker.js是负责解析 PDF 的 Worker 脚本,必须和主库一起引入,不然会报 "Missing PDF worker" 错误。就像买手机必须带充电器,不然开不了机。
第二步:准备 HTML 容器
需要一个 canvas 来画画(PDF 的每一页都是画出来的):
<div class="pdf-container">
<canvas id="pdfCanvas"></canvas>
</div>
<!-- 再加个工具栏 -->
<div class="toolbar">
<button id="prev">上一页</button>
<span id="pageNum"></span>
<button id="next">下一页</button>
</div>
第三步:核心渲染代码
这部分是灵魂,我加了详细注释:
// 获取DOM元素
const canvas = document.getElementById('pdfCanvas');
const ctx = canvas.getContext('2d');
const prevBtn = document.getElementById('prev');
const nextBtn = document.getElementById('next');
const pageNumEl = document.getElementById('pageNum');
// 全局变量
let pdfDoc = null; // PDF文档对象
let pageNum = 1; // 当前页码
const scale = 1.5; // 缩放比例
// 加载PDF
function renderPage(num) {
// 获取第num页
pdfDoc.getPage(num).then(page => {
// 设置canvas尺寸(考虑缩放)
const viewport = page.getViewport({ scale: scale });
canvas.height = viewport.height;
canvas.width = viewport.width;
// 渲染配置
const renderContext = {
canvasContext: ctx,
viewport: viewport
};
// 开始渲染
const renderTask = page.render(renderContext);
// 渲染完成后更新页码
renderTask.promise.then(() => {
pageNumEl.textContent = `${num}/${pdfDoc.numPages}`;
});
});
}
// 异步加载PDF
function loadPdf(url) {
// PDFJS.getDocument()返回一个Promise
pdfjsLib.getDocument(url).promise.then(pdfDoc_ => {
pdfDoc = pdfDoc_;
// 初始渲染第一页
renderPage(pageNum);
}).catch(err => {
// 错误处理(比如PDF损坏)
alert(`加载失败:${err.message}`);
});
}
// 绑定按钮事件
prevBtn.addEventListener('click', () => {
if (pageNum <= 1) return;
pageNum--;
renderPage(pageNum);
});
nextBtn.addEventListener('click', () => {
if (pageNum >= pdfDoc.numPages) return;
pageNum++;
renderPage(pageNum);
});
// 启动!加载服务器上的PDF
loadPdf('/static/test.pdf');
运行这段代码,你会看到 PDF 神奇地出现在网页上,还能翻页!那一刻,我激动得差点给电脑磕个头。
2.3 进阶技巧:让 PDF.js 更懂你
1. 搜索功能
用户最爱问的:"怎么搜关键词?" 安排!
// 搜索PDF中的文字
function searchText(text) {
const results = [];
// 逐页搜索
for (let i = 1; i <= pdfDoc.numPages; i++) {
pdfDoc.getPage(i).then(page => {
page.getTextContent().then(content => {
// 遍历文本片段
content.items.forEach(item => {
if (item.str.includes(text)) {
results.push({
page: i,
text: item.str
});
}
});
// 显示结果
if (i === pdfDoc.numPages) {
console.log('搜索结果:', results);
}
});
});
}
}
注意:大 PDF 全页搜索会很慢,建议加个 loading 动画,不然用户还以为网页卡死了。
2. 自定义样式
嫌弃默认的灰白风?可以给 canvas 加个背景图,或者在上面画水印:
// 渲染完成后加水印
renderTask.promise.then(() => {
// 画水印
ctx.save();
ctx.fillStyle = 'rgba(255,0,0,0.1)';
ctx.font = '40px Arial';
ctx.rotate(-0.2); // 旋转角度
ctx.fillText('内部资料', 100, 300);
ctx.restore();
});
3. 懒加载
加载 1000 页的 PDF 时,一次性加载会让浏览器崩溃。可以只加载当前页和前后两页:
// 只加载可见区域附近的页面
function lazyLoadPages(visiblePage) {
const start = Math.max(1, visiblePage - 2);
const end = Math.min(pdfDoc.numPages, visiblePage + 2);
// 只渲染start到end的页面
}
三、其他工具:各有神通
除了 PDF.js,还有些工具在特定场景下很好用,就像食堂除了米饭,还有面条和包子。
3.1 PDFObject:简单到极致
这是个轻量级库,本质是对 iframe 和 embed 标签的封装,但处理了各种浏览器兼容问题。
<!-- 引入 -->
<script src="https://cdnjs.cloudflare.com/ajax/libs/pdfobject/2.2.8/pdfobject.min.js"></script>
<!-- 使用 -->
<div id="pdfContainer"></div>
<script>
// 第二个参数是容器ID
PDFObject.embed("/static/test.pdf", "pdfContainer", {
width: "100%",
height: "800px"
});
</script>
优点:5 分钟就能上手,适合只需要简单预览的场景。缺点:依赖浏览器渲染能力,自定义程度低,就像快餐,能吃饱但别指望有多好吃。
3.2 商业方案:有钱能使鬼推磨
如果公司不差钱,可以考虑商业 SDK,比如:
- Adobe PDF Embed API:Adobe 爸爸的产品,功能强到离谱(支持批注、签名),但免费版有水印,付费版按流量收费,适合大企业。
- Foxit SDK:福昕的网页 SDK,本地化支持好,价格比 Adobe 亲民,文档是中文的(对英语渣太友好了)。
就像出去下馆子,花钱买服务,不用自己洗碗(处理复杂需求)。
四、避坑指南:那些年我踩过的雷
4.1 跨域问题
当你兴高采烈地用 PDF.js 加载其他域名的 PDF 时,会遇到这个错误:
Access to fetch at 'https://别人的域名/test.pdf' from origin '你的域名' has been blocked by CORS policy
解决办法有三:
- 后端代理:让你们后端大哥转发一下请求,就像让公司前台帮你收快递。
- 对方服务器开 CORS:跟对方网站管理员说 "大哥,加个 Access-Control-Allow-Origin 呗"。
- 用 base64:把 PDF 转成 base64 字符串传给前端,但大文件会让 JS 内存爆炸。
4.2 大文件加载慢
一个 100MB 的 PDF,直接加载会让用户等到花儿都谢了。解决思路:
- 后端分片传输:把 PDF 切成小块,前端加载一点渲染一点,就像吃火锅涮肉,一片一片来。
- 预加载:用户看第 1 页时,偷偷加载第 2、3 页,翻页时就不会卡。
4.3 移动端适配
在手机上渲染时,经常出现内容超出屏幕的情况。可以监听窗口大小变化,动态调整缩放比例:
window.addEventListener('resize', () => {
// 重新计算缩放比例
const newScale = window.innerWidth / 800; // 假设原宽度是800px
renderPage(pageNum, newScale); // 重写renderPage支持动态缩放
});
五、终极方案:根据场景选工具
需求场景 | 推荐工具 | 优点 | 缺点 |
简单预览,快速上线 | PDFObject | 代码少,学习成本低 | 自定义弱,依赖浏览器 |
复杂交互(搜文字、加水印) | PDF.js | 开源免费,功能强 | 配置复杂,需自己处理优化 |
企业级需求(电子签名、协作) | Adobe/Foxit SDK | 专业稳定,有技术支持 | 收费,可能有版权问题 |
小程序 / APP | 原生控件 + JS 桥接 | 体验好,性能强 | 需原生开发配合 |
六、结语:从 "能看" 到 "好看"
前端渲染 PDF 的发展史,就是一部前端工程师与产品经理的斗智斗勇史。从最初的 iframe 凑数,到用 PDF.js 实现复杂交互,我们追求的不仅是 "能看",更是 "好看、好用、好快"。
最后送大家一句金玉良言:永远不要相信产品经理说的 "就简单显示一下"—— 因为下一秒他就会问:"能加个在线编辑功能吗?"