@embedpdf-editor/chapter-snippet
v1.2.21
Published
框架无关的章节 PDF 阅读器运行时。它把 Preact 阅读器封装成 Web Component。
Readme
@embedpdf-editor/chapter-snippet
框架无关的章节 PDF 阅读器运行时。它把 Preact 阅读器封装成 Web Component。
Vue 2 请使用本包(@embedpdf-editor/chapter-snippet),不要用已移除的 @embedpdf-editor/vue2-chapter-viewer。宿主为 Vue 2 / 传统页面 / 微前端,或任何不想引入 React/Vue3 渲染器的场景。
能力与 React/Vue3 渲染器保持一致:多章连续滚动、划线/高亮、附注、段落书签、章节目录和标注导入导出。
安装
pnpm add @embedpdf-editor/chapter-snippetVite
chapterSnippetViteResolve() 用于 Vite + Vue2/snippet 场景:
// vite.config.ts
import { defineConfig } from 'vite';
import { chapterSnippetViteResolve } from '@embedpdf-editor/chapter-snippet/vite';
export default defineConfig({
...chapterSnippetViteResolve(),
});它会:
| 配置 | 作用 |
| --- | --- |
| server.headers | 开发环境添加 COOP/COEP,满足 worker/wasm 常见要求 |
| optimizeDeps.exclude | 避免 Vite 二次预构建 snippet 和 PDFium 引擎 |
Vue CLI / Webpack
无需修改 vue.config.js。 安装后直接:
import ChapterEmbedPDF from '@embedpdf-editor/chapter-snippet';发包构建会对 dist/embedpdf-chapter.js 做语法降级(去掉 ?? 等 ES2020 语法),Webpack / Vue CLI 默认可 parse。默认 wasmUrl 指向 jsDelivr 上的 pdfium.wasm,无需复制到 public/。
离线或内网部署时,可传 wasmUrl(init 顶层,与 options 同级):
// 相对路径:自行把 dist/pdfium.wasm 放到静态目录
wasmUrl: '/pdfium.wasm',
// 自有 OSS / CDN(完整 HTTPS 地址,示例)
wasmUrl:
'https://hep-editor.oss-cn-beijing.aliyuncs.com/public/editor-public/js/pdfium.wasm',详见 docs/get-started/01-installation.md。
@embedpdf-editor/chapter-snippet/webpack 为可选辅助(仅 monorepo Vue 2.6 解析、或自定义 devServer COOP/COEP),普通用户不必使用。
完整示例见 examples/chapter-viewer-demo-vue2。
快速开始
不需要把 pdfium.wasm 手动放到宿主项目的 public/。默认会从 @embedpdf-editor/chapter-snippet/dist/pdfium.wasm 加载。只有当你要走 CDN 或自定义静态域名时,才需要传 wasmUrl。
import ChapterEmbedPDF, {
CHAPTER_SNIPPET_EVENTS,
} from '@embedpdf-editor/chapter-snippet';
const viewer = ChapterEmbedPDF.init({
type: 'container',
target: document.getElementById('reader'),
worker: true,
options: {
manifest: {
chapters: [
{
chapterId: 'ch-1',
title: '第一章',
globalPageRange: [1, 12],
localPageRange: [0, 11],
source: { url: '/pdfs/chapter-1.pdf' },
},
],
},
notes: {
loadNotes: async () => [],
onUpdateNote: async () => {},
// 两阶段删除:业务先确认外部数据,返回 true 才让插件擦 PDF 笔记
onRequestDeleteNote: async () => true,
},
bookmarks: {
load: async () => [],
persist: async () => {},
onRequestRemove: async () => true,
},
features: {
markup: true,
bookmarks: true,
notes: true,
selectionToolbar: true,
zoom: { pageWidth: 720 },
},
},
});
viewer?.addEventListener(CHAPTER_SNIPPET_EVENTS.noteRequestCreate, (event) => {
const { record, complete } = event.detail;
openNoteDialog(record, async (content) => {
const noteId = 'note-' + Date.now();
const nodeId = await saveNote(content, record);
await complete({ noteId, content, nodeId });
});
});
viewer?.addEventListener(CHAPTER_SNIPPET_EVENTS.ready, (event) => {
console.log('registry ready', event.detail.registry);
});容器需要稳定高度:
#reader {
height: 100vh;
min-height: 0;
}配置
| 字段 | 默认 | 说明 |
| --- | --- | --- |
| type | 必填 | 目前使用 'container' |
| target | 必填 | 挂载目标元素 |
| options | 必填 | 推荐的章节阅读器配置,和 React/Vue3 的 options 语义一致 |
| wasmUrl | dist/pdfium.wasm | 自定义 PDFium wasm 地址 |
| worker | true | 是否启用 PDFium worker |
| fallbackToDirectEngine | true | 首章 worker 加载超时后自动降级到 direct engine |
| workerOpenTimeoutMs | 8000 | 首章加载超时时间 |
| features | - | 与 options.features 深度合并;顶层同名字段优先。勿放进 createChapterViewerEditorOptions() |
| className / viewportClassName | - | 传给内部阅读器容器 |
旧版本的 editorInput 仍可使用,但新代码应改为 options,并把 features 写在 options.features 或顶层 features。
PDF 三步加载(Vue 2 推荐)
| 步骤 | 说明 |
| --- | --- |
| 1 | manifest.chapters:仅页码;segmentPageThreshold 写在章节上 |
| 2 | chapterPdfLoader.loadChapterUrls:按章 getOneChap,每章只调一次 |
| 3 | chapterPdfLoader.openPdf(可选):解密等;省略则直接打开 ctx.url |
options: {
manifest: {
chapters: [
{
chapterId: item._id,
title: item.title,
globalPageRange: [item.startPage, item.endPage],
localPageRange: [0, item.endPage - item.startPage],
segmentPageThreshold: item.page,
},
],
},
chapterPdfLoader: {
async loadChapterUrls(chapter) {
const res = await getOneChap(chapter.chapterId);
if (!res.success) throw new Error(res.message);
const raw = res.data.resourceUrl;
return Array.isArray(raw) ? raw : raw ? [raw] : [];
},
async openPdf(ctx) {
const decrypted = await decryptPdf(ctx.url);
return { buffer: decrypted }; // ArrayBuffer / Uint8Array / DataView 都支持
},
},
notes: { /* ... */ },
bookmarks: { /* ... */ },
},勿在 loader 外再请求章节详情;勿把 segmentPageThreshold 放进 source。
03-manifest.md · 12-segmented-pdf-and-per-chapter-storage.md
按章持久化
options.notes / options.bookmarks 回调里的 chapterId、localPageIndex 与单 URL 章相同。划线备份用 exportChapterAnnotations,JSON 键为 chapters[chapterId](导出 markup 时会拉全部分段)。
ChapterViewerOptions(与 React / Vue3 一致)
| 字段 | 说明 |
| --- | --- |
| manifest | 章节目录,见 React README Manifest |
| notes | NoteCallbacks,见 React README |
| bookmarks | ParagraphBookmarkCallbacks,见 React README |
| chapterPdfLoader / overlapStrategy | 自定义 PDF 加载与重叠页策略 |
| features | 功能开关与 UI 定制,见下 |
加密 PDF 可在底层 createPdfChapterEditor 选项中设置 passwordProvider(如 CallbackPasswordProvider from @embedpdf-editor/chapter-core)。
features 配置
与 React / Vue3 同一套 ChapterViewerConfig(@embedpdf-editor/chapter-viewer)。完整字段表见 React README:features 配置。
| 模块 | 要点 |
| --- | --- |
| markup | styles 四类划线;annotationMenu 默认选中后在下方显示「删除」;onClick 配置后不弹菜单;squiggly.offsetY 默认 4 |
| bookmarks | marker / hover 自定义图标;marker.onClick 配置后不弹删除浮窗 |
| notes | marker.renderIcon、renderMenuActions、iconSize、offsetX/offsetY、highlightColor、highlightStyle(任意 CSS)、alwaysVisible;onClick(高亮区与图标同一事件)、selectedOutline |
| zoom | pageWidth、min / max / enabled;实际上限不超过 [data-chapter-scroll-viewport] 宽度,resize 时自动 clamp |
| scrollViewport | background(默认 #f1f5f9),[data-chapter-scroll-viewport] 背景 |
| page | background(默认 #ffffff),单页 PDF 画布背景 |
| selectionToolbar | 两行浮窗、色盘 markupColors、extraActions[].onClick / onExtraAction;或监听 DOM 事件 selectionExtraAction |
| pageOverlays | 页内矩形框;defaultBoxStyle(任意 CSS)、renderContent、onClick(ctx.payload);数据经 importChapterAnnotations 的 pageOverlays 注入 |
| markup | 除 styles 外可配 onCreated、onRequestDelete(Promise<boolean>,true 才真删)、onDeleted |
| bookmarks(options) | 除 load / persist 外可配 onAdded(新增书签,含 position) |
features: {
markup: {
styles: {
highlight: { color: '#fef08a', opacity: 0.45 },
underline: { color: '#dc2626', thickness: 1.5, offsetY: 2.5 },
squiggly: { color: '#dc2626', thickness: 1.5, offsetY: 4 },
},
annotationMenu: {
enabled: true,
renderMenu: ({ onDelete }) => {
const btn = document.createElement('button');
btn.textContent = '移除';
btn.onclick = onDelete;
return btn;
},
},
},
notes: {
marker: {
renderIcon: () => /* DOM */,
renderMenuActions: ({ onEdit, onDelete }) => /* DOM */,
// 默认 false:仅 hover 高亮时显示图标。
// true:图标常驻显示,hover 感知区仅覆盖图标本身。
alwaysVisible: true,
},
},
selectionToolbar: {
selectionBackground: 'rgba(255, 193, 7, 0.35)',
// 复制默认开启,浮窗最左侧;隐藏:hiddenBuiltinActions: ['copy']
renderCopyIcon: () => {
const span = document.createElement('span');
span.textContent = '📋';
span.setAttribute('aria-hidden', 'true');
return span;
},
markupColors: ['#93c5fd', '#fde047'],
extraActions: [
{
id: 'translate',
label: '翻译',
onClick: ({ selectedText, chapterId, localPageIndex }) => {
console.log('翻译', selectedText, chapterId, localPageIndex);
},
},
],
// 或统一回调:onExtraAction: (ctx) => { ... }
},
markup: {
onCreated: ({ kind, strokeColor, position }) => {
console.log('新划线', kind, strokeColor, position);
},
},
},划词后点复制会将选中文本写入剪贴板。扩展按钮回调与 DOM 事件 detail 均含 selectedText。程序化:
import { copyTextToClipboard } from '@embedpdf-editor/chapter-snippet';
await copyTextToClipboard('文本');renderMenu / renderIcon / renderCopyIcon 在 snippet(Preact)内执行,请返回 DOM 或 Preact 节点;Vue 2 宿主不要用 h() 直接塞进 Shadow DOM。
pageOverlays(矩形叠加层)
features: {
pageOverlays: {
defaultBoxStyle: {
border: '2px dashed #ef4444',
cursor: 'pointer',
},
renderContent: ({ payload }) => {
const span = document.createElement('span');
span.textContent = payload ?? 'QR';
return span;
},
onClick: ({ overlay, payload }) => {
console.log(overlay.overlayId, payload);
},
},
},通过 importChapterAnnotations(registry, { chapterId, pageOverlays: [...] }) 注入数据;后端 QR 比例坐标转换见 13-page-overlays.md(qrTargetToPageOverlayRecord 由 React/Vue3 包导出,snippet 可手写 rectPdfCoord 或自行实现相同公式)。
事件
snippet 通过 DOM CustomEvent 抛出动作。常量见 CHAPTER_SNIPPET_EVENTS。
若在 options 里已写 onClick / onCreated / onAdded 等回调,仍会派发同名 DOM 事件(便于 Vue 2 只绑 @xxx)。
| 常量 | 事件名 | detail(关键字段) |
| --- | --- | --- |
| noteRequestCreate | chapter-note-request-create | record(含 selectedText、position)、complete({ noteId, content?, nodeId? }) |
| noteRequestEdit | chapter-note-request-edit | record、nodeId? |
| selectionExtraAction | chapter-selection-extra-action | actionId、selectedText、chapterId、rectsPdfCoord |
| bookmarkAdded | chapter-bookmark-added | bookmark、position |
| markupCreated | chapter-markup-created | kind、annotationId、strokeColor、position、record |
| setPdfPageBackground | chapter-set-pdf-page-background | background(CSS 颜色,派发以更新页背景) |
| pdfPageBackgroundChange | chapter-pdf-page-background-change | background(更新后通知) |
| ready | chapter-viewer-ready | registry(导出标注用) |
el.addEventListener('chapter-selection-extra-action', (e) => {
const { actionId, selectedText, chapterId } = e.detail;
if (actionId === 'translate') translate(selectedText, chapterId);
});
el.addEventListener('chapter-markup-created', (e) => {
const { strokeColor, position, record } = e.detail;
saveMarkup({ strokeColor, position, record });
});
// 运行时更新 PDF 页背景色
el.dispatchEvent(
new CustomEvent('chapter-set-pdf-page-background', {
detail: { background: '#1e293b' },
bubbles: true,
composed: true,
}),
);
el.addEventListener('chapter-pdf-page-background-change', (e) => {
console.log('page background', e.detail.background);
});markup record 顶层额外带 localPageIndex、globalPageIndex、globalPageNumber。宿主若持有 ChapterPdfViewer ref 或兼容的滚动对象,可直接调用 scrollToMarkupRecord(viewer, markupRecord)、scrollToNoteRecord(viewer, noteRecord)、scrollToBookmarkRecord(viewer, bookmarkRecord) 回到对应位置。
外部列表删除标记后,需要同步移除渲染器内存里的对象。snippet 没有 ChapterPdfViewer ref,可在 chapter-viewer-ready 里保存 registry 后调用插件 capability:
import { removeChapterMarkupAnnotation } from '@embedpdf-editor/chapter-snippet';
let registry;
el.addEventListener('chapter-viewer-ready', (event) => {
registry = event.detail.registry;
});
async function deleteNote(noteId) {
await api.deleteNote(noteId);
registry?.getPlugin('note')?.provides()?.removeNote(noteId);
}
async function deleteBookmark(bookmarkId) {
await api.deleteBookmark(bookmarkId);
registry?.getPlugin('paragraph-bookmark')?.provides()?.purgeBookmark(bookmarkId);
}
async function deleteMarkup(record) {
await api.deleteMarkup(record.nodeId ?? record.annotation.id);
if (registry) await removeChapterMarkupAnnotation(registry, record);
}removeChapterMarkupAnnotation(registry, record) 会把 chapterId + localPageIndex 解析成实际 documentId + pageIndex,分段 PDF 也可直接使用。
附注创建:宿主弹窗 → 保存 → 调用 complete({ noteId, content?, nodeId? }) 注册锚点(也可事先写 record.nodeId)。划线 onCreated、书签 onAdded 同样可写 record.nodeId 或 return { nodeId }。编辑/删除 DOM 事件与回调均带 nodeId。
完整回调表:11-events-callbacks-and-component-api.md
Vue 2 用法
Vue 2 组件只需要在 mounted 时初始化,在 beforeDestroy 时销毁:
import ChapterEmbedPDF from '@embedpdf-editor/chapter-snippet';
export default {
props: {
options: { type: Object, required: true },
},
mounted() {
this.viewer = ChapterEmbedPDF.init({
type: 'container',
target: this.$refs.host,
options: this.options,
worker: true,
});
},
beforeDestroy() {
this.viewer?.destroy?.();
},
};完整示例见 examples/chapter-viewer-demo-vue2。
标注导入导出
包内直接导出章节标注 IO:
import {
exportAllChapterAnnotations,
importChapterAnnotationsArchive,
chapterAnnotationsArchiveToJson,
parseChapterAnnotationsArchiveJson,
} from '@embedpdf-editor/chapter-snippet';在 snippet 中可通过 chapter-viewer-ready 事件拿到 registry,再调用这些 API。导出格式包含 bookmarks、notes、markup、pageOverlays,版本常量为 CHAPTER_ANNOTATIONS_ARCHIVE_VERSION。
| 选项 | 说明 |
| --- | --- |
| mode | replace 清空后导入;merge 合并 |
| ensureChapterLoaded | 默认 true;含 markup 的分段章会加载 全部段 再合并页码 |
| bookmarks / notes / markup / pageOverlays | 默认 true,可关闭某一类 |
| persistNotes / persistBookmarks | 导入后写回业务存储 |
示例见 examples/chapter-viewer-demo-vue2/src/components/AnnotationsDemoBar.vue。详见 10-annotations-io.md。
教程索引
| 主题 | 文档 |
| --- | --- |
| 目录 | docs/get-started/README.md |
| wasmUrl(含 OSS 示例) | 01-installation.md |
| 划词复制 | 07-selection-toolbar.md |
| 事件常量 | 11-events-callbacks-and-component-api.md |
| 分段 + 按章存储 | 12-segmented-pdf-and-per-chapter-storage.md |
| 页内矩形叠加层 | 13-page-overlays.md |
与 React / Vue3 渲染器的区别
| 项 | React / Vue3 | chapter-snippet(本包,含 Vue 2) |
| --- | --- | --- |
| 运行时 | 框架内 editor-engine 组件 | Shadow DOM Web Component + Preact |
| 初始化 | <ChapterPdfViewer options={...} /> | ChapterEmbedPDF.init({ type: 'container', target, options }) |
| 动态 manifest | 改 options.manifest(自动 setManifest) | 改 init 的 options.manifest(Preact 内同步) |
| Registry | onInitialized / useRegistry() | chapter-viewer-ready → detail.registry |
| Vite | chapterViewerViteResolve() | chapterSnippetViteResolve() |
许可
- 本包 JavaScript / 类型定义:MIT(见
LICENSE、NOTICE) dist/pdfium.wasm:来自 PDFium,适用 Apache License 2.0(见LICENSE.pdfium、NOTICE.pdfium、THIRD-PARTY-NOTICES.md)- 再分发本包或随产品打包 wasm 时,须保留上述许可文件
常见问题
| 现象 | 处理 |
| --- | --- |
| 卡在“正在加载 PDFium” | 检查 dist/pdfium.wasm 是否随包发布并能被浏览器访问;自定义部署时传 wasmUrl |
| 卡在“正在加载 xxx.pdf” | 优先确认章节 PDF URL 是否 200;如只在 worker 模式出现,可保留默认 fallback 或临时设 worker: false 定位环境问题 |
| 只显示空白 | 确认宿主元素有高度,且没有被父容器 overflow/flex 布局压到 0 |
| 划词后没有笔记弹窗 | 监听 chapter-note-request-create,保存后调用 detail.complete({ noteId, ... }) |
| Vue/Vite 开发环境异常预构建 | 使用 chapterSnippetViteResolve(),确保 snippet 和 PDFium 引擎没有被 optimizeDeps 预构建 |
| 分段章存了 #sN | 业务层只用 chapterId;引擎段 ID 勿写入库 |
