PDF 生成模板编辑器
开源可视化 PDF 生成引擎,支持自定义模板,并提供面向开发者的 API,灵活构建文档生成流程。
ComPDFKit Web SDK 采用严格的三层架构。理解各层之间的边界对于定制 UI 和扩展 SDK 至关重要。
集成方页面
└─ webviewer.js(iframe 嵌入 — packages/core/webviewer.js)
└─ Webview Vue 应用(packages/webview — 开源)
└─ Core 桥接层(packages/webview/src/core/)
└─ ComPDFKitViewer(packages/core — 闭源)
└─ Web Worker(compdfkit_worker.js)
└─ WASM(ComPDFKit.wasm / pdfium.wasm)| 层 | 位置 | 状态 | 是否开源 |
|---|---|---|---|
| 嵌入入口 | packages/core/webviewer.js | 创建 iframe | 是(薄层) |
| UI 层 | packages/webview | Pinia stores | 是 |
| Core 桥接层 | packages/webview/src/core/ | 无状态(仅一个注册表) | 是 |
| 引擎 | packages/core | 基于 WASM | 否 |
webviewer.js 集成方调用 ComPDFKitViewer.init(options, element)。它创建一个指向已构建 webview 应用的 <iframe>,与 iframe 进行配置握手,并从 iframe.contentWindow.instance 捕获 instance 对象({ UI, Core, docViewer })后解析返回。详见 快速开始。
packages/webview 一个 Vue 3 + Pinia 单页应用。它持有所有 UI 状态(哪些面板打开、当前工具、主题、工具栏定义)并渲染整个界面。它从不直接导入 packages/core——仅通过 core 桥接层与引擎通信。
子系统:
src/apis/ — 组装到 window.instance.UI(引擎接口组装到 window.instance.Core)的公共 API 接口。src/core/ — 无状态桥接层。src/components/ — 200+ 个 Vue 组件。src/stores/ — Pinia stores(viewer、document)。src/helpers/、src/hooks/、src/constants/。src/core/ 一个由约 120 个单函数模块组成的扁平门面,将调用转发到 ComPDFKitViewer 引擎实例。它不持有业务状态,也不进行 DOM 操作——其唯一的可变结构是 documentViewers Map(多实例注册表)。详见 Core 桥接层。
packages/core ComPDFKitViewer 类(src/index.js,约 5400 行)是 SDK 单体式核心。它管理 PDF 渲染、批注、表单、内容编辑和测量,并通过 MessageHandler 与 WASM worker 通信。它通过 Rollup 构建为 packages/webview/lib/webview.min.js(主库)和 lib/PDFWorker.js(worker),webview 在构建时导入。
端到端加载序列:
1. 集成方:ComPDFKitViewer.init(options, el)
2. webviewer.js:创建 iframe → webviewer/index.html?version=...#d=...&docId=...
3. iframe 启动:main.js → createApp(App= Suspenser.vue) → setupStore + i18n → mount('#app')
4. Suspenser.vue 在 <Suspense> 中渲染 <App/>(fallback:加载 logo)
5. App/index.vue setup():
await i18nextPromise // 翻译就绪
initDocument({ $t }) // 从 lib/webview.min.js 构造 new ComPDFKitViewer(options)
// → core.setDocumentViewer(1, documentViewer)
defineWebViewerInstanceUIAPIs() // window.instance = { docViewer, Core, UI }
await loadConfig(initOptions) // iframe↔父窗口握手,应用集成方 options
读取 hash 参数:css / theme / header
6. App onMounted:postMessage({ type: 'viewerLoaded', docId }) → window.parent
7. webviewer.js:捕获 iframe.contentWindow.instance,触发 'ready',解析 init Promise
main.js将两个插件应用于同一个 app:setupStore(app)(Pinia)和i18n(app)(i18next-vue)应用于同一个createApp(App)实例,再挂载。(早期版本为 i18n 创建了第二个createApp(App)并挂载它,导致启用了 Pinia 的 app 未被挂载——此问题已修复。)
instance 对象 定义于 packages/webview/src/apis/index.js:
window.instance = {
docViewer, // = core.getDocumentViewer()(实例 1)
Core: objForWebViewerCore, // 整个 core 桥接层的展开
UI: objForWebViewerUI, // 所有 apis/*.js 工厂,已注入 store
};objForWebViewerUI 由 apis/ 模块构建。每个模块是一个工厂 (stores...) => (...args) => result;index.js 在注册时注入 Pinia store,因此公共接口是内部函数。两种 store 访问模式并存:
useViewer、useDocument):API 直接调用 store 的 action/getter。大多数 API 使用此模式。viewerStore、documentStore,由 api_helpers/createEncapsulatedStore.js 构建):仅暴露 dispatch(action)(args) 和 getState(key)(返回深拷贝)。弹窗 API、getPassword 和 getPanels 使用此模式。桥接层通过 src/core/documentViewers.js 中的单个 Map 支持多个并存的 PDF 查看器:
const documentViewerMap = new Map();
export const getDocumentViewer = (number = 1) => documentViewerMap.get(number);
export const setDocumentViewer = (number, documentViewer, options) => { /* … */ };
export const deleteDocumentViewer = (number) => { documentViewerMap.delete(number); };1——主文档。无参数的 getDocumentViewer() 解析到实例 1。CompareDocumentContainer.vue 注册。docNum 参数;其余硬编码到实例 1。因此多实例是按方法可选的,并非整个 API 接口统一支持。apis/ → core/ → engine。API 从不直接导入 packages/core,而是通过 src/core/。桥接层从不触碰 Pinia store 或 DOM。src/core/ 文件不含业务逻辑——它们是纯委托函数。每个文件一个函数;文件名 === 函数名;docNum 始终是最后一个参数。apis/ 模块导出一个工厂,动词前缀(set/get/open/disable),含 JSDoc。core.eventBus())进行跨模块通信——annotationChange、toolChanged、scalechanging、pagerendered 等。详见 事件。ComPDFKitViewer.enqueue() 序列化异步操作,防止 WASM worker 上的竞态条件。document store 是引擎工具/样式状态的 UI 侧镜像——setActiveToolColor 和 setToolState 等 setter 既写入 store,也推送到 core。dispatch/getState 明确作为 API 访问通道,使公共接口与 Pinia 内部解耦。引擎中部分批注类型共享实现:
circle 映射到 square。ink 映射到 curve。ink 映射到 arc。在使用工具和批注样式时这些别名很重要。