React中docxtemplater生成的docx BLOB如何转换为保留原模板的PDF
问题描述
- 技术栈为React.js + Node.js,已通过docxtemplater实现基于用户输入生成Word文档的功能,运行正常。
- 需求为支持用户直接下载生成内容对应的PDF格式文件,docxtemplater本身仅支持导出docx格式,无法直接生成PDF。
- 已实现流程:
- 生成docx格式的BLOB文件
- 通过GridFS将生成的docx BLOB存入MongoDB
- 已掌握通过GET请求拉取MongoDB中存储的BLOB文件的逻辑
- 卡点:拉取到docx格式的BLOB后,无法转换为完整保留原docx模板样式的PDF文件。
现有业务代码
import React, { useState, useEffect } from "react"; import Docxtemplater from "docxtemplater"; import PizZip from "pizzip"; import PizZipUtils from "pizzip/utils/index.js"; import { saveAs } from "file-saver"; import axios from "axios"; function loadFile(url, callback) { PizZipUtils.getBinaryContent(url, callback); } export const DocxFile = ({ formData }) => { const [file, setFile] = useState(null); const [currentlyUploading, setCurrentlyUploading] = useState(false); const [docxId, setDocxId] = useState(null); const [progress, setProgress] = useState(null); const [inputContainsFile, setInputContainsFile] = useState(false); const [template, setTemplate] = useState(); const generateDocument = () => { loadFile(template, function (error, content) { if (error) { throw error; } var zip = new PizZip(content); var doc = new Docxtemplater(zip, { paragraphLoop: true, linebreaks: true, }); doc.setData({ company: formData.general.companyClient, version: formData.documentVersion, quote: formData.general.quoteNumber, HERE MY DATA //Working :) }); try { doc.render(); } catch (error) { function replaceErrors(key, value) { if (value instanceof Error) { return Object.getOwnPropertyNames(value).reduce(function ( error, key ) { error[key] = value[key]; return error; }, {}); } return value; } if (error.properties && error.properties.errors instanceof Array) { const errorMessages = error.properties.errors .map(function (error) { return error.properties.explanation; }) .join("\n"); } throw error; } var out = doc.getZip().generate({ type: "blob", mimeType: "application/vnd.openxmlformats-officedocument.wordprocessingml.document", }); saveAs( out, `${name}.${documentVersion}.docx` ); setFile(out); setInputContainsFile(true); }); }; const fileUploadHandler = () => { const fd = new FormData(); fd.append("docx", file, file.name); axios .post(`api/docx/upload`, fd, { onUploadProgress: (ProgressEvent) => { setProgress((ProgressEvent.loaded / ProgressEvent.total) * 100); console.log( "upload progress", Math.round((ProgressEvent.loaded / ProgressEvent.total) * 100) ); }, }) .then(({ data }) => { setDocxId(data); setFile(null); setInputContainsFile(false); setCurrentlyUploading(false); }) .catch((err) => { console.log(err); if (err.response.status === 400) { const errMsg = err.response.data; if (errMsg) { console.log(errMsg); } } else { console.log("other error", err); setInputContainsFile(false); setCurrentlyUploading(false); } }); }; const handleClick = () => { if (inputContainsFile) { setCurrentlyUploading(true); fileUploadHandler(); } }; return ( <> <button className="modify__button1 enregistrer generator__button" onClick={generateDocument} > Generate the docx </button> <label htmlFor="file" onClick={handleClick}> {file ? <>SUBMIT</> : <></>} </label> </> ); };
上传完成后MongoDB中存储的BLOB记录如下图:
解决方案
不要在前端做docx转PDF,所有前端侧的转换库对docx样式的兼容度都极低,复杂模板的排版、字体、格式大概率错乱,转换逻辑必须放在Node.js后端实现。
目前开源方案里对docx样式还原度最高的方案是调用LibreOffice无头模式做转换,和本地用Word打开docx再导出PDF的效果基本一致,能100%保留原有模板的样式、表格、页眉页脚、图片等元素。
实现步骤
环境依赖安装
服务端先安装LibreOffice:- Ubuntu/Debian系统执行命令:
apt install libreoffice libreoffice-writer - CentOS/RHEL系统执行命令:
yum install libreoffice libreoffice-writer - Windows本地开发环境直接安装LibreOffice桌面版即可,记住安装路径。
Node.js侧安装转换封装依赖:npm install libreoffice-convert
- Ubuntu/Debian系统执行命令:
后端转换逻辑
从GridFS拉取到docx的Buffer数据后,直接调用转换方法即可,示例代码:const libre = require('libreoffice-convert'); /** * docx Buffer转PDF Buffer * @param {Buffer} docxBuffer 从GridFS读取的docx文件Buffer * @returns {Promise<Buffer>} 转换后的PDF Buffer */ async function docxToPdf(docxBuffer) { return new Promise((resolve, reject) => { libre.convert(docxBuffer, '.pdf', undefined, (err, pdfBuffer) => { if (err) return reject(err); resolve(pdfBuffer); }); }); } // 下载PDF的接口示例 router.get('/api/docx/pdf/:id', async (req, res) => { try { // 这里是你已经实现的从GridFS读取docx的逻辑 const docxBuffer = await getDocxFileFromGridFS(req.params.id); const pdfBuffer = await docxToPdf(docxBuffer); // 设置响应头,直接返回PDF流供前端下载 res.setHeader('Content-Type', 'application/pdf'); res.setHeader('Content-Disposition', 'attachment; filename="导出文档.pdf"'); res.send(pdfBuffer); } catch (err) { res.status(500).send('文档转换失败'); } });前端调用下载
前端请求接口拿到PDF Blob后,直接用已经引入的file-saver保存即可:const downloadPdf = async (docxId) => { const res = await axios.get(`/api/docx/pdf/${docxId}`, { responseType: 'blob' }); saveAs(res.data, '导出文档.pdf'); }
注意事项
- 如果docx模板使用了特殊字体,必须把对应字体文件安装到运行LibreOffice的服务器上,否则会出现字体替换导致的排版错位。安装字体后执行
fc-cache -fv刷新字体缓存即可生效。 - 不要尝试用纯JS实现的docx转PDF库,这类库是自行解析docx结构重新排版,对表格合并、文本框、特殊缩进、页眉页脚等复杂样式的支持极差,还原度完全达不到业务要求。
- 如果项目部署在无法安装系统依赖的Serverless环境,可以用Docker部署Gotenberg服务作为转换接口,本质还是封装了LibreOffice的转换能力,只需要传文件就能返回PDF,不需要额外装系统依赖。
内容的提问来源于stack exchange,提问作者Elodie Muller
相关产品推荐
相关产品推荐

