架构设计
理解 InkLayer 的分层架构是深入使用和扩展的前提。本章说明各层的职责、数据流和设计决策。
分层架构总览
┌─────────────────────────────────────────────┐
│ 用户 API 层 │
│ PdfAnnotator / PdfViewer 组件 │
│ (React: FC + Provider、Vue: SFC + Slots) │
├─────────────────────────────────────────────┤
│ 扩展系统 (Extensions) │
│ Toolbar / Sidebar / SelectionBar │
│ Painter (Konva 绘制引擎) │
│ Editor (批注编辑器: 14 种类型) │
│ Transform (坐标编解码器) │
│ Store (状态管理: Zustand / Pinia) │
├─────────────────────────────────────────────┤
│ 核心抽象层 (Core) │
│ Annotation Core (框架无关数据模型) │
│ Adapter (Konva / PDF.js 适配器接口) │
│ Integration (存储/加载/导入/导出) │
│ Store Mapper (新旧格式双向映射) │
├─────────────────────────────────────────────┤
│ 基础设施层 │
│ PDF.js (~4.3) - PDF 渲染引擎 │
│ Konva (9.0) - Canvas 2D 图形 │
│ pdf-lib - PDF 操作 │
│ ExcelJS - Excel 导出 │
└─────────────────────────────────────────────┘
各层职责
1. 用户 API 层
提供给你的顶层接口,提供即开即用的组件:
- PdfAnnotator:完整的批注器,包含工具栏 + 编辑 + 侧边栏
- PdfViewer:轻量查看器,不包含批注功能
- React:Functional Component + Context Provider 模式
- Vue:Single File Component + Provide/Inject + Slots 模式
2. 扩展系统层
提供可拔插的功能扩展,用户可自定义替换:
| 模块 | 职责 | 可自定义? |
|---|---|---|
| Toolbar | 批注工具选择器(高亮、画笔、矩形…) | ✅ 完全替换 |
| Sidebar | 批注列表面板、搜索面板 | ✅ 完全替换 |
| SelectionBar | 文本选区的弹出操作栏 | ⚠️ 部分定制 |
| Painter | Konva 批注绘制引擎 | ❌ 底层核心 |
| Editor | 14 种批注类型的创建/编辑逻辑 | ⚠️ 可扩展 |
| Store | 批注状态管理(Zustand / Pinia) | ⚠️ 可读取 |
3. 核心抽象层
与 UI 框架和渲染引擎完全解耦,是 InkLayer 最重要的设计:
- Annotation Core:框架无关的批注数据模型,定义
Annotation的完整类型体系 - Adapter:渲染适配器接口,将 Annotation 映射到具体渲染引擎(Konva)
- Integration:存储格式、导入导出、格式兼容层
- Store Mapper:运行时状态与持久化数据之间的双向映射
设计亮点:React 和 Vue 版本共享完全相同的 Core 代码。这意味着你在一个包中学到的批注模型,可以直接复用到另一个包。修复 Core 中的 bug 会同时惠及两个框架。
4. 基础设施层
| 依赖 | 版本 | 作用 |
|---|---|---|
| PDF.js | ~4.3.136 | PDF 解析、页面渲染、文本内容提取 |
| Konva | ^9.0.0 | 基于 Canvas 的 2D 图形渲染,批注绘制 |
| pdf-lib | ^1.17.1 | PDF 操作(创建、修改、导出批注到 PDF) |
| ExcelJS | ^4.4.0 | 批注数据导出为 Excel |
| web-highlighter | ^0.7.4 | 网页文本高亮选区 |
框架绑定对比
| 维度 | React (inklayer-react) | Vue (inklayer-vue) |
|---|---|---|
| 组件模式 | Functional Component | SFC (Single File Component) |
| 状态管理 | Zustand | Pinia |
| 上下文 | React Context | Vue Provide/Inject |
| UI 组件库 | Radix UI Themes | shadcn-vue (reka-ui) |
| 样式方案 | SASS/SCSS | Tailwind CSS 4 + CVA |
| 国际化 | i18next + react-i18next | vue-i18n |
| 图标 | react-icons | @lucide/vue |
| Template 定制 | Render Props / children | Named Slots |
| 编程式操作 | Ref + imperativeHandle | Ref + defineExpose |
数据流
InkLayer 的数据流是单向的,从 Core 向外层传递:
写入路径:
用户操作 → Konva Node 创建/修改
→ Painter Editor 处理
→ Adapter.extract() 提取 Annotation
→ Store.commit() 更新状态
→ Integration 序列化 → 后端持久化
读取路径:
后端数据 → Integration.parse() 反序列化
→ Annotation[] 对象
→ Store.load() 载入状态
→ Adapter.render() 生成 Konva Node
→ Canvas 渲染显示
双框架共享设计
InkLayer 的两个包共享完全相同的核心模块。架构上通过以下方式实现:
- Core 模块零依赖:
annotation.core.ts、adapters/、integration.ts不引入任何 React/Vue 代码 - Adapter 模式解耦:Konva 渲染通过
AnnotationRendererAdapter接口隔离 - Store 各自实现:Zustand 和 Pinia 分别实现
IAnnotationStore接口 - UI 分别绑定:工具栏、侧边栏等 UI 组件分别用各自框架实现
自定义扩展
注册自定义批注 Adapter
import { AdapterRegistry } from 'inklayer-react/core'
const registry = AdapterRegistry.getInstance()
// 为自定义批注类型注册 Adapter
registry.register('custom-kind', {
render(annotation, context) {
// 自定义渲染逻辑
},
update(node, annotation, context) {
// 自定义更新逻辑
},
extract(node, context) {
// 自定义提取逻辑
}
})
自定义工具栏(React)
<PdfAnnotator
url="/doc.pdf"
actions={({ save, annotations }) => (
<button onClick={save}>
保存 ({annotations.length} 个批注)
</button>
)}
/>
自定义侧边栏(Vue)
<PdfAnnotator url="/doc.pdf">
<template #sidebar-header>
<MyCustomHeader />
</template>
<template #sidebar-content>
<MyCustomAnnotationList :annotations="store.annotations" />
</template>
</PdfAnnotator>