首页
/
行业洞察
/
正文
INDUSTRY INSIGHT · 深度
Hexo博客集成PDF.js:实现文章内嵌PDF阅读与续读功能
📅 2026/10/5 1:23:01
✍️ 爱科研究院
👁 阅读 3,247
如果你跟我一样平时在Hexo博客上写技术笔记、发资料汇总一定会遇到这个需求文章里要放一个PDF附件比如论文原文、产品手册、数据报告。直接扔一个链接出去倒是省事但读者点开后浏览器要么把PDF渲染成冷冰冰的独立标签页要么直接触发下载阅读体验跟你的博客风格完全割裂。所以我花了点时间把PDF.js接到了Hexo博客里让文章页面上就能直接翻开PDF支持翻页、缩放、打印还能自己控制阅读器长什么样。这篇文章不跟你绕概念直接讲清楚Hexo博客怎么用PDF.js包括核心原理、目录和路径怎么放、文章页内嵌代码怎么写、部署到GitHub Pages之后常见的“failed to fetch”报错怎么排查以及如何把“读到第几页”这个进度记录下来做成类似“下次继续阅读”的功能。内容覆盖从入门到进阶的完整链路适合想在博客里优雅展示PDF的Hexo用户。1. 为什么Hexo博客要接PDF.js浏览器直接看PDF的体验困境1.1 原生PDF查看方式的槽点先聊点真实的体验问题。很多Hexo博客早期处理PDF附件用的都是最原始的办法在文章里贴一个a href/files/xxx.pdf下载PDF/a。这个办法对站长来说确实省事但读者那边就遭罪了。Chrome、Edge、Firefox 虽然内置了PDF查看器但那是浏览器的“默认行为”不是网站的一部分。读者点开链接后整个标签页被PDF占满地址栏前面出现一个文件图标浏览器工具栏上突兀地冒出“保存”“打印”“旋转”按钮——这一瞬间你的博客品牌、导航栏、侧边栏、相关文章推荐全部消失了。如果读者想回到文章列表要么按返回键要么手动关标签页心智负担非常大。移动端就更严重。手机浏览器打开一个动辄十几MB的PDF系统自带的阅读器可能会自动横屏、缩放卡顿甚至直接白屏。读者在手机上遇到这种体验基本不会坚持看完大概率默默关掉页面。这意味着你辛辛苦苦写好的背景说明、摘要导读都没能起到引导阅读的作用。还有一点容易被忽略直接暴露PDF文件地址等于把文件索引暴露给了搜索引擎和爬虫。某些场景下你可能是故意的但很多场景下你只是想让读者先看摘要再决定要不要全文阅读。直接挂链接这个主动权就没了。1.2 PDF.js能解决什么不能解决什么PDF.js是Mozilla开源的一个纯前端PDF渲染引擎核心思路是用HTML5的Canvas和JavaScript把PDF解析并绘制到网页上。你不需要在服务器端装任何插件也不需要让读者安装阅读器一个现代浏览器就能跑。这东西能解决的问题非常直接渲染外观完全由你的页面控制可以嵌入到文章正文中保持博客的视觉统一。支持上一页/下一页、页码跳转、缩放、旋转、打印这些阅读器常用操作。页面交互可以自己接管。比如监听当前页码把进度保存到localStorage或后端实现“从上次阅读位置继续”。移动端体验可控你可以针对手机屏幕调整阅读器宽度、按钮大小。但也要说清楚它不能做什么。PDF.js本身只是一个渲染器它不负责文件管理、目录解析之外的复杂逻辑。有些扫描版PDF没有文本层虽然能显示但无法选中文字这不是PDF.js的bug而是源文件本身没有OCR。另外如果你的PDF文件受密码保护PDF.js也支持输入密码解密但那需要额外处理不是默认就有的功能。1.3 为什么不建议用iframe/embed直接嵌有些朋友会说那不引入PDF.js用iframe srcxxx.pdf或者embed不也能在网页里显示PDF吗能但体验看运气。iframe方式本质上还是调用了浏览器内置的PDF插件。在桌面端Chrome里表现还行但到Firefox、Safari上不同浏览器渲染出来的界面完全不同你没法做到统一。更麻烦的是很多主题对iframe有样式重置嵌入后高度、宽度经常失控你很难用CSS去精细控制内部元素。移动端更不可控某些浏览器会直接忽略iframe只显示一个下载条。还有一点iframe方式对跨域限制很敏感。如果你的博客部署在A域名PDF文件放在B域名的存储桶里浏览器可能会因为X-Frame-Options或者CORS策略拒绝渲染白屏没商量。PDF.js同样要处理跨域问题但至少它有明确的报错信息和配置项你能知道问题出在哪而不是对着一个空白iframe发呆。所以如果想认真做阅读体验PDF.js是更靠谱的方案。它把PDF阅读器的“内核”拿到了你自己手里剩下的UI交互你想怎么设计都行。2. 在Hexo里引入PDF.js存量部署与路径陷阱2.1 CDN、npm、本地静态文件三种方式对比Hexo项目里引入PDF.js主要有三条路CDN引包、npm依赖、本地静态文件托管。很多人第一次选型时会纠结我直接把三种方式的优劣摆出来。方式加载速度可控性自定义难度适合场景CDN引入快但依赖第三方服务稳定性低想看官方最新版需要自己改版本号简单几行script标签就完事快速验证、功能演示npm依赖需要打包工具配合Hexo里要写脚本中版本固定但构建链复杂高需要处理模块路径你已经在用复杂构建流程本地静态文件慢一些但完全可控高上传到自己的服务器/GitHub Pages中部署时要小心路径正式博客、离线环境、长期维护我的建议很明确大多数Hexo博客直接用CDN方案做快速上线没问题但如果你的博客追求稳定、想要二次开发阅读器UI那就把PDF.js的整个build目录和web目录放到source下面作为纯静态资源托管。这里顺带回答一个经常被问到的问题npm方式在Hexo里并不友好。Hexo的核心是静态页面生成不是前端工程化平台。你就算在package.json里装了pdfjs-dist静态页面生成时也不会自动帮你把依赖打包进HTML。所以实际执行起来反而比直接放静态文件麻烦得多。2.2 Hexo部署到GitHub Pages时的路径问题很多人的Hexo博客部署在GitHub Pages上访问地址可能是https://username.github.io也可能是https://username.github.io/repo/这种子路径。这就是PDF.js最头疼的路径陷阱。先解释一下Hexo的生成逻辑。你在项目根目录执行hexo generate后public文件夹就是最终的站点内容。source里的文件、文件夹会原样拷到public。所以推荐做法是在source目录下建一个lib/pdfjs文件夹把PDF.js的构建文件解压进去。生成后就能通过/lib/pdfjs/web/viewer.html访问到官方自带的阅读器页面。但这里有个关键点你的根路径不一定总是/。GitHub Pages项目站点访问路径是https://username.github.io/repo/此时你博客的实际根路径是/repo/。如果你在主题配置文件里没有设置root: /repo/那么所有静态资源的绝对路径都会对不上PDF.js的worker、阅读器、PDF文件都会报404。在Hexo的_config.yml里这个配置项叫root。部署到GitHub Pages时我强烈建议你打开public/index.html看一眼里面引用的CSS、JS路径是不是以/repo/开头。如果不是进入主题的_config.yml或者主配置_config.yml修改root值。另外PDF文件本身也有路径问题。如果你把PDF放在source/pdf/xxx.pdf在文章里引用时用绝对路径/pdf/xxx.pdf还是相对路径../pdf/xxx.pdf取决于文章生成后所在目录层级。我的习惯是统一用绝对路径并且让PDF路径和博客root配置保持一致也就是写/repo/pdf/xxx.pdf。这样做的好处是不管文章嵌套多深都不会因为相对路径算错导致文件加载失败。2.3 主题模板中加载PDF.js的推荐位置下一步要考虑的是把PDF.js的脚本和样式放到哪个页面里加载。很多人一上来就把PDF.js的link和script塞进Hexo主题的head里让全站每个页面都加载一遍。如果是CDN方式倒还好如果是本地静态文件这个体积可不算小。PDF.js核心JS加Worker加CSS加起来可能接近1MB全部站点都加载纯属浪费。更合理的做法是只在你需要展示PDF的文章页面里加载PDF.js。Hexo里实现这个思路很简单给文章在front-matter里加一个自定义字段比如--- title: 某产品白皮书 date: 2025-01-01 pdf: /pdfs/whitepaper.pdf ---然后在主题模板里判断如果page.pdf存在就动态加载PDF.js的脚本。如果你的主题不支持这种灵活注入也可以用Hexo的after_render:html过滤器或者干脆使用一个自定义的Tag插件只在文章内容中输出PDF.js的标签。这部分的宗旨只有一个PDF.js按需加载不污染全站性能。3. 实操在Hexo文章内渲染PDF并控制阅读交互3.1 一段最简PDF.js渲染代码假设PDF.js的静态文件已经放在/lib/pdfjs下你可以在任意页面里写这样一段HTMLdiv idpdf-container stylewidth: 100%; height: 600px; border: 1px solid #ddd;/div script src/lib/pdfjs/build/pdf.min.js/script script var pdfUrl /pdfs/whitepaper.pdf; var container document.getElementById(pdf-container); var pdfDoc null; var currentPage 1; pdfjsLib.GlobalWorkerOptions.workerSrc /lib/pdfjs/build/pdf.worker.min.js; pdfjsLib.getDocument(pdfUrl).promise.then(function (doc) { pdfDoc doc; renderPage(currentPage); }); function renderPage(num) { pdfDoc.getPage(num).then(function (page) { var scale 1.5; var viewport page.getViewport({ scale: scale }); var canvas document.createElement(canvas); canvas.width viewport.width; canvas.height viewport.height; var ctx canvas.getContext(2d); container.innerHTML ; container.appendChild(canvas); page.render({ canvasContext: ctx, viewport: viewport }); }); document.getElementById(pageNum).textContent num; } document.getElementById(prev).addEventListener(click, function () { if (currentPage 1) return; currentPage--; renderPage(currentPage); }); document.getElementById(next).addEventListener(click, function () { if (currentPage pdfDoc.numPages) return; currentPage; renderPage(currentPage); }); /script这段代码的核心是三件事配置worker、用getDocument加载PDF、用getPage加render绘制到Canvas。你需要在旁边补上上一页、下一页的按钮以及显示页码的元素。这是最原始的“自己造轮子”版本优点是代码完全可控缺点是分页、缩放、目录这些东西都要自己实现。3.2 基于Hexo的Tag Plugin封装写法如果你在博客里会多次使用PDF阅读器每次都在Markdown里粘贴上面那一大段HTML维护起来很痛苦。更优雅的方式是用Hexo的Tag Plugin机制封装一个短代码。在Hexo项目的scripts目录下新建pdf_tag.jshexo.extend.tag.register(pdf, function (args) { var url args[0]; return div classhexo-pdf>--- pdfs: - /pdfs/manual-1.pdf - /pdfs/manual-2.pdf ---然后在模板中循环输出PDF阅读器容器。配合Tag Plugin和全局脚本每个容器都会独立加载自己的PDF互不干扰。要注意的是容器里的按钮、页码要依赖全局脚本里的事件委托处理否则多个阅读器同时存在时容易互相抢事件。4. 排查实录v2.16.105的“failed to fetch”到底卡在哪4.1 报错现场与第一印象PDF.js接入最常遇到的报错就是控制台里出现一行信息PDF.js v2.16.105 (build: 172ccdbe5) Info: Failed to fetch注意这行日志的级别是“Info”不是“Error”很多人看到它以为只是提示信息但紧接着PDF区域一片空白这个时候就知道事情不对劲了。“Failed to fetch”翻译成人话就是PDF.js试图通过HTTP请求去获取PDF文件但请求失败或者被拦截了。这个报错出现的原因非常集中在两个方向第一PDF文件本身路径不对返回了404第二请求因为跨域策略被浏览器拦截了。还有少部分情况是网络慢导致超时但那个通常是偶发现象不是每次必现。4.2 五分钟定位法从URL到Network再到CORS我遇到这个报错从来不看PDF.js源码而是直接按下F12打开开发者工具按下面这个顺序排查。第一步看地址。在浏览器地址栏里直接访问你写的那个PDF地址比如https://你的域名/pdf/whitepaper.pdf。如果浏览器显示404那就是路径错了问题出在Hexo的静态资源拷贝或者路径书写上跟PDF.js一点关系都没有。第二步看Network面板。如果地址能直接访问就刷新页面在Network里过滤whitepaper.pdf看这个请求的状态码是多少。顺带看一眼对应请求的Response Headers里有没有Access-Control-Allow-Origin这个字段。如果PDF和博客在同一个域名下一般不会触发CORS如果PDF放在LeanCloud、阿里云OSS、COS这类对象存储里那基本都会触发跨域。第三步看浏览器的Console完整报错。PDF.js有时候会进一步给出 “Expected content-type: application/pdf” 或 “Access to fetch at xxx from origin yyy has been blocked by CORS policy” 这样的描述。前者说明后端返回的MIME类型不对后者说明真就是跨域问题。按照这三步排查五分钟内基本能锁定根因。4.3 排查中发现的高频根因路径与MIME类型从实测经验看频率最高的还是结构问题Hexo博客部署到GitHub Pages后你的PDF文件如果放在source/_posts里面Hexo会把PDF文件当作文章渲染路径会变得非常奇怪。正确做法是把PDF放在source/pdf/这样独立的资源目录或者放在source/uploads/下面。Hexo会原样把纯静态文件拷贝到public对应目录不会经过文章渲染流程这时候路径就可控了。其次是MIME类型问题。GitHub Pages对常见的.pdf类型返回的是application/pdf很少出问题。真正容易出问题的是你自己搭的Nginx服务器配置文件里如果没有加location ~ \.pdf$ { default_type application/pdf; }某些服务器会把.pdf当成二进制流返回PDF.js去解析时校验Content-Type不通过就会直接抛“Expected content-type: application/pdf”之类的报错。这个问题在本地用hexo server预览时不一定复现因为hexo内置的静态服务器返回头是对的部署到自己的Nginx后才暴露。4.4 部署到GitHub Pages后根治路径问题如果你的博客用的是GitHub Pages有两个需要注意的地方。第一仓库站点的根路径问题。当你通过https://username.github.io/repo/访问博客时所有资源和PDF路径开头必须是/repo/。如果你在_config.yml里的root配置正确那么hexo generate生成的HTML里绝对路径一般会自动带上/repo/但你自己在Markdown里手写的路径不会自动改。所以要么所有PDF路径都写成/repo/pdf/xxx.pdf要么就用相对路径比如./pdf/xxx.pdf这取决于你文章的目录层级。第二让PDF跟博客同域部署这是避开CORS最省心的方法。很多人喜欢用LeanCloud之类的存储放PDF结果部署到GitHub Pages后跨域问题一堆。你需要在对象存储那边配置CORS规则允许你的博客域名访问。如果PDF不大直接跟博客一起提交到GitHub仓库用Pages自己托管一劳永逸不会有CORS问题。5. 进阶把阅读进度记起来做自己的“续读”功能5.1 先想清楚进度存哪里PDF.js可以让你知道用户当前读到第几页这个信息本身的存储位置决定了功能的上限。如果你只需要单浏览器内的“续读”用localStorage就够了。好处是不用搭后端坏处是用户换个浏览器、清一下缓存进度就丢了。如果你想让进度跟随用户账号跨设备同步那就必须有一个后端接口。很多Hexo博客并没有真正的后端服务所以实际落地方案通常是用第三方BaaS服务比如LeanCloud、Supabase存数据或者自己写一个简单的Node/云函数接口。选择存储方案时先问自己一个问题这个博客的读者真的需要跨设备同步进度吗大多数场景下localStorage能覆盖80%的需求。我会先讲localStorage方案再讲后端方案。5.2 实现方案一localStorage无后端方案localStorage思路的核心是每次页码变化就把“当前页码”和“PDF文件地址”存到浏览器本地。下次用户打开同一篇带有PDF的文章时读取localStorage如果找到对应记录就把阅读器定位到那一页。代码逻辑大致是这样function saveProgress(pdfUrl, pageNum) { var key pdf-progress- pdfUrl; localStorage.setItem(key, JSON.stringify({ page: pageNum, time: Date.now() })); } function loadProgress(pdfUrl) { var key pdf-progress- pdfUrl; var data localStorage.getItem(key); if (!data) return null; try { return JSON.parse(data).page; } catch (e) { return null; } }注意key的设计。直接用PDF路径作为key不同文章的PDF不会互相覆盖。如果同一篇文章有多个PDF也可以把key带上文章IDpdf-progress-post-123- /pdfs/manual-1.pdf。保存时机也有讲究。不建议在每一页渲染完成后立刻写localStorage那样写得太频繁。更稳妥的做法是在阅读器容器mouseleave或页面visibilitychange事件里保存。在翻页按钮点击的setTimeout里做一次防抖保存。比较激进的做法是每页渲染完成后直接保存但实测对性能影响很小因为localStorage写入是很轻的操作。5.3 实现方案二后端API对接数据库localStorage方案的缺点是只能在用户本机保存。如果你真的要做跨设备同步那就需要一个后端。Hexo是纯静态站点没有传统意义上的服务端。所以这个后端只能外置最省事的办法是接入Supabase或LeanCloud这类BaaS。简单说你的逻辑是前端通过fetch把进度POST到云端数据库下次用户访问时再从云端拉取。接口结构可以设计成// 保存进度 fetch(https://你的接口地址/api/pdf-progress, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ pdf: pdfUrl, page: currentPage, user: userId }) }); // 拉取进度 fetch(https://你的接口地址/api/pdf-progress?pdf encodeURIComponent(pdfUrl) user userId) .then(function (res) { return res.json(); }) .then(function (data) { if (data data.page 1) { jumpToPage(data.page); } });后端API需要做的逻辑也很简单接收PDF标识和页码按用户ID查询或插入记录。这里要注意的是“防抖”同样重要否则用户每翻一页就产生一个请求数据库顶不住。我实测的体验是滚动翻页非常顺滑但网络请求会在后台密集发出所以必须加一个至少800ms的节流。5.4 恢复进度的交互设计进度保存好了恢复的时候要有次设计感。千万不要一打开页面就强制调到上次页码很多人的浏览习惯是先看文章开头再决定要不要继续看PDF。如果页面刚加载就跳到第50页用户会一头雾水。我建议的做法是在PDF阅读器上方加一条提示条内容近似“检测到你上次读到第35页点击继续阅读”。用户点击后才跳转。同时提供一个“从头开始”按钮方便用户主动重置进度。这个交互成本很低但体验好很多。很多使用者纯粹是被动阅读他们希望系统记住进度又不想被进度绑架。提示条的同时给出选择权是最稳妥的。6. 使用PDF.js过程中的几个细节与我的习惯做法版本锁定的问题我单独提一句。PDF.js的更新节奏其实挺快的不同版本之间API有变化。比如getDocument返回的Promise链老版本用.then新版本也兼容但有些参数在新版本里换成了对象写法。我见过有人直接从官方示例复制了一段新版代码结果在自己博客里因为版本太老而报错。所以建议在项目里固定一个版本不要随便升级。另外PDF.js体积不小。如果你只是偶尔用一下本地托管确实占资源但我仍然推荐本地托管优先于CDN。原因不光是稳定还有一个关键点CDN域名跟你博客域名不同PDF.js加载Worker、字体时可能会产生额外的跨域请求某些CDN没配好CORS就会出现“Failed to fetch”非常折腾。本地托管则完全没有这个问题。最后再分享一个小技巧如果你用的是hexo-server本地预览务必在_config.yml里把root路径保持和线上一致否则本地看一切正常部署到GitHub Pages后才发现路径全是歪的。这个坑我踩了不止一次现在已经养成习惯每次部署之前先在本地跑一遍hexo generate然后用npx serve public起一个静态服务验证路径确认无误再推到远程分支。Hexo博客接PDF.js这件事技术门槛并不高但前前后后的路径处理、静态资源管理、交互打磨才是真正拉开体验差距的地方。照着上面这些思路操作基本能绕开我当年踩过的绝大多数坑。
📌 标签:
工业官网
设计趋势
AI 建站
SEO
获取完整报告 →
RELATED ARTICLES
推荐阅读
2026/10/5 1:23:01
DCE容器云平台:面向信创与多集群的K8s企业级交付操作系统
2026/10/5 1:23:01
STM32G491RE 驱动 MR25H40CDF MRAM 实现工业数据存储与读取
2026/10/5 1:23:01
工业嵌入式存储选型与实战:MRAM与NOR Flash的SPI读写、掉电保护及避坑指南
2026/10/5 2:03:04
使用 Docusaurus 构建 LichtFeld Studio 文档站点:本地开发与静态部署实战指南
2026/10/5 2:03:04
YOLO半自动标注实战:auto_label.py脚本与避坑指南
2026/10/5 2:03:04
一键解析八大网盘真实下载链接:网盘直链下载助手使用指南
2026/10/5 2:03:04
对于Redis:渐进式遍历scan、数据库的解析
2026/10/5 2:03:04
VibeSentinel-AI:基于振动分析与边缘计算的预测性维护系统实战
2026/10/5 1:58:03
douyin-downloader 抖音批量下载:5 分钟跑通,一次存全博主作品
2026/10/5 0:02:57
AZ-104题库深度拆解:从刷题到掌握Azure管理员核心考点
2026/10/5 0:02:57
WorkBuddy:基于MCP协议的组织级工作流神经中枢
2026/10/5 0:02:57
大模型 / AI 应用常见面试题及答案汇总(2026 最新版):用 TaoToken 统一 Key 跑通高频考点代码验证
2026/10/4 0:00:57
Jev+Agent接管浏览器:browser-use实战与jev-ultrafast性能优化
2026/10/5 1:10:25
多智能体集群实战:DeepAgents编排、MCP与A2A协议及Skills体系
2026/10/4 0:00:57
hindsight:面向LLM应用的事后可观测性工程实践
2026/10/4 2:41:08
我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
2026/10/4 17:59:15
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026/10/3 15:20:14
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)