基于 snapdom-plugin-template 开发 SnapDOM v3 插件:从脚手架到发布完整指南
【免费下载链接】snapdomHigh-performance engine for capturing, modifying, and converting DOM elements into any format.项目地址: https://gitcode.com/GitHub_Trending/sn/snapdom
本指南以仓库中的 插件模板说明 为主体,结合 插件规范、插件核心实现 与官方插件包 packages/plugins 中的源码级证据,系统讲解 SnapDOM v3 插件从脚手架搭建、生命周期钩子开发、自定义导出,到 npm 发布的全流程。读完本文,你将能独立完成一个"工厂函数 + 生命周期钩子 + 自定义导出"的标准 SnapDOM 插件,并正确声明其运行阶段(needs)与纯度(pure),在捕获、变换、导出、集成四类场景中落地。
模板是什么:一个零依赖的插件起步包
packages/plugin-template是 SnapDOM v3 官方的插件脚手架,其 package.json 表明它是一个独立的 npm 包("type": "module"、"main": "index.js"、入口为./index.js)。关键设计是:模板对核心没有相对依赖,不依赖 monorepo 的目录结构即可独立工作,因此你可以把它复制到任何位置开发插件。
模板的核心文件是 index.js,它导出一个工厂函数myPlugin(options):
export function myPlugin(options = {}) { const { example = 'default', } = options; return { name: 'my-plugin', // Pick the hook(s) you need. Delete the rest. // Full lifecycle: beforeSnap → beforeClone → resolveNode → afterClone → beforeRender → afterRender // → defineExports → [beforeExport → exporter → afterExport] → afterSnap afterClone(ctx) { // Runs after cloning + style inlining. ctx.clone is the cloned DOM tree. // This is the most common hook — modify the clone here. }, // ...其余钩子以注释形式给出 }; } export default myPlugin;从模板源码可以提炼出插件的三条基本规范:
- 工厂函数模式:接受
options对象并给出默认值,返回插件实例。模板中默认导出myPlugin(同时保留具名导出与默认导出,便于不同导入方式)。 - 唯一
name:name用于去重与"局部覆盖全局"的优先级判定(见下文源码分析),模板使用 kebab-case 的'my-plugin'。 - 按需挑选钩子:模板把所有钩子以注释形式列出,开发者只取消注释自己需要的部分,其余删除。
安装与脚手架:两条等价路径
路径一:直接安装核心作为开发依赖
npm install --save-dev @zumer/snapdom@latest路径二:用 degit 从模板脚手架化(推荐)
npx degit zumerlab/snapdom/packages/plugin-template snapdom-plugin-yourname cd snapdom-plugin-yourname npm install --save-dev @zumer/snapdom@latestdegit会将模板目录克隆为snapdom-plugin-yourname(注意 npm 社区插件命名约定为snapdom-plugin-[name])。模板没有相对核心依赖,因此脱离 monorepo 布局也能独立安装运行。发布前请务必替换包名、描述与示例 option,并将peerDependencies的版本范围(模板中为"@zumer/snapdom": "^3.0.0")与你实际测试过的核心版本对齐——官方 插件规范 与安装说明始终跟踪已发布的核心版本。
使用方式:在捕获中挂载插件
模板 README 给出的最小使用示例:
import { snapdom } from '@zumer/snapdom'; import { myPlugin } from './index.js'; const result = await snapdom(element, { plugins: [myPlugin({ example: 'value' })] }); const image = await result.toPng();plugins选项支持两种注册方式:per-capture(每次捕获)与全局注册。全局注册方式为snapdom.plugins(myPlugin()),适用于所有捕获;per-capture 方式在单次捕获中生效。两者叠加时,从 插件核心 的mergePlugins实现可以看出:per-capture 插件优先于全局插件,同名插件会被去重(后者覆盖前者)。该模块还允许三种插件定义形式:纯对象、[factory, options]数组、{ plugin, options }对象(见normalizePlugin),模板使用的工厂函数形式是官方推荐的标准形态。
Options 设计:参数表与实现对应
模板 README 给出了示例参数表:
| Option | Type | Default | Description |
|---|---|---|---|
example | string | 'default' | Describe this option |
对应 index.js 中的解构实现:
export function myPlugin(options = {}) { const { example = 'default' } = options; return { name: 'my-plugin', afterClone(ctx) { /* ... */ } }; }这是工厂模式的标配:所有选项必须有默认值(解构赋默认值),这样myPlugin()不带参数调用也不会崩溃。官方插件 context-export 是更完整的范例——它解构了format、maxTextLength、maxNodes、geometry四个选项并各自给出默认值,还通过options.needs ?? 'render'暴露了运行阶段选项。发布前请把example替换为你的真实选项,并在 README 中用同样的三列格式(Option/Type/Default/Description)记录每个参数。
生命周期钩子:模板钩子背后的完整规范
模板 README 明确说明:afterClone修改克隆后的 DOM 树,模板中的钩子体为空,直到你添加自己的实现;插件还可以通过defineExports增加 HTML 或结构化上下文等输出。要真正理解这些钩子,需要结合 PLUGIN_SPEC.md 与 src/core/plugins.js 的实现。
完整钩子时序
beforeSnap → beforeClone → resolveNode (per node) → afterClone → beforeRender → afterRender → defineExports → [beforeExport → exporter → afterExport] → afterSnap其中resolveNode对捕获子树中的每个源节点执行;方括号内的导出段对每一次导出调用执行;afterSnap在首次成功导出后执行一次。
各钩子的运行时机与典型用途:
| Hook | 运行时机 | 常见用途 |
|---|---|---|
beforeSnap | 捕获工作开始前 | 校验选项、设置默认值 |
beforeClone | DOM 被克隆前 | 预处理源 DOM(须在 afterClone 中撤销) |
resolveNode | 克隆构建期间,逐节点 | 替换/跳过单个节点(脱敏、自定义组件) |
afterClone | DOM 与样式克隆完成后 | 变换克隆:叠加层、样式、替换 |
beforeRender | 选定的渲染器运行前 | 调整克隆或生成的 CSS |
afterRender | 渲染产物生成后 | 读取ctx.dataURL/ctx.meta;SVG 路径下还有ctx.svgString |
defineExports | 捕获后的结果构建期 | 新增导出格式(toPdf、toAscii) |
beforeExport | 每次导出调用前 | 调整导出选项(质量、尺寸) |
afterExport | 每次导出调用后 | 观察导出结果(日志、上传、测量) |
afterSnap | 首次成功导出后,一次 | 清理捕获范围内的资源 |
钩子上下文 ctx
捕获类钩子(beforeSnap到afterRender以及afterSnap)共享同一个上下文对象ctx,其中既有归一化后的捕获选项(scale、dpr、width、height、backgroundColor、quality、useProxy、cache、embedFonts、filter、exclude、engine等),也有各阶段产生的中间值:clone(克隆 DOM 树)、nodeMap、classCSS/fontsCSS/baseCSS、svgString(序列化 SVG 源)、dataURL、meta(冻结的渲染几何信息)。注意:clone、nodeMap、styleCache、svgString在afterRender运行后即被释放,避免每个结果对象常驻整棵克隆树。
导出钩子收到的是带export块的每导出拷贝,其中export.url默认是 SVG data URL(原生 html-in-canvas 捕获成功后则为 PNG);beforeExport/afterExport的第二个参数形如:
beforeExport(ctx, { format, options }) // format: 'png' | 'blob' | 'download' | 你的自定义 key afterExport (ctx, { format, options, result }) // result: 导出器实际返回的结果钩子规则
- 钩子可以是同步或异步的,SnapDOM 会
await所有钩子(见 runHook,返回非undefined时会替换累积的 payload)。 - 捕获选项应在
beforeSnap中设置(如ctx.scale、ctx.width、ctx.clip、ctx.outerTransforms)。例外是plugins、needs、invalidate、cache这四个在钩子运行前就已解析的选项——在钩子中修改它们无效。 - 改导出选项用
beforeExport,观察结果用afterExport,提供全新导出器用defineExports。 beforeClone中对真实 DOM 的修改必须撤销(典型做法是在afterClone中还原),不能影响线上页面。
afterExport与 v2 相比不再把返回值链式传递给下一个钩子;自定义输出应当放到defineExports中。
深入理解 needs:插件声明运行到哪一步
模板的注释与规范都强调needs声明。捕获流水线只有两个阶段:
element ──▶ [clone] ──▶ [render] ──▶ exports{ name: 'my-plugin', needs: 'clone' }:跑到afterClone为止,不渲染像素;{ name: 'my-plugin' }(默认):'render',完整跑完克隆与渲染。
从 stages.js 的resolveStage实现可见两条硬性规则:捕获会跑到所有插件声明的最大深度(默认'render');从未产生的产物绝不会被伪造——如果捕获停在'clone',调用url、toPng()、toCanvas()等图像导出会抛出异常并指名是哪个插件降低了阶段,且绝不会按需重新捕获(那将是另一个时间点的图像)。result.needs会报告实际运行的阶段。
两个重要限制:
- 只有 per-capture 插件可以降低阶段。全局插件若声明
needs: 'clone',会在注册时被拒绝(registerPlugins 直接throw),因为那会让应用里每一次捕获都停在像素之前。 - 旧版本曾存在的
'dom'阶段(停在克隆之前)已移除,合法值只有'clone'与'render'。插件可用assertNeeds('my-plugin', options.needs, ['clone', 'render'])(从@zumer/snapdom/plugins导出)在构造时校验调用方传入的阶段值。
同时支持两个阶段的插件应接受一个needs选项:
export function myPlugin(options = {}) { return { name: 'my-plugin', needs: options.needs ?? 'render', /* hooks */ } }官方范例:contextExport({ needs: 'clone' })(仅要结构化文本,跳过渲染)与agentMap({ image: false, needs: 'clone' })(只要元素坐标地图,不要图像)。注意 context-export 源码 在beforeClone阶段就把整棵语义树冻结进ctx.__contextSnapshot,toContext()只负责格式化这份冻结快照——这正是"克隆即冻结"原则的体现:延迟读取会得到另一个时刻的页面。
引擎快路径与 pure 声明(v3 新增)
SnapDOM 会复用未变化的捕获(memoization)或重建变化的子树(差分重捕获)。默认情况下,包含resolveNode、beforeSnap、beforeClone、afterClone、beforeRender或afterRender的插件会禁用这些快路径,确保它们的钩子不会被跳过。判断逻辑在 hasImpureRenderPlugins:捕获插件列表中任一未声明pure: true且含上述渲染钩子的插件,都会让 auto-burst 与 diff 路径采取保守策略。
如果你的捕获钩子是确定性且幂等的(相同输入必然产生相同输出,不依赖时间戳、计数器等外部状态),可以声明:
{ name: 'my-plugin', pure: true, afterClone(ctx) { /* … */ } }pure: true会让插件重新加入"未变化重复捕获"的 memo 快路径。注意事项:
- 捕获钩子若只限
beforeRender/afterRender,还可使用差分重捕获;但参与克隆构建的钩子(beforeSnap、beforeClone、resolveNode、afterClone)在内容变化后仍强制完整重捕获(拼接路径无法跳过它们)。 - 停在
needs: 'clone'的捕获永远不做 memo(没有渲染产物可供复用)。 - 声明 pure 却读取变化的外部状态,会提供过期结果——这是 v3 中需要特别小心的正确性边界。
用 defineExports 添加自定义导出
模板的defineExports注释展示了核心模式:返回一个自定义导出方法对象,调用后成为result.toMyFormat()。规范中 PDF 导出的完整示例:
export function pdfExport(options = {}) { return { name: 'pdf-export', defineExports(ctx) { return { pdf: async (ctx, opts) => { const captureUrl = ctx.export.url; // SVG by default; PNG after successful native html-in-canvas // convert to PDF... return pdfBlob; } }; } }; } // 注册后: const result = await snapdom(element, { plugins: [pdfExport()] }); const blob = await result.toPdf({ width: 800 });实现层面,核心用runAll收集所有插件defineExports的返回值(runAll,非undefined结果按插件顺序收集),因此每个插件返回自己的一份导出方法映射。导出 key 冲突时的优先级是per-capture 插件 > 全局插件 > 核心内置——也就是说,你的局部插件可以覆盖toPng、toJpg、toCanvas等内置导出器。构建自定义导出时可使用ctx.export.url,或在defineExports内访问ctx.exports(包含不含钩子的核心导出器png、canvas、blob等,便于组合而不重复进入导出流水线)。
官方包中 ascii-export 是纯导出类插件的范例(toAscii()),context-export 则演示了"beforeClone 冻结快照 + defineExports 格式化输出"的组合模式,二者都值得对照阅读。
从模板到发布:包配置与目录提交
package.json 关键字段
模板的 package.json 已经给出了完整骨架,发布时替换名称与描述即可:
{ "name": "snapdom-plugin-yourname", "version": "1.0.0", "description": "A SnapDOM plugin that does X", "type": "module", "main": "index.js", "exports": { ".": "./index.js" }, "files": ["index.js", "README.md"], "keywords": ["snapdom", "snapdom-plugin", "dom-capture"], "peerDependencies": { "@zumer/snapdom": "^3" }, "license": "MIT" }要点:核心必须放在peerDependencies(而非 dependencies),exports限定模块入口,files只打包发布需要的文件。官方 packages/plugins/package.json 展示了多入口插件的exports写法(每个插件一个子路径,便于 tree-shaking 单独导入)。
命名约定
- npm 包名:
snapdom-plugin-[name] - 插件
name字段:小写 kebab-case,如'watermark'、'redact'、'pdf-export' - 主导出:camelCase 工厂函数,如
myPlugin
发布流程
npm publish发布后可通过提交 PR(在 docs/community-plugins.md 的表格中追加一行,格式为| name | description | category | npm | github | author |)将插件列入插件目录;也可以开 Issue 提供插件名、npm 链接、分类与一句话描述。更详细的提交流程见 CONTRIBUTING_PLUGINS.md。
实战模板:从 example 选项到可用插件
把模板改造成一个可运行的插件,只需三步:替换 name、替换选项、填充钩子。以规范中的水印插件为例(它演示了 afterClone 变换克隆的完整写法):
export function watermark(options = {}) { const { text = '© SnapDOM', fontSize = 14, color = 'rgba(0,0,0,0.15)', position = 'bottom-right', rotate = -30, } = options; return { name: 'watermark', afterClone(ctx) { const overlay = document.createElement('div'); const posStyles = { 'top-left': 'top:8px;left:8px', 'top-right': 'top:8px;right:8px', 'bottom-left': 'bottom:8px;left:8px', 'bottom-right': 'bottom:8px;right:8px', 'center': 'top:50%;left:50%;transform:translate(-50%,-50%)' }; overlay.style.cssText = ` position:absolute; ${posStyles[position] || posStyles['bottom-right']}; font-size:${fontSize}px; color:${color}; pointer-events:none; z-index:999999; white-space:nowrap; ${position !== 'center' && rotate ? `transform:rotate(${rotate}deg)` : ''} `; overlay.textContent = text; ctx.clone.style.position = 'relative'; ctx.clone.appendChild(overlay); } }; }水印插件的要点与模板注释一致:afterClone拿到的是ctx.clone(克隆并内联样式后的 DOM 树),对它做任何修改都不会影响线上页面——这正是"在克隆上工作"这一插件核心安全模型。同类参考还包括官方 filter(对克隆施加 CSS 滤镜)、replace-text(替换克隆中的文本)、redact-inputs(掩码敏感输入并支持整块排除,含beforeRender额外处理)等。
插件开发最佳实践
综合模板、规范与仓库测试(如 plugins.stages.test.js 中对defineExports返回自定义导出方法的验证),插件开发应遵循:
- 只在需要的钩子内做插件工作。
- 还原 DOM:
beforeClone中做了修改,就在afterClone中撤销。 - 坚持工厂模式:始终接受 options、始终提供默认值。
- name 唯一:先查插件目录避免冲突。
- 尽量从错误中恢复;否则抛出有用的错误,而不是返回不完整的输出。
- 文档化你的选项:类型、默认值、描述(对应模板 README 的参数表格式)。
- 依赖最小化,理想情况下为零依赖。
- 用
scale: 2测试:高 DPI 最容易暴露像素计算问题。 - 仅当捕获钩子确定且幂等时标记
pure: true,避免返回陈旧结果。 - 将核心声明为
peerDependencies,并在 README 中提供安装、用法、选项与示例。
插件按能力可分为五类:Capture(修改 DOM 捕获方式,如自定义组件解析器)、Transform(改变克隆输出,如水印、滤镜、脱敏)、Export(新增输出格式,如 PDF、ASCII)、Integration(对接外部服务,如上传 S3)、Utility(调试覆盖层、性能计时器等开发工具)。以本模板为起点,你可以轻松落地其中任何一类。
结语
packages/plugin-template为 SnapDOM v3 插件开发提供了零依赖、可独立工作的起点:工厂函数 + 生命周期钩子 +defineExports是插件的三大支柱,needs与pure则是 v3 中决定捕获深度与快路径参与的关键声明。模板本身是骨架,真正的能力来自 PLUGIN_SPEC.md 中定义的完整钩子契约、src/core/plugins.js 中的插件调度实现,以及 packages/plugins 中 13 个官方插件可对照研读的实战范例。照着模板改一个afterClone变换或defineExports导出,再配合同步的peerDependencies发布,你的第一个 SnapDOM 社区插件即可上线。
【免费下载链接】snapdomHigh-performance engine for capturing, modifying, and converting DOM elements into any format.项目地址: https://gitcode.com/GitHub_Trending/sn/snapdom
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考