一个框架无关的 HTML 转 PDF 库,基于 pdf-lib,默认支持中文(思源黑体)。
通过解析 DOM 的真实布局(getBoundingClientRect / Range)直接绘制 PDF 内容,不依赖 html2canvas,因此导出的是可选中、可搜索的矢量文本,而非位图截图。
框架无关。 包只导出一个工具函数
htmlToPdf,它只接收一个原生 DOM 元素,可在 Vue / React / 原生 JS 中直接使用。
- 框架无关:单个工具函数,任意框架可用,不牵入任何 UI 框架依赖
- 使用 pdf-lib 直接生成矢量 PDF(文本可选中、可搜索)
- 默认支持中文,内置思源黑体(Source Han Sans SC)
- 自动字体子集化:只嵌入页面实际用到的字形,文件通常约 500KB
- 完整的 TypeScript 类型支持
- 支持多页文档(内容流自动分页,或
data-pdf-page手动分页) - 支持常见 HTML/CSS:标题、段落、列表、表格、图片、Canvas、引用、代码块、粗体、斜体、颜色、背景、边框、圆角、字符间距、文本对齐
- 实验性支持
::before/::after伪元素(仅背景色和边框,需明确尺寸)
npm install @hmfw/html-to-pdf
# 或
yarn add @hmfw/html-to-pdf
# 或
pnpm add @hmfw/html-to-pdf本库不依赖任何 UI 框架,在 Vue / React / 原生 JS 中用法相同。
从 GitHub 仓库 下载字体文件到 public/fonts/ 目录:
# 需要的文件:
# - Source_Han_Sans_SC_Regular.woff
# - Source_Han_Sans_SC_Bold.woff确保字体可通过 /fonts/Source_Han_Sans_SC_Regular.woff 和 /fonts/Source_Han_Sans_SC_Bold.woff 访问。
💡 字体文件较大(每个约 13-14MB),不随 npm 包发布。由于字体子集化功能默认开启,PDF 最终只嵌入实际使用的字形(通常约 500KB)。
或使用 CDN / 自定义路径:
await htmlToPdf(element, {
fontPaths: {
regular: 'https://your-cdn.com/fonts/Source_Han_Sans_SC_Regular.woff',
bold: 'https://your-cdn.com/fonts/Source_Han_Sans_SC_Bold.woff',
},
})详见 自定义字体文档。
import { htmlToPdf } from '@hmfw/html-to-pdf'
const result = await htmlToPdf(element, { filename: 'document' })
// PDF 已自动下载
if (result.success && result.blob) {
// 如需自行处理,可使用 result.blob(上传、预览等)
}Vue 3 示例:
<template>
<div ref="pdfContainer" data-pdf>
<h1>标题</h1>
<p>这是一段中文内容</p>
<p>测试粗体:<strong>粗体文本</strong></p>
</div>
<button @click="handleExport" :disabled="exporting">
{{ exporting ? '生成中...' : '导出 PDF' }}
</button>
</template>
<script setup>
import { ref } from 'vue'
import { htmlToPdf } from '@hmfw/html-to-pdf'
const pdfContainer = ref(null)
const exporting = ref(false)
const handleExport = async () => {
exporting.value = true
try {
await htmlToPdf(pdfContainer.value, { filename: 'my-document' })
} finally {
exporting.value = false
}
}
</script>React / 原生 JS 示例:见 多框架使用文档。
当使用自定义字体时,如果字体中缺少某些字符,这些字符会显示为 ⛝ (U+26DD)。
推荐方案:使用字符转换
当使用繁体字库导出包含简体字的内容时(或反之),可以配置 OpenCC 进行字符转换:
await htmlToPdf(element, {
fontPaths: {
regular: '/fonts/SourceHanSansHK-Regular.otf', // 香港繁体字库
bold: '/fonts/SourceHanSansHK-Bold.otf'
},
converterOptions: { from: 'cn', to: 'hk' } // 简体→香港繁体
})常用配置:
{ from: 'cn', to: 'hk' }:简体→香港繁体(推荐){ from: 'cn', to: 'tw' }:简体→台湾繁体{ from: 'tw', to: 'cn' }:繁体→简体
读取元素真实布局生成 PDF,自动触发浏览器下载,并返回 { success, blob?, error? }。
{
filename?: string // 文件名(不含扩展名),默认 'document'
pageSize?: 'A4' | 'A3' | 'Letter' // 或自定义 { width, height }(单位 pt),默认 'A4'
orientation?: 'portrait' | 'landscape' // 页面方向,默认 'portrait'
fontPaths?: { // 自定义字体路径(可选)
regular?: string // Regular 字体 URL
bold?: string // Bold 字体 URL
}
basePath?: string // 部署基础路径,默认 '/'
fontSubset?: boolean // 是否子集化字体,默认 true
converterOptions?: { from: string; to: string } // OpenCC 字符转换配置
fontLoadTimeout?: number // 字体加载超时(毫秒),默认 30000
canvasResolver?: (canvas) => string | ArrayBuffer | null // 自定义 canvas 图片来源
canvasPixelRatio?: number // ECharts 自动探测兜底的像素比
debug?: boolean // 是否在控制台输出性能报告(默认 false)
}ECharts 高清图表示例:
import * as echarts from 'echarts'
await htmlToPdf(element, {
canvasResolver: (canvas) => {
const dom = canvas.closest('[_echarts_instance_]')
const inst = dom && echarts.getInstanceByDom(dom)
return inst ? inst.getDataURL({ type: 'png', pixelRatio: 3, backgroundColor: '#fff' }) : null
},
})PDF_CONTAINER_ATTR='data-pdf'— 标记导出根元素PDF_PAGE_ATTR='data-pdf-page'— 标记分页块
不加任何分页标记时,内容超过一页会按内容流自动分页,切页时尽量落在段落、标题、图片、表格行等不可分割元素的边界上:
<div data-pdf style="max-width: 794px; padding: 40px;">
<h1>很长的报告</h1>
<p>第一段……</p>
<!-- 内容超过一页时自动续到下一页 -->
<table>...</table>
</div>- 页边距「所见即所得」:直接由容器的 CSS
padding控制 - 内容放得下时仍输出单页
给导出根元素加 data-pdf,每个 data-pdf-page 标记一个 PDF 页面:
<div data-pdf>
<div data-pdf-page>
<div>第一页内容</div>
</div>
<div data-pdf-page>
<div>第二页内容</div>
</div>
</div>注意:
data-pdf-page可以不是data-pdf的直接子元素,允许嵌套包装元素data-pdf-page之间不可嵌套
为确保内容正确适配 PDF 页面,建议设置 data-pdf 容器的宽度与目标 PDF 页面宽度一致:
| 页面尺寸 | 推荐容器宽度 | CSS 设置 |
|---|---|---|
| A4 纵向 | 794px | max-width: 794px |
| A4 横向 | 1123px | max-width: 1123px |
| Letter | 816px | max-width: 816px |
.pdf-document {
max-width: 794px; /* A4 纸张宽度 */
margin: 0 auto;
padding: 40px; /* 页边距(自动分页时生效) */
background: white;
box-sizing: border-box;
}详见 常见问题 - 容器宽度设置。
| 类别 | 支持情况 |
|---|---|
| 文本 | 中英文混排、字号、颜色(hex / rgb / rgba) |
| 字重 | Regular / Bold(font-weight ≥ 600 使用 Bold) |
| 斜体 | italic / oblique(通过 skew 变换模拟) |
| 图片 | <img>(PNG / JPG / SVG)、<canvas>、内联 <svg> |
| 盒子样式 | 背景色、透明度、边框(逐边)、圆角 |
| 伪元素 | ::before / ::after(仅背景色和边框,需明确尺寸,详见文档) |
| 结构 | 表格、列表、引用、<pre>/<code>(保留换行) |
| 间距 / 对齐 | letter-spacing、text-align |
| 不支持 | emoji、阴影、渐变背景、变换、复杂 flex/grid 重排 |
完整特性列表见 常见问题 - 支持的特性。
仅支持现代浏览器,不支持 IE:
| 浏览器 | 最低版本 |
|---|---|
| Chrome | 90 |
| Firefox | 88 |
| Safari | 14 |
| Edge | 90 |
npm install # 安装依赖
npm run dev # 开发模式(启动示例 src/App.vue)
npm run build # 构建库(生成 dist + 类型声明)
npm run type-check # 类型检查
npm test # 运行单元测试构建产物:dist/index.mjs(ESM)、dist/index.cjs(CJS)、dist/index.d.ts(类型)。
- 多框架使用指南 - Vue / React / 原生 JS 完整示例
- 自定义字体 - 字体路径配置、繁体字库、授权说明
- 字符转换 - OpenCC 简繁转换配置
- 常见问题 - 缺字、字体加载失败、容器宽度、已知限制
- 伪元素支持 -
::before/::after使用说明
MIT