当前页面没有目录
当前页面没有目录
给自己博客补一个小功能。
就是在文章详情页下面分享卡片里面,加一个真正能拿来用的下载按钮。
不是那种随便截个图就完事的下载。
而是你正在看的这篇文章,它的正文、配图、代码块、Mermaid、PlantUML,最好都能尽量按当前页面的阅读样子,被导出成一个排版还不错的文件,让别人真的可以存下来、转发出去、离线看。
一开始我以为这件事不会太难。
后来真正做才发现,浏览器端文章导出这件事,最麻烦的从来不是按钮本身,而是下面这几个体验问题:
- 长文导出特别慢
- 浏览器看起来像卡死了一样
- 用户根本不知道现在进行到哪一步了
- 代码高亮、行内代码、图表、图标这些细节特别容易在导出时变形
- 万字长文一旦直接把截图、编码、压缩全堆到主线程,前端线程压力会一下子冲上来
所以这次我最后落下来的,不只是一个下载按钮。
而是一套更适合静态博客前端环境的文章导出策略。
先看看结果#
现在这套博客文章下载,已经变成了两个能力:
- 长图导出,适合把整篇文章导成一张高清 PNG
- PDF 导出,适合导出成文章文档
它们底层都围绕文章正文做导出,但侧重点不一样。
长图更强调连续阅读体验。
PDF 更强调文档分发体验。
更重要的是,这次长图导出不再是那种一把梭把整个页面丢给浏览器主线程硬算的做法了。
现在的核心设计是:
主线程只负责分段截图,PNG 编码和压缩搬到 Web Worker。
这句话看起来很轻,但其实它决定了长文导出时,用户到底是在等一个靠谱的前端任务,还是在等浏览器原地发呆。
为什么旧方案不行#
最早那版思路其实很直接。
把文章节点克隆出来,丢给前端渲染库,生成一张超长画布,然后直接转成图片。
听着没毛病。
小文章也确实能跑。
问题是一旦碰到真正的长文,尤其是那种带很多代码块、图表、卡片组件、动态小部件的文章,这个方案的问题就会一下子放大出来。
第一,导出等待非常长。
第二,整个页面体感会变得很僵。
因为超长文章的分段截图、像素读取、PNG 编码和压缩,如果都压在主线程里做,浏览器就很容易给人一种快死机的感觉。
第三,用户没有反馈。
只会看到一个按钮变成加载状态,然后开始怀疑到底是网络慢了,还是浏览器炸了,还是导出根本没开始。
说真的,这种体验对于文章下载这种本来应该很实用的功能来说,太不友好了。
选择的优化方案#
这次我没有继续在旧方案上打补丁,而是把长图导出拆成了更清晰的一条链路。
核心思路是:
- 先把当前文章整理成一个专门用于导出的离屏节点
- 等待字体、图片、Mermaid、PlantUML 这些资源稳定
- 按正文真实宽度来生成导出舞台
- 长图模式下,把整篇文章切成多个可控高度的渲染分片
- 主线程只负责把这些分片逐段截图成高分辨率 canvas
- 再把 PNG 编码和压缩优先搬到 Web Worker 里做
- 如果用户环境不支持这条路,再自动回退到主线程兜底
用图来看效果的话,大概是这样:

