架构
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 | 否 |
1. 嵌入入口 — webviewer.js
集成方调用 ComPDFKitViewer.init(options, element)。它创建一个指向已构建 webview 应用的 <iframe>,与 iframe 进行配置握手,并从 iframe.contentWindow.instance 捕获 instance 对象({ UI, Core, docViewer })后解析返回。详见 快速开始。
2. UI 层 — 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/。
3. Core 桥接层 — src/core/
一个由约 120 个单函数模块组成的扁平门面,将调用转发到 ComPDFKitViewer 引擎实例。它不持有业务状态,也不进行 DOM 操作——其唯一的可变结构是 documentViewers Map(多实例注册表)。详见 Core 桥接层。
4. 引擎 — 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 访问模式并存:
- 直接 Pinia store(
useViewer、useDocument):API 直接调用 store 的 action/getter。大多数 API 使用此模式。 - 封装 store(
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。 - 实例 2 和 3 用于文档对比功能("旧文件"和"新文件"查看器),由
CompareDocumentContainer.vue注册。 - 约 120 个桥接文件中仅约 13 个显式传递
docNum参数;其余硬编码到实例 1。因此多实例是按方法可选的,并非整个 API 接口统一支持。
关键设计模式
- 单向依赖规则:
apis/ → core/ → engine。API 从不直接导入packages/core,而是通过src/core/。桥接层从不触碰 Pinia store 或 DOM。 - 无状态桥接层:
src/core/文件不含业务逻辑——它们是纯委托函数。每个文件一个函数;文件名 === 函数名;docNum始终是最后一个参数。 - 每个文件一个函数(apis): 每个
apis/模块导出一个工厂,动词前缀(set/get/open/disable),含 JSDoc。 - EventBus: 引擎使用自定义 EventBus(
core.eventBus())进行跨模块通信——annotationChange、toolChanged、scalechanging、pagerendered等。详见 事件。 - 消息队列:
ComPDFKitViewer.enqueue()序列化异步操作,防止 WASM worker 上的竞态条件。 - store 镜像引擎: Pinia
documentstore 是引擎工具/样式状态的 UI 侧镜像——setActiveToolColor和setToolState等 setter 既写入 store,也推送到core。 - 封装 store 接缝: 两个 store 上的
dispatch/getState明确作为 API 访问通道,使公共接口与 Pinia 内部解耦。
批注类型别名
引擎中部分批注类型共享实现:
- 无测量的
circle映射到square。 - 有测量的
ink映射到curve。 - 带弧点的
ink映射到arc。
在使用工具和批注样式时这些别名很重要。