前言

用 pdf.js 给静态站加 PDF 在线预览是常见做法 (部署方法和接入方式见文末相关文章)。但部署完打开 viewer.html, 遇到一个诡异现象:

页面只有一条灰线, 内容区完全塌陷。 更迷惑的是按 F12 看, DOM 元素全都在, 结构完整, 好像什么都没发生过。

这篇记录完整的排查和修复过程, 核心结论一句话: viewer.html 必须作为独立页面存在, 不能被 Hexo 渲染进主题模板。

一、原因: height:100% 的高度链断了

pdf.js 的 viewer 布局依赖一条链式的高度声明:

1
2
3
4
html, body { height: 100%; }
#outerContainer { height: 100%; }
#mainContainer { height: 100%; }
#viewerContainer { height: 100%; }

每一层的 100% 都依赖父层有一个确定的高度。直接访问 viewer.html 时, 这条链从 html 一路传下去, 全部有效。

而 viewer.html 一旦被 Hexo 当成一篇文章渲染, 它的内容会被塞进主题的 layout, 变成主题内容区里的一个”内嵌块”。主题的内容区容器高度通常是 auto —— 由内容撑开的。

于是链条在这里断了:

1
html (100%) → body (100%) → 主题容器 (auto!) → #outerContainer (100% × auto = 0)

100% 乘上一个不确定的父高度, 结果就是 0。所有内部容器依次塌陷, 页面只剩一条边框线。

这也解释了”DOM 全在但看不见”: 元素都在文档里, 只是几何尺寸算出来是 0。

二、解法一: skip_render 让 viewer 保持独立页

在站点 _config.yml 里把 pdf.js 目录排除出渲染流程:

1
2
skip_render:
- pdf.js/**
  • skip_render 的路径相对 source 目录, 支持 glob
  • 被排除的文件会原样拷贝到 public, 不套主题模板, 不动内容

改完构建后验证一下产物: 打开 public/pdf.js/web/viewer.html, 搜不到主题的容器 div (比如 #content-inner) 就说明它还是独立 HTML。

注意: 如果之前验证过”不 skip_render 也能用”, 那大概率是靠巧合 —— 比如主题模板恰好没包住它, 或者当时用 iframe 加载没暴露问题。主题升级或布局一变就会爆发。结论以 skip_render 为准, 不要依赖巧合。

三、解法二: viewer 内部的相对路径改根绝对

独立页解决了”被主题包裹”的问题, 但还有一层隐患: viewer 内部的资源引用是相对路径, 包括:

文件相对引用示例建议改为
web/viewer.htmlviewer.css、locale/locale.properties、../build/pdf.js、viewer.js/pdf.js/web/viewer.css 等根绝对路径
web/viewer.js./images/、../build/pdf.sandbox.js/pdf.js/web/images/、/pdf.js/build/pdf.sandbox.js

什么时候会爆发? 典型场景就是 pjax 站点: pjax 拦截跳转时, 目标页里的相对路径会按来源页地址解析, 全部 404。另外把 viewer 挂到别的路径下也会出问题。

改法: 在 viewer.html 和 viewer.js 里全局搜 href="、src=" 和资源目录字符串, 把所有不以 / 开头的站内资源引用补上前缀 /pdf.js/...。

两点提醒:

  • pdf.js 不同版本文件名不同 (新版本是 pdf.mjs、pdf_viewer.mjs), 思路完全一致: 相对改根绝对
  • 改完用 DevTools Network 筛一遍 404 确认清零

四、pjax 站点的链接: 带 query 的要开新窗口

资源列表页里的查看链接通常长这样:

1
<a href="/pdf.js/web/viewer.html?file=/files/a.pdf">查看</a>

在 pjax 站点里这个链接会被拦截, 而 pjax 的 exclude 对带 query 的 URL 经常匹配不上 (精确 href 匹配), 结果 viewer 被塞进主题容器, 又回到第一节的高度塌陷。

可靠解法是开新窗口:

1
<a href="/pdf.js/web/viewer.html?file=/files/a.pdf" target="_blank" rel="noopener">查看</a>

pjax 不接管带 target 的链接, 这是比 exclude 稳得多的方案。

五、调试方法论: 量尺寸, 别数元素

这次排查里最有价值的教训: 验证页面必须量几何尺寸, 而不是只数 DOM 元素。

塌陷时元素全在, “数元素”的验证会给你一个假象的”通过”。正确姿势是在控制台量 getBoundingClientRect:

1
2
3
4
5
6
['#outerContainer', '#mainContainer', '#viewerContainer']
.map(sel => {
const el = document.querySelector(sel);
return el ? `${sel}: ${el.getBoundingClientRect().height}px` : `${sel}: 不存在`;
})
.forEach(console.log);

输出类似:

1
2
3
#outerContainer: 0px      ← 问题层
#mainContainer: 0px
#viewerContainer: 0px

从上往下第一个变 0 的层级, 就是高度链断裂的位置。这个方法适用于所有”DOM 在但看不见”的布局问题, 比”看一眼觉得不对”高效得多。

六、验证清单

改完之后按这张表过一遍:

检查项方法
viewer 独立渲染产物 HTML 里搜不到主题容器
直开正常地址栏直接访问 viewer.html?file=... 内容完整
站内入口正常从资源页点”查看” (新窗口) 内容完整
无 404Network 筛 404 清零
高度链完整控制台量三个容器高度均大于 0

小结

  1. viewer.html 必须 skip_render, 保持独立页 —— 被 Hexo 渲染进主题后, height:100% 的链断在主题容器上, 内容区塌陷为 0
  2. viewer 内部相对路径改根绝对 —— 防 pjax 解析歧义和路径迁移问题
  3. 带 query 的查看链接用 target="_blank" —— 别指望 exclude 能匹配上
  4. 调试”看不见”的问题先量 getBoundingClientRect —— DOM 在 ≠ 可见

相关文章: