做项目的都知道,越是不起眼的需求越容易在验收时翻车。最近帮一个老牌办公系统做功能改造,客户提了个平常到不能再平常的需求:内容在KindEditor里编辑完,点一下“导出PDF”,整个文档要自动转成PDF存进档案库。乍一听,这不就是HTML转PDF吗?可一往下聊就发现,项目所在的环境对控件和组件有明确的国产化要求,wkhtmltopdf、无头Chrome那一套工具链根本过不了选型评审。于是这个“边角需求”直接升级成了整场改造里最麻烦的模块之一。
这篇文章就从这次改造讲起,完整拆解在KindEditor这类老牌富文本编辑器上,如何用满足国产化要求的服务端控件/组件,把编辑器内容自动格式转换成PDF。内容包括方案取舍、环境准备、核心实现流程,以及我实际踩过的坑。适合正在维护遗留办公系统、又撞上组件国产化替换需求的朋友参考。
1. 需求不是“转PDF”,而是“在受限环境下把HTML渲染成可归档PDF”
1.1 KindEditor转PDF的典型场景
用到KindEditor的系统,绝大多数是上了年纪的办公类系统:公文管理、公告发布、简历填写、问题单、合同备忘、多人在线协作编辑页面。KindEditor当年能火,靠的是体积小、上手快、中文支持好,集成到后台管理界面非常顺手。但它在输出内容的时候,交出来的是一段“自由散漫”的HTML——标题、段落、图片位置全靠class甚至直接靠全局样式表撑着。它不是一个抽象语法树,更不算一份结构化的文档,它就是“页面里某个可编辑区域的DOM快照”。
一旦有PDF归档需求,问题就暴露了。PDF是一种对排版稳定性和字体嵌入要求极高的格式,而KindEditor输出的HTML恰恰在这两点上都极其松散。我接触到的需求基本就三类:一是点按钮手动导出,用于审批流程留档;二是保存后自动转PDF,用于跨系统归档或者做全文检索;三是把历史存量文章批量转成一批PDF,用于电子档案迁移。这三类场景对性能和异步任务的要求完全不同,方案设计不能一概而论。
先说需求一,最稳妥的做法就是同步转换:前端把内容提交到后端,后端在请求周期内完成PDF生成,直接把文件流返回浏览器下载。需求二就必须考虑异步化,因为保存动作本身不能因为转换失败而卡住主流程,否则用户会明显感知到保存变慢。需求三则是批处理场景,需要任务队列、失败重试、进度反馈,和前两个的架构设计差别很大。
1.2 三条技术路线的取舍
针对这个需求,业内早就有现成方案,但每条路线的适用条件完全不同,我先给结论:在带“国产化控件选型”约束的项目里,前两条路线大概率会在评审阶段被否掉,但理解它们的前因后果很有必要。
第一条是纯前端转换。常见做法是html2canvas把KindEditor的编辑区域截图,再用jsPDF把图贴进PDF;或者先把DOM和样式重新拼一遍布局,再用开源库直接输出PDF。优点是根本不用动服务端,部署压力小。缺点是html2canvas是“截图思维”,对复杂表格、滚动区域、浮动层经常截不全;jsPDF对中文排版支持弱,中文字体嵌入要做大量额外工作;分页基本靠猜,PDF里的文字出现截半行的情况很常见。真要硬抗纯前端方案,最后代码量往往比后端方案还大,效果还不稳定。
第二条是国外主流渲染方案。效果最好的是wkhtmltopdf,以及用无头Chromium渲染的puppeteer系列工具。它们确实不叫“控件”,而是命令行工具或者服务端模块,渲染CSS的能力强,几乎能做到“所见即所得”。但要命的地方在于:部署Chromium依赖很多系统库,容器里还得处理沙箱权限,安全合规团队看到供应链清单里一堆半开源组件,第一轮就直接毙掉。所以在有国产化约束的项目里,这条路走得通但不长久,后面还要返工。
第三条就是本文要展开的做法:使用国产化PDF生成组件/引擎,部署在服务端,前端只保留一个“提交内容、拿回文件”的接口。这类引擎通常提供Java、C++或Python的SDK,有些还支持独立微服务部署方式。它们对内联HTML的解析能力不一定比无头浏览器强,但对“字体注册、页边距、页眉页脚、水印、重复表头”这些企业级PDF特性支持得非常好,可控性也强。这也是我认为在国产化环境里唯一站得住脚的路线。
1.3 “国产化控件”的真实形态,先破个误区
这里必须先破一个认知误区。十几年前一说控件,大家想到的是ActiveX,要浏览器里装客户端插件,基本只能跑在IE上。现在这种模式早就没有生存空间了,国产化环境下的浏览器大多基于Chromium内核,本身就不含IE的控件通道。所以现在项目文档里写“控件”,实际部署形态已经演变成两类。
一类是服务端SDK组件:以Jar包、Linux的so、Windows的dll形态存在,被业务系统直接调用,通过API传HTML、输出PDF流。另一类是独立的转换微服务:把PDF生成引擎封装成HTTP服务,业务系统通过POST请求提交内容,再取回文件。后者对跨语言系统更友好,也是我实际项
