架构设计

理解 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文本选区的弹出操作栏⚠️ 部分定制
PainterKonva 批注绘制引擎❌ 底层核心
Editor14 种批注类型的创建/编辑逻辑⚠️ 可扩展
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.136PDF 解析、页面渲染、文本内容提取
Konva^9.0.0基于 Canvas 的 2D 图形渲染,批注绘制
pdf-lib^1.17.1PDF 操作(创建、修改、导出批注到 PDF)
ExcelJS^4.4.0批注数据导出为 Excel
web-highlighter^0.7.4网页文本高亮选区

框架绑定对比

维度React (inklayer-react)Vue (inklayer-vue)
组件模式Functional ComponentSFC (Single File Component)
状态管理ZustandPinia
上下文React ContextVue Provide/Inject
UI 组件库Radix UI Themesshadcn-vue (reka-ui)
样式方案SASS/SCSSTailwind CSS 4 + CVA
国际化i18next + react-i18nextvue-i18n
图标react-icons@lucide/vue
Template 定制Render Props / childrenNamed Slots
编程式操作Ref + imperativeHandleRef + defineExpose

数据流

InkLayer 的数据流是单向的,从 Core 向外层传递:

写入路径:
  用户操作 → Konva Node 创建/修改
    → Painter Editor 处理
    → Adapter.extract() 提取 Annotation
    → Store.commit() 更新状态
    → Integration 序列化 → 后端持久化

读取路径:
  后端数据 → Integration.parse() 反序列化
    → Annotation[] 对象
    → Store.load() 载入状态
    → Adapter.render() 生成 Konva Node
    → Canvas 渲染显示

双框架共享设计

InkLayer 的两个包共享完全相同的核心模块。架构上通过以下方式实现:

  1. Core 模块零依赖annotation.core.tsadapters/integration.ts 不引入任何 React/Vue 代码
  2. Adapter 模式解耦:Konva 渲染通过 AnnotationRendererAdapter 接口隔离
  3. Store 各自实现:Zustand 和 Pinia 分别实现 IAnnotationStore 接口
  4. 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>