Vue.js - 富文本编辑器Tiptap使用详解14(将内容导出为PDF)
作者:hangge | 2026-03-06 08:30
前文我演示了如何将 Tiptap 内容导出为 Word (点击查看),本文演示如何实现将 Tiptap 内容导出为 Pdf 文件,同样有纯客户端导出,以及由服务端导出两种方案。
(2)使用 html2pdf.js 特别要注意如下几点:
(2)关键代码说明:



十四、将内容导出为 PDF(客户端导出)
1,安装依赖
(1)要实现客户端导出,我们需要先安装 html2pdf.js 这个依赖库。
提示:html2pdf 其实就是 jsPDF + html2canvas 的封装。
npm install html2pdf.js
- 整个过程中,无论我们在页面上是文字、SVG、路径、删除线、表格,最终都被 html2canvas 渲染成一张位图,jsPDF 再把这张图放入 PDF。
- 因此这些元素都不是矢量的,文字无法复制,且放大后会模糊。
DOM → html2canvas(渲染成 bitmap) → jsPDF(insertImage) → PDF
2,样例代码
(1)下面代码我们将编辑器的 HTML 注入到一个隐藏容器,设置打印样式后调用 html2pdf 导出。
<template>
<div class="p-4">
<div class="mb-3 flex gap-2">
<button @click="exportPdf" class="btn">导出为 PDF</button>
</div>
<div class="editor-box border rounded">
<editor-content :editor="editor" />
</div>
<!-- 隐藏的渲染容器,用于让 html2pdf 渲染带样式的内容 -->
<div ref="pdfContainer" style="display:none;"></div>
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent } from '@tiptap/vue-2'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
// 推荐:TableKit 一次引入所有 table 相关扩展
import { TableKit } from '@tiptap/extension-table'
import { TextStyleKit } from '@tiptap/extension-text-style'
import { Color } from '@tiptap/extension-color'
import Highlight from '@tiptap/extension-highlight'
import html2pdf from "html2pdf.js";
export default {
components: { EditorContent },
data() { return { editor: null } },
mounted() {
this.editor = new Editor({
extensions: [StarterKit, Link, Image, TableKit, TextStyleKit,
Color.configure({
types: ['textStyle'],
}),
// multicolor: true 允许自定义颜色
Highlight.configure({
multicolor: true,
}),
],
content: `
<p>欢迎访问<a href="https://www.hangge.com">hangge.com</a></p>
<p><img src="http://localhost:8080/images/logo.png" alt="logo"></p>
<p></p>
<p>测试<strong>加粗</strong>测试<em>斜体</em>测试<u>下划线</u>测试<s>删除线</s></p>
<p></p>
<p><span style="color: rgb(255, 102, 255);">修改文字颜色</span></p>
<p><span style="font-size: 22px;">调整文字大小</span></p>
<p></p>
<p>下面是一个3x3表格</p>
<table>
<thead>
<tr><th>ID</th><th>用户名称</th><th>用户年龄</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>刘小龙</td><td>88</td></tr>
<tr><td>2</td><td>张大伟</td><td>66</td></tr>
</tbody>
</table>
`,
})
},
methods: {
// 导出 PDF
async exportPdf() {
try {
// 1) 获取编辑器 HTML
const html = this.editor.getHTML();
// 2) 准备一个容器,注入 HTML 和必要样式
const container = this.$refs.pdfContainer;
container.innerHTML = ''; // 清空
const wrapper = document.createElement('div');
wrapper.className = 'pdf-export-wrapper';
wrapper.innerHTML = html;
container.appendChild(wrapper);
// 3) 注入或复制必要的 CSS(字体、印刷样式等)
this.injectStyles(container);
// 4) 配置 html2pdf 选项(A4、纵向、边距等)
const opt = {
margin: [10, 10, 10, 10], // mm: top/right/bottom/left
filename: 'tiptap-export.pdf',
image: { type: 'jpeg', quality: 0.98 },
html2canvas: { scale: 2, useCORS: true, allowTaint: false, logging: false },
jsPDF: { unit: 'mm', format: 'a4', orientation: 'portrait' },
pagebreak: { mode: ['css', 'legacy'] } // 支持 CSS page-break
};
// 5) 运行导出(返回 Promise)
await html2pdf().set(opt).from(wrapper).save();
} catch (err) {
console.error("导出 PDF 失败:", err);
this.$emit('export-error', err);
} finally {
// 可选:清理容器
// this.$refs.pdfContainer.innerHTML = '';
}
},
// 注入打印样式:控制行高、段落间距、表格样式、分页等
injectStyles(container) {1
const style = document.createElement('style');
style.innerHTML = `
/* 基本字体与行高 */
.pdf-export-wrapper {
font-family: "Arial", "Helvetica", sans-serif;
font-size: 12pt;
line-height: 1.6; /* <- 重点:增大行高 */
color: #222;
box-sizing: border-box;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
word-wrap: break-word;
overflow-wrap: break-word;
}
/* 段落与标题间距,避免挤在一起 */
.pdf-export-wrapper p { margin: 0 0 10px; }
.pdf-export-wrapper h1, .pdf-export-wrapper h2, .pdf-export-wrapper h3 {
margin: 6px 0 8px;
line-height: 1.3;
}
/* 表格样式: padding + 防止被拆行 */
.pdf-export-wrapper table { width: 100%; border-collapse: collapse; table-layout: auto; }
.pdf-export-wrapper th { background: #f9fafb; }
.pdf-export-wrapper th, .pdf-export-wrapper td {
padding: 8px 6px; /* <- 重点:给单元格足够高度 */
vertical-align: top; /* 避免垂直居中导致覆盖 */
border: 1px solid #ddd;
page-break-inside: avoid;
}
.pdf-export-wrapper tr { page-break-inside: avoid; break-inside: avoid; }
/* 表头跨页重复(如果支持) */
.pdf-export-wrapper thead { display: table-header-group; }
.pdf-export-wrapper tfoot { display: table-footer-group; }
/* 手动分页辅助类 */
.page-break { break-after: page; page-break-after: always; }
/* 图片不超过容器宽度并自动缩放 */
.pdf-export-wrapper img { max-width: 100%; height: auto; display: block; }
/* 防止子元素使用 transform/translate 导出错位,若有可手动覆盖 */
.pdf-export-wrapper [style*="transform"] { transform: none !important; }
`;
container.appendChild(style);
}
},
beforeDestroy() { this.editor?.destroy() }
}
</script>
<style scoped>
.btn { padding:6px 10px; border-radius:4px; border:1px solid #ddd; background:#fff; cursor:pointer }
.editor-box { min-height: 220px; padding: 12px; background:#fff; }
/* ====== 关键:为编辑器内 table 添加样式 ====== */
/* Vue2 scoped 下使用深度选择器确保样式能命中 ProseMirror 渲染的元素 */
/* 如果你的环境不识别 ::v-deep,请试试 >>> 或 /deep/ */
::v-deep .ProseMirror table {
width: 100%;
border-collapse: collapse;
table-layout: auto;
margin: 6px 0;
}
/* 单元格边框与内边距 */
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
border: 1px solid #d1d5db; /* 灰色边框,可按需调整 #e5e7eb #cbd5e1 等 */
padding: 8px 10px;
vertical-align: middle;
}
/* 表头样式(可选) */
::v-deep .ProseMirror th {
background: #f9fafb;
font-weight: 600;
}
/* 表格在窄屏时不要换行太多(可选) */
::v-deep .ProseMirror td {
word-break: break-word;
}
/* 如果使用 Tailwind 的 prose 可能会有默认的 table 样式,下面加个更高权重以覆盖 */
::v-deep .ProseMirror table,
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
/* 提高优先级避免被 reset 覆盖 */
border-color: #d1d5db;
}
/* Tiptap 表格单元格被选中时的高亮效果(与官网一致) */
::v-deep .ProseMirror .selectedCell {
background-color: rgba(200, 200, 255, 0.4) !important;
/* 防止 reset 覆盖 */
}
/** 下面是解决列宽拖拽调整时鼠标样式问题的 CSS */
/* 提升优先级:靠右侧 5~8px 时鼠标应变成 col-resize */
::v-deep .ProseMirror table td,
::v-deep .ProseMirror table th {
position: relative;
}
::v-deep .ProseMirror table td::after,
::v-deep .ProseMirror table th::after {
content: "";
position: absolute;
right: -3px; /* 命中区域 */
top: 0;
width: 6px;
height: 100%;
cursor: col-resize !important;
}
</style>
(2)关键代码说明:
- html2pdf.js 内部用 html2canvas 将 DOM 渲染为画布,再由 jsPDF 打包成 PDF。html2canvas 对外链图片有跨域问题,开启 useCORS 并确保图片允许跨域或使用 DataURL。
- html2canvas.scale 值越大清晰度越高,但文件越大。scale:2 常用。
- pagebreak.mode: ['css', 'legacy'] 让 html2pdf 支持 CSS page-break/break-after 等属性。
- 若样式不生效,可以把全局 CSS 复制到 container 中,或者在 wrapper 内添加必要的内联样式。
3,运行测试
(1)访问页面,点击“导出为 PDF”按钮后即可生成并下载 pdf 文件。

