ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

Etherpad标题插件ep_headings2:从钩子机制到导出还原的部署指南

Etherpad标题插件ep_headings2:从钩子机制到导出还原的部署指南 简介Etherpad 是一款常用于团队协作、在线文档共创的实时编辑器ep_headings2 是基于 JavaScript 的官方标题插件为 pad 提供 h1、h2 等不同层级标题适合需要规范长文结构、提升排版效率的 Etherpad 使用者、维护者及插件开发者。除基础插入功能外它支持活动标题高亮、复制/粘贴、导入/导出并内置测试与 lint 检查由 Etherpad 基金会维护还配有覆盖多种语言的翻译文件可直接用于生产环境。资源压缩包共 54 个文件、约 86KB38 个 json 主要是多语言 locale 文案js 是核心逻辑css、ejs 负责编辑栏按钮和界面样式yml、md 则用于 CI 配置与使用说明整体结构清晰、扩展友好。目前已有 290 人学习。通过该资源既可以快速部署体验也可以作为 Etherpad 插件开发范本研究标题插入、工具栏定制、语言包组织及自动化测试等关键实现为协同编辑平台扩展文档排版能力提供参考。1. Etherpad 的「长条文档」困局ep_headings2 标题插件能救到什么程度Etherpad 的协作文档写到三十页时最崩溃的不是观点不合而是滚轮滑半天找不到「这一节从哪开始」。默认编辑器把每个段落渲染成同一种样式CtrlF 成了唯一的导航工具。ep_headings2 这个标题插件就是为这个场景设计的它给 Etherpad 编辑区加上 H1-H3 标题按钮用一小段 JavaScript 把「我这行是标题」变成文档的行属性渲染时呈现层级导出时还原成真正的 h1 标签。它适合不想迁移协作工具、只想给现有 pad 流程补上结构感的团队也适合对文本结构有执念的写作者。部署它本身不复杂但插件机制、工具栏配置、样式重写、导出还原这几个环节各有各的坑。下面按我实际部署的顺序从钩子机制讲起一路到常见翻车现场和二次开发每个步骤都给出可复现的命令和参数。2. 它改的不是编辑器而是文档模型hook 机制与标题属性链路2.1 插件不是改源码ep.json 与客户端钩子Etherpad 的插件体系和「改 vendor 源码」完全是两条路。官方提供一套 hook 机制编辑器初始化、每次按键、每行内容渲染、导出 HTML这些生命周期都留了回调口子。插件要做的只是把自己的代码挂到对应的钩子上。所以 ep_headings2 的代码量其实很小难的不是写插件是理解钩子的时序和参数形状。解开 ep_headings2 的插件包根目录必有 ep.json这就是整个插件与 Etherpad 之间的接线图。按目前主流版本的常见写法结构是这样{ parts: [ { name: ep_headings2, client_hooks: { aceEditorCSS: [ep_headings2/static/css/headings.css], aceInitInnerdocbody: ep_headings2/static/js/hooks.js, aceAttribsToClasses: ep_headings2/static/js/hooks.js, aceCreateDomLine: ep_headings2/static/js/hooks.js, aceEditEvent: ep_headings2/static/js/hooks.js, acePostWriter: ep_headings2/static/js/hooks.js }, hooks: { eejsBlock_editorContainer: ep_headings2/index.js } } ] }重点看client_hooks这一段。aceEditorCSS注入插件自己的样式表aceAttribsToClasses把行属性翻译成 CSS classaceCreateDomLine决定这行内容被包裹成什么标签aceEditEvent监听编辑动作比如你点工具栏的 H1 按钮就是在这里把heading属性写到当前行aceInitInnerdocbody在编辑器 DOM 初始化时执行一次用来注册一些启动逻辑。hooks里的eejsBlock_editorContainer是服务端钩子页面渲染时把标题按钮拼进编辑区容器。所以「装完插件为什么没有按钮」这个问题先看 ep.json 里有没有服务端钩子再看前端文件路径有没有写错。路径写错时按钮区域通常是一片空白但不报任何错误查起来比较费劲。不同 release 的钩子名会有差异。老一点的 ep_headings 可能没有acePostWriter新版本则依赖它处理行内容的后置修正。如果你下载的包里 ep.json 和这里不一致以包内实际内容为准。我判断一个标题插件是否成熟有三个标准aceAttribsToClasses和aceCreateDomLine必须都在、导出 HTML 的钩子必须存在、ep.json 里不能出现指向不存在文件的路径。满足这三点基本不会出大岔子。2.2 从属性到样式标题在每行文本里的完整链路Etherpad 的底层模型不是 HTML 文档而是「行 行属性」的文本模型。每行文本可以挂一个属性集合比如heading: 1。你点工具栏的 H1本质上是对当前行做一次属性写入这个属性随 changeset 广播给所有协作者也随 pad 内容持久化。标题因此不是某种皮肤层的视觉装饰而是数据的一部分。这也是为什么重新打开 pad 标题还在、其他协作者端也会同步更新。ep_headings2 的前端 hooks.js 里最核心的代码可以浓缩为两段 JavaScript 逻辑。第一段把属性映射成 class// 行属性 - DOM class 的映射 function aceAttribsToClasses(hook, context) { if (context.attribs[heading]) { // 注意拼出来的是 heading1不是 heading-1 return [{ key: heading context.attribs[heading] }]; } return []; }第二段在行渲染时用这个 class 决定包什么标签// 渲染行时把 heading class 变成带标题语义的标签 function aceCreateDomLine(hook, context) { const result { extraOpenTags: , extraCloseTags: }; if (context.cls.indexOf(heading1) ! -1) { result.extraOpenTags h1; result.extraCloseTags /h1; } return result; }第一段里context.attribs是当前行属性对象heading键对应的值来自属性池类型是字符串。返回的数组会成为该行 DOM 节点的附加 class所以标题行的 DOM class 里必然能看到heading1之类的值。第二段里context.cls是编辑器组装完的完整 class 字符串extraOpenTags和extraCloseTags是渲染的包裹标签。这里写成h1导出路径和复制路径拿到的是语义标签而不是一串自定义 div。这也是我推荐用它、而不是自己写「加粗换颜色」当标题插件的原因。链路走通之后其他协作者端会通过 changeset 自动拿到标题属性不需要重新加载页面。你在这边把某行改成 H2对方屏幕上的那一行在几百毫秒内就变成 H2 样式。网络环境一般的时候这个反馈有延迟那是 Etherpad 同步机制本身的行为不是插件的锅。还要提一个容易忽略的边界Etherpad 的行是「一行一个属性集合」同一行可能同时存在列表属性和标题属性class 上会出现bullet和heading1共存。ep_headings2 不处理这种组合冲突。实际协作里我一般要求团队不要对标题行同时套列表缩进否则大纲层级和视觉缩进会互相误导。这不是 bug是模型本身的特性。我自己拿到插件后第一步不是改样式而是开浏览器开发者工具选中一个标题行看它的 DOM class。如果 class 里带heading1说明属性和前端链路通了如果只有别的 class问题大概率出在aceAttribsToClasses的返回格式或属性名拼写。这种检查方式比对着日志猜快得多。3. 跑通最小可用配置安装、按钮布局与层级取舍3.1 标准安装流程与依赖落地安装本身不复杂坑集中在依赖。先看代码# 在 Etherpad 根目录执行注意不要在皮肤目录里装 cd /opt/etherpad-lite # 官方推荐走插件安装脚本 ./bin/installPlugin.sh --plugin ep_headings2如果你在本地临时验证也可以直接npm install ep_headings2。两者的差别在于 installPlugin.sh 会顺带走一遍 Etherpad 的依赖检查并更新插件缓存列表npm 直装适合验证目录结构最后同样要重启节点才能生效。决定用 npm 安装的话要关注这些依赖落地问题。ep_headings2 依赖async、jsdom这一类的包其中 jsdom 在较老版本的 Node 上会触发 node-gyp 编译编译失败就不会完整装进 node_modules。我建议 Etherpad 节点保持在 Node 14 LTS 或更高版本安装前后检查node_modules/ep_headings2和node_modules/jsdom是否存在。安装完重启节点访问/admin/plugins看到 ep_headings2 的状态是 Active说明插件已经被正确扫描到。装好后可以顺手做个冒烟测试新建 pad输入正文点 H1 按钮看当前行是否变成标题样式再按两次回车观察后续行是否恢复普通文本。这个操作 30 秒内能验证主链路值得养成习惯。后面所有排障都要以「新 pad 能复现」为基准不要拿旧 pad 的缓存状态当结论。3.2 工具栏按钮配置三档标题还是六档ep_headings2 默认提供 H1 到 H6 的能力但工具栏上按钮太多会挤压真正的编辑空间。我实际部署过的团队里最终长期存活的配置基本都是 H1-H3。把需要显示的按钮列在 toolbar 的按钮集合里{ toolbar: { buttons: { heading1: { command: h1, class: buttonHeading1 }, heading2: { command: h2, class: buttonHeading2 }, heading3: { command: h3, class: buttonHeading3 } } } }这个配置的意思是编辑器工具栏只暴露三个标题按钮点击后触发h1、h2、h3指令指令最终在aceEditEvent里写成heading: 1这样的属性。按钮的class字段控制按钮本身的样式类一般不需要动。如果某一项被移除对应按钮就不渲染但属性层仍然可以写进文档区别只在 UI 入口。已经存在的 H4-H6 内容不受影响只是没有入口可点。标题层级的取舍直接决定协作体验。下面是我实际在用的标准层级class 名典型用途我的建议H1heading1pad 主标题、文章标题保留H2heading2章节标题保留H3heading3小节标题保留H4-H6heading4~6非常深的内容结构隐藏有人会问 H4 为什么不留着备而不用。实际操作里超过三级的文档在多人协作中很难维持一致性——第一个人把某段设成 H4第二个人认为它应该归到 H3整个文档的层级就开始漂移。限制到三档的团队通常三个月后标题结构仍然可维护放开六档的团队大部分文档最后只剩 H1 和 H2。这不算严谨的统计数据但在多支团队实操里表现稳定。除了按钮数量工具栏的空间占用同样值得留意。Etherpad 的工具栏没有折叠分栏按钮越多编辑行越矮。我部署过一版开了 H1-H6 的配置团队反馈里出现频率最高的一句话是「按钮那排太长了」。改回三档后pad 版面清爽很多。如果你确实需要深层级建议给团队成员发一份层级规则并在 pad 开头用 H1 写明「本文档标题仅用到第三层」这比任何配置都有效。4. 把样式换成本团队的皮肤CSS 重写与导出还原4.1 CSS 重写与 Ace 编辑器内的即时反馈ep_headings2 自带的标题样式属于「能看但未必符合团队规范」基本都要重写。样式文件在 static/css/headings.css但实际生效的选择器要带编辑器容器上下文否则改了半天没反应/* 覆盖 ep_headings2 的标题样式 */ #innerdocbody .heading1 { font-size: 26px; font-weight: 700; border-bottom: 1px solid #d0d7de; margin-top: 18px; } #innerdocbody .heading2 { font-size: 21px; font-weight: 600; margin-top: 14px; } #innerdocbody .heading3 { font-size: 17px; font-weight: 600; color: #1f2328; }#innerdocbody是 Etherpad 编辑区内部容器的 id加这个限定才能命中编辑器里的标题行。三个选择器分别对应 H1、H2、H3。改完样式后不需要重新编译但浏览器大概率要硬刷新才会加载新的 CSS因为静态资源带 hash缓存的旧文件不会自动失效。这里有个容易踩的坑编辑区、只读视图、导出视图是三条不同的渲染通道。有些版本里只读模式没有#innerdocbody这个容器需要单独为只读端写一套选择器。我自己通常是先改编辑区再用 pad 的只读链接打开核对一遍避免一边好看一边返工。经验之谈不要用!important打天下。Etherpad 的 CSS 加载顺序和插件注册顺序有关老老实实把选择器写精确比一票否决式覆盖更稳定。如果只想调整字号建议用相对单位比如1.25em因为 pad 页面有缩放绝对像素在投屏时会显得过小或过大。还有一点标题行不是块级元素它是行容器里套了一层包裹标签H1 的 margin 会直接影响行模型计算。margin 改得过大跨行选择文本时会跳出奇怪的空白我实测下来 H1 的 margin-top 控制在 0.4em 到 0.8em 之间比较稳。4.2 导出 HTML 与 PDF 时的标题还原导出是标题插件最容易掉链子的环节也最值得提前验收。pad 的 HTML 导出依赖exportHtml这条路径文档行内容经渲染层生成 DOM再序列化成 HTML。ep_headings2 的aceCreateDomLine在这里浮现用武之地——把heading1包成h1。但我遇到过插件状态正常、编辑区正常、导出结果却是纯文本的怪事排查后确认是导出端根本没使用aceCreateDomLine返回的包裹标签而是直接输出了行内容的裸文本。验收导出结果用一句命令就够# 导出 HTML 后检查标题标签是否存在 grep -E h[1-3] exported.html | head -20如果 grep 不到h1到h3先看导出的 HTML 里行 div 的 class确认有没有heading1。如果有 class 但没有 h 标签说明包裹逻辑没有在导出路径生效。解决方法是回到 ep.json 确认导出相关钩子存在或者看导出模板是否过滤了 extraOpenTags。我再补一句经验PDF 导出一般是借 headless 浏览器把 HTML 渲染成 PDF标题视觉上正常但生成的 PDF 书签里很可能没有对应条目因为书签依赖真正的 heading 大纲而不是「长得像标题」的样式。要 PDF 有目录前提是 HTML 导出阶段就把 h 标签还原对。这个链路我是在部署第二周才发现的典型的「编辑区看着都对、交付文件全错」。另外一个容易被当成 bug 的边界是外部内容粘贴。从网页复制一个h2章节一/h2进 padEtherpad 的粘贴处理器会剥离大部分富文本格式标题语义也不会自动转成 heading 属性。ep_headings2 不负责把外部标题转换成内部属性你需要先粘贴纯文本再手动选中该行点 H2 按钮。这是 Etherpad 的粘贴安全策略在起作用不是插件缺陷。团队里如果有人反复踩这个点把规则写进 pad 首页说明比改代码有效。导出模板里还有一个细节标题行默认不会自动分页导出 PDF 时经常出现 H1 落在页面底部、正文排到下一页的情况。需要在导出模板的 CSS 里给 heading1 加page-break-after: avoid让标题与后续内容保持在同一页。这类样式写在导出模板中效果最直接。5. 避坑ep_headings2 的五个高频翻车现场ep_headings2 的安装链路不算长真正让人血压升高的问题大多出现在「把它当成一键安装插件」之后。下面五条翻车记录来自真实环境按现象、原因、解决三段写清楚。5.1 按钮装了看不见现象插件在 /admin/plugins 里是 Active服务也重启过但编辑区上方就是没有标题按钮。原因多数情况是浏览器缓存了旧的编辑器脚本和样式。Etherpad 重启后静态资源文件名会变但已经打开的长驻页面还在用旧版本。另一个常见原因是服务端注入按钮的模板块被其他插件覆盖两个插件同时挤占eejsBlock_editorContainer这个注入点渲染顺序冲突导致按钮消失。解决先开无痕窗口验证排除缓存因素。无痕窗口下有按钮回正常窗口硬刷新一次Ctrl/Cmd Shift R。无痕窗口下也没有按钮检查 ep.json 里eejsBlock_editorContainer指向的 index.js 是否存在再看是否有其他插件也注册了同一个钩子。两个插件写同一个钩子时后加载的往往覆盖先加载的这种问题查插件的加载顺序比改代码更快。5.2 标题在编辑器里正常导出后变纯文本现象编辑区里 H1 样式明显导出的 HTML 却找不到任何h1连heading1class 都没有。原因导出路径与编辑路径不完全重合。有些版本导出用的是独立的 exporter hook没有走aceCreateDomLine标题属性就没被翻译成标签和 class。解决先把导出文件存下来用 grep 分别检查 class 和 h 标签两步定位到底断在哪一环。修复方向有两种补全导出 hook或者覆盖导出模板手动把 heading 属性映射到 h 标签。无论用哪种改完都要新建一个 pad 重新走一遍导出验证不要拿旧的导出文件判断。提示导出验收的标准动作是「先 grep class再 grep h 标签」。class 有而标签没有问题在导出模板class 都没有问题在 exporter 钩子。5.3 多人同时改同一行标题属性互相覆盖现象两个协作者几乎同时把各自的段落设成标题结果一个人的标题生效另一个人的没了插件也没有任何报错。原因Etherpad 的 changeset 以光标位置为基准两个并发属性写入落在同一行时后者在属性池中的写入覆盖了前者。这是 Etherpad 属性合并的固有行为不是插件逻辑错误。解决把它当协作规则处理而不是代码缺陷。团队内部约定标题行由负责该章节的人单独维护其他人不要在同一行做标题操作。真被覆盖了就让对方撤销重做这比在代码里做补偿简单得多。想实现真正自动合并需要自己写属性分发策略成本远高于收益小团队不建议碰。5.4 安装时 jsdom 编译失败现象npm install 卡在 node-gyp rebuild报错指向 jsdom 的编译步骤随后安装中断node_modules 里的 ep_headings2 目录不完整。原因ep_headings2 的依赖里有 jsdomjsdom 在低版本 Node 或缺少编译工具链的环境比如精简版 CI 镜像下需要本地编译一旦编译失败就中断整个安装流程。解决升级 Node 到 14 LTS 以上确认系统里有 python3 和 make 工具。内网离线安装时先把 npm registry 的依赖缓存整理好保证 jsdom 相关的包能完整拉取。装完后立刻检查node_modules/ep_headings2目录里有没有完整的 hooks.js如果只有 package.json 没有源码文件说明依赖树不完整需要先修复再重启 Etherpad。5.5 只读视图里标题变成普通文本现象编辑区标题一切正常但 pads 的只读链接打开后标题和正文没有任何区别。原因只读视图和编辑视图共用文档内容模型但使用的 CSS 命名空间不同。如果你覆盖样式时只写了#innerdocbody前缀只读端没有这个容器标题样式自然丢失。解决重写样式时把只读端的场景一起考虑。常见做法是额外加一层只读容器相关的选择器或者把公共样式抽出来不依赖具体 id。改完之后每次调整 CSS 都要在三个地方确认编辑区、只读链接、导出 HTML。这个习惯能避免「改完编辑区忘了只读端」的低级失误。6. 把标题属性变成你自己的小工具二次开发与验证技巧6.1 不点按钮也能读出标题层级随着对插件机制越来越熟我逐渐把标题属性当成一个数据接口来用。不点任何按钮也能把当前 pad 的标题结构完整拉出来。下面这段 JavaScript 可以在 pad 页面的开发者工具里直接跑// 在 pad 页面开发者工具里运行padClient 是当前 pad 客户端对象 function collectHeadings(padClient) { const text padClient.getText().split(\n); const list []; for (let i 0; i text.length; i 1) { const headingLevel padClient.getAttributeOnLine(i, heading); if (headingLevel) { list.push({ line: i 1, level: headingLevel, text: text[i] }); } } return list; }padClient.getText()拿到的文本行序和属性行序一一对应getAttributeOnLine的第二个参数传属性名heading返回1、2或空值。这个函数在不少场景下比工具栏更好用一键输出所有标题行、生成 Markdown 大纲、把标题列表直接粘贴进会议纪要。我在团队里跑过一段时间协作时用它把 pad 的标题提取出来直接生成周报结构省掉了很多手工整理的时间。注意行号从 0 开始输出到表格时要加一否则对不上原文。6.2 我把部署验证固化成四步插件这类资源最大的风险不是代码不能跑而是「以为它能跑」。从那以后我每次部署完标题插件都强制自己走一遍四步链路新建 pad依次在开头打出 H1、H2、H3 三个标题换一个无痕窗口打开同一个 pad确认协作者端标题样式同步导出 HTML用 grep 检查 h1-h3 标签真实存在最后对着只读链接检查一遍样式是否与编辑区一致。这四步每次都要完整走通省掉了后面无数次「标题又没生效」的血泪排查。这已经成了我接任何 Etherpad 插件的默认习惯。希望帮到你。本文还有配套的精品资源点击获取
返回列表