这套结构最大的好处,不是听起来高级。
而是把「分段截图」和「PNG 编码压缩」这两件事拆开了。
浏览器主线程还是要负责把文章逐段截图出来,这一步没法完全绕开。
但后面最容易拖慢页面、最容易让用户觉得浏览器卡住的那部分 PNG 编码和压缩工作,现在可以尽量往 Worker 里放了。
为什么是使用 Web Worker,发挥了什么作用?#
Web Worker 是什么。
你可以把它理解成,浏览器在主线程之外额外分出来的一个后台工作线程。
页面上的点击、滚动、动画、输入响应,这些事大多都压在主线程上。
如果你把特别重的计算也一起塞进去,页面体感就很容易变卡。
Web Worker 的价值就在这里。
它可以把一部分不需要直接操作页面 DOM 的重活拆出去,在后台慢慢算,算完再把结果回传给主线程。
所以它特别适合拿来做这些事:
- 图片编码、压缩、格式转换
- 大文本处理和大 JSON 解析
- 文件导出、文件预处理
- 搜索索引、数据聚合、复杂计算
- 音视频处理前的一些像素级或数据级操作
- 那种会跑很久,但用户又不希望页面直接卡住的前端任务
当然,它也不是万能的。
Web Worker 不能像主线程那样直接去操作页面节点,很多跟真实 DOM 强绑定的事,它还是做不了。
我也没有把长图导出的所有工作一股脑全塞进 Worker。
因为浏览器环境这件事,从来都不值得你过度乐观。
如果我把整套长图导出完全写死在 Worker 路线上,那结果很可能是:
- 某些环境支持得很好
- 某些环境因为 API 差异直接不能用
- 某些场景里导出到一半出错,用户连回退都没有
所以我最后更喜欢的做法,不是只押一种最理想方案,而是:
优先走体验更好的 Worker 路线,但必须保留一条稳定的主线程兜底通道。
也就是说,支持 Worker + OffscreenCanvas + CompressionStream + createImageBitmap 的环境,就走更顺滑的高性能路径。
不支持,或者 Worker 编码中途失败,那就自动切回主线程编码。
这样做有点像是给前端导出能力做了一层弹性。
像文章节点克隆、资源稳定、分段截图这种强依赖页面布局和 DOM 渲染状态的步骤,主线程还是得接着做。
真正适合搬进去的,是后面那段更重、更耗时、但不需要直接碰页面结构的 PNG 编码和压缩工作。
理想情况下,你拿到的是更好的体验。
但再差,也不至于因为某个浏览器特性没就绪,整篇文章完全导不出来。
真实进度回调反馈#
对于真实进度反馈这一点还是挺重要的。
因为用户对等待最烦的,不是时间本身。
而是你让他等的时候,什么都不告诉他。
所以现在这套导出会给出一条比较真实的进度轨迹。
不是假装每 100ms 加一点数字的那种进度条。
而是按导出阶段去推进:
- 开始准备文章节点
- 等待图片、字体和图表资源完成
- 主线程分段截图
- Worker 编码压缩 PNG
- 最后合成并触发下载
这件事的意义很现实。
当用户在导出一篇万字长文的时候,他至少知道现在是在准备资源,还是已经开始编码,还是马上就好了。
这种可感知的过程,会比你单纯把按钮改成一个转圈圈,体验好太多。
新增哪些库:html2canvas-pro 和 jsPDF#
前端文章导出这件事,说到底还是在浏览器里把一段真实 DOM 变成另一个文件格式。
这里我最后选的是两条比较明确的路线。
长图导出这边,我用的是 html2canvas-pro。
原因也很直接。
我这个博客本身已经用了不少现代 CSS 写法,还带有比较复杂的文章内容结构。
相比老一些的实现,html2canvas-pro 在这类场景下会更稳一点,尤其是对一些现代颜色函数和复杂样式的兼容会更友好。
PDF 导出这边,我用的是 jsPDF。
思路也不复杂。
先把文章导出舞台渲染出来,再按分页切片生成 PDF 页面。
这样就能让文章详情页按博客主题的视觉风格导出成文档,而不是变成一种完全脱离当前阅读体验的陌生排版。
最难搞的,其实不是大图,而是细节#
真正把这套东西做顺以后,我越来越觉得,文章导出这件事,最折腾人的从来不是按钮,也不是文件保存。
而是正文里的那些细节。
比如:
- Mermaid 图要先变成稳定可导出的静态图形
- PlantUML 要确保在导出时能拿到真实资源
- 一些网站卡片、GitHub 卡片、头像和图标要在导出前先冻结
- 行内代码在浏览器里能正常换行,但导出时很容易被截图引擎搞乱
后面我专门又补了一层导出专用处理。
尤其是行内代码这块。
因为正文里像 contentHash + currentRevision 健康状态 + forceRebuild 这种会跨行的 inline code,在浏览器里看着没问题,导成长图时却很容易整块背景被拉长、文字错位,甚至出现丢字。
就像下图这样:

所以我最后没有再继续硬顶原始 code 标签,而是在导出前把这类已经发生换行的行内代码重建成更稳定的导出结构,让它尽量按当前正文的换行结果去截图。
这一步其实很关键。
因为用户真正感知到的,不是你用了什么库。
而是导出来一看,觉得这张长图和他刚才在页面里看到的文章,是不是同一篇东西。
文章详情页现在怎么开启下载#
这次我也顺手把文章级开关一起做了。
也就是说,不是所有文章都必须开放下载。
你可以在文章 frontmatter 里按篇控制。
如果你想开放 PDF 导出,可以写:
1pdfExport: true如果你想开放长图导出,可以写:
1imageExport: true现在页面上会统一显示一个 下载 按钮。
背后到底走 PDF 还是长图,由文章自己的 frontmatter 来决定。
这种方式也比较好。
因为它没有把下载能力做成一种全站强制功能,而是把控制权留给了每篇文章本身。
如果你想体验这套长图导出,最适合去试哪一篇#
如果只是随便找一篇短文章来点下载,其实很难感觉出这套设计值不值。
真正能看出差别的,还是长文。
所以如果你想直接体验这次长图导出的效果,我最推荐你去试这篇:
这篇本身就是一篇万字级长文,正文很长,里面还有不少图、代码、流程说明和技术拆解。
拿它来做长图导出测试,基本能比较完整地感受到这套策略到底解决了什么问题。
包括:
- 长文导出时是不是还清晰
- 页面是不是没有以前那么容易卡住
- 进度反馈是不是更有用
- 最后导出来的长图,是不是还能保持文章正文那种连续阅读感
最后#
这次看起来像是在做一个下载按钮。
但做着做着,我反而越来越觉得,这更像是在给博客补一层内容分发能力。
页面内阅读是一种体验。
导出成长图、导出成 PDF,又是另一种体验。
如果这两者之间的落差特别大,那下载功能就只是看起来有。
但如果导出来的结果,真的还能保住这篇文章原本的阅读感觉,那这个功能才算真正落地了。
至少到现在这个阶段,我觉得它已经开始往这个方向靠近了。
后面如果我继续优化,我最想做的事大概还是两件:
- 再继续压一压超长文章导出的等待体感
- 再继续收细节,让导出的版式更像当前正文本身
反正现在,它已经不是那个一导长文就让浏览器看起来像快卡死的版本了。
如果这篇文章对你有帮助,欢迎分享!
部分信息可能已经过时
















