(2)打开下载下来的 pdf 文件,可以看到里面内容如下:

附:通过后端进行将内容导出为 PDF
1,实现原理
前端把 editor.getHTML()(或带全套 head/styles 的完整 HTML)POST 到后端 → 后端用 OpenHTMLToPDF 生成返回文档流 → 前端触发下载。
2,后端服务接口准备
这里我们使用 SpringBoot 项目作为后台服务,其提供 /export-pdf 接口,接收 JSON { html: "...", filename?: "xxx.docx" },返回 application/pdf 二进制流。具体实现参考我们之前文章:
3,前端代码
<template>
<div class="p-4">
<div class="mb-3 flex gap-2">
<button @click="exportViaServer" class="btn">服务端导出为 PDF</button>
</div>
<div class="editor-box border rounded">
<editor-content :editor="editor" />
</div>
</div>
</template>
<script>
import { Editor } from '@tiptap/core'
import StarterKit from '@tiptap/starter-kit'
import { EditorContent } from '@tiptap/vue-2'
import Link from '@tiptap/extension-link'
import Image from '@tiptap/extension-image'
// 推荐:TableKit 一次引入所有 table 相关扩展
import { TableKit } from '@tiptap/extension-table'
import { TextStyleKit } from '@tiptap/extension-text-style'
import { Color } from '@tiptap/extension-color'
import Highlight from '@tiptap/extension-highlight'
export default {
components: { EditorContent },
data() { return { editor: null } },
mounted() {
this.editor = new Editor({
extensions: [StarterKit, Link, Image, TableKit, TextStyleKit,
Color.configure({
types: ['textStyle'],
}),
// multicolor: true 允许自定义颜色
Highlight.configure({
multicolor: true,
}),
],
content: `
<p>欢迎访问<a href="https://www.hangge.com">hangge.com</a></p>
<p><img src="https://www.hangge.com/blog/images/logo.png" alt="logo"></p>
<p></p>
<p>测试<strong>加粗</strong>测试<em>斜体</em>测试<u>下划线</u>测试<s>删除线</s></p>
<p></p>
<p><span style="color: rgb(255, 102, 255);">修改文字颜色</span></p>
<p><span style="font-size: 22px;">调整文字大小</span></p>
<p></p>
<p>下面是一个3x3表格</p>
<table>
<thead>
<tr><th>ID</th><th>用户名称</th><th>用户年龄</th></tr>
</thead>
<tbody>
<tr><td>1</td><td>刘小龙</td><td>88</td></tr>
<tr><td>2</td><td>张大伟</td><td>66</td></tr>
</tbody>
</table>
`,
})
},
methods: {
async exportViaServer() {
const html = this.editor.getHTML();
console.log('html', html);
// 内联一些样式,提升导出效果
const style = `
<style>
body{ font-family: 'Arial', sans-serif; font-size:12pt; color:#111; }
table { border-collapse: collapse; width:100%;}
th { background: #F9FAFB; font-weight: 600; }
td, th { border: 1px solid #ccc; }
</style>
`;
const fullHtml = `<html><head><meta charset="utf-8">${style}</head>
<body>${html}</body>
</html>`;
try {
const resp = await fetch('http://localhost:8088/export-pdf', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ html: fullHtml, filename: 'tiptap-export.pdf' })
});
if (!resp.ok) {
const txt = await resp.text();
throw new Error('导出失败:' + txt);
}
const blob = await resp.blob();
// 下载
const url = window.URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'tiptap-export.pdf';
document.body.appendChild(a);
a.click();
a.remove();
window.URL.revokeObjectURL(url);
} catch (err) {
console.error(err);
alert('导出失败:' + err.message);
}
}
},
beforeDestroy() { this.editor?.destroy() }
}
</script>
<style scoped>
.btn { padding:6px 10px; border-radius:4px; border:1px solid #ddd; background:#fff; cursor:pointer }
.editor-box { min-height: 220px; padding: 12px; background:#fff; }
/* ====== 关键:为编辑器内 table 添加样式 ====== */
/* Vue2 scoped 下使用深度选择器确保样式能命中 ProseMirror 渲染的元素 */
/* 如果你的环境不识别 ::v-deep,请试试 >>> 或 /deep/ */
::v-deep .ProseMirror table {
width: 100%;
border-collapse: collapse;
table-layout: auto;
margin: 6px 0;
}
/* 单元格边框与内边距 */
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
border: 1px solid #d1d5db; /* 灰色边框,可按需调整 #e5e7eb #cbd5e1 等 */
padding: 8px 10px;
vertical-align: middle;
}
/* 表头样式(可选) */
::v-deep .ProseMirror th {
background: #f9fafb;
font-weight: 600;
}
/* 表格在窄屏时不要换行太多(可选) */
::v-deep .ProseMirror td {
word-break: break-word;
}
/* 如果使用 Tailwind 的 prose 可能会有默认的 table 样式,下面加个更高权重以覆盖 */
::v-deep .ProseMirror table,
::v-deep .ProseMirror th,
::v-deep .ProseMirror td {
/* 提高优先级避免被 reset 覆盖 */
border-color: #d1d5db;
}
/* Tiptap 表格单元格被选中时的高亮效果(与官网一致) */
::v-deep .ProseMirror .selectedCell {
background-color: rgba(200, 200, 255, 0.4) !important;
/* 防止 reset 覆盖 */
}
/** 下面是解决列宽拖拽调整时鼠标样式问题的 CSS */
/* 提升优先级:靠右侧 5~8px 时鼠标应变成 col-resize */
::v-deep .ProseMirror table td,
::v-deep .ProseMirror table th {
position: relative;
}
::v-deep .ProseMirror table td::after,
::v-deep .ProseMirror table th::after {
content: "";
position: absolute;
right: -3px; /* 命中区域 */
top: 0;
width: 6px;
height: 100%;
cursor: col-resize !important;
}
</style>
4,运行测试
(1)启动后台服务,访问前端页面,点击“服务端导出为 PDF”按钮后,浏览器会自动通过后端生成并下载 pdf 文件。

(2)打开下载下来的 docx 文件,可以看到文件内容如下:

全部评论(0)