拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Etherpad标题插件ep_headings2安装配置与避坑指南

Etherpad标题插件ep_headings2安装配置与避坑指南

简介:ep_headings2 是一款为 Etherpad 在线协作文档提供标题层级功能的开源插件,适合需要在实时编辑面板中快速完成 h1 至 hn 标题样式设置的个人或团队用户。包内共 54 个文件,压缩后仅 86KB,核心为 5 个 JavaScript 文件与 38 个 JSON 文件,前者负责标题按钮和编辑逻辑,后者主要是多区域语言包(含中文 zh-cn、繁体 zh-hant 等)及配置数据,另附 README、LICENSE、测试与 CI 配置,方便二次开发或本地化。资源已吸引约 290 人浏览学习,可作为了解 Etherpad 插件结构、国际化实现及前端工具栏扩展的轻量级样例。通过阅读源码能掌握 h1 等标题命令的注册方式、活动标题高亮显示、复制粘贴及导入导出兼容处理等细节,对希望在 Etherpad 生态中开发同类功能的工程师有直接参考价值。

1. ep_headings2 到底解决了什么:从编辑器里的“标题”按钮说起

在 Etherpad 上维护过内部知识库的人,大概率遇到过同一个尴尬:多人同时编辑时,字号调了,层级乱了,导出的 HTML 根本分不清谁是标题。ep_headings2 是 Etherpad 生态里专门解决这个问题的标题插件,它给工具栏补上“标题 1/2/3”这类真正的标题选择器,而不是简单把字号放大。它能做的事,刚好是默认编辑器缺的那块:让标题层级从视觉样式变成文档结构,再支撑后续的样式定制与导出。如果你正打算搭团队协作笔记、公共 Wiki,或者已经跑了 Etherpad 但嫌标题太弱,这个插件几乎不用改代码就能直接进生产环境。

2. 先弄懂 Etherpad 插件机制和 ep_headings2 的设计取舍:为什么是属性标记而不是直接改 HTML

2.1 Etherpad 插件骨架:ep.json、hooks 与客户端资源

Etherpad 的插件体系并不复杂,但它和普通 CMS 插件很不一样:Etherpad 的编辑器是协同编辑器,所有内容操作都要通过 called “changeset” 的增量协议同步,因此插件不能像改静态页面那样直接往 HTML 里塞标签。常见做法是每个ep_开头的 npm 包都携带三样东西:package.json描述插件名和依赖;ep.json声明 hooks 入口;static/目录放客户端 JS 与 CSS。

ep.json 里的 hooks 是插件和 Etherpad 内核之间的约定。比如aceEditorCSS用来注入编辑器样式,aceAttribsToClasses用来把文本属性变成 CSS 类名,eejsBlock_editbarMenu用来在编辑工具栏里插入控件。一个标题插件如果要做得完整,一般会同时用到这几个 hook:一部分管“入口在哪里”,一部分管“渲染成什么样”,还有一部分管“导出时要不要保留语义”。

实际看到的 ep_headings2 或同类插件,其 ep.json 大致长这样:

{ "parts": [], "hooks": { "aceEditorCSS": "ep_headings2/static/css/headings.css", "aceAttribsToClasses": "ep_headings2/static/js/hooks", "aceInitInnerdocbodyHead": "ep_headings2/static/js/hooks", "eejsBlock_editbarMenu": "ep_headings2/static/js/editbar" } }

这里每个字段都值得解释:aceEditorCSS指向的 CSS 只作用于编辑器画布;aceAttribsToClasses在收到带标题属性的变化时,把它映射成对应的 CSS 类;aceInitInnerdocbodyHead会在文档初始化时往编辑区头部塞必要的 meta 信息;eejsBlock_editbarMenu则负责往工具栏下拉或按钮组里加入口。理解这一层后再装插件,你就不容易把“插件没装上”误判成“功能坏了”。

2.2 ep_headings2 和内置标题功能、手改 HTML 的差别

默认 Etherpad 的工具栏里不是完全没有标题按钮,而是它通常只能做“近似标题”:放大字号、加粗、改颜色,本质上都还是文本样式。对协同编辑来说,文本样式只影响显示,不影响结构;导出成 HTML 时,别人拿到的也只是一堆<strong>和<span style="font-size:...">,根本识别不了目录和层级。ep_headings2 解决的是结构问题:它用 Etherpad 的文本属性(attribute)记录“这行是一级标题还是二级标题”,而不是靠视觉样式伪装。

有人会问,既然要结构,为什么不直接把<h1>写进 pad 内容?这是最容易翻车的一条路。Etherpad 的协同文字是带属性序列化的,如果直接把 HTML 标签当正文存进去,两个人同时编辑标题和正文时,changeset 在 OT 合并阶段会互相踩踏,轻则多出半个标签,重则整段文字在冲突回滚时丢失。ep_headings2 的属性标记方案避开了这个雷:标题级别是挂在文本上的一个结构化属性,比如h:1、h:2、h:3,它会随着文本一起参与合并和撤销,而不是变成游离的 HTML 标签。

这里面还有一个容易被忽略的好处:当 Etherpad 升级或换导出插件时,属性可以稳定映射。你可以在编辑器中把标题渲染成蓝色大号字,也可以在导出阶段映射成<h1>;如果哪天换了皮肤,只需要改 CSS,而不必回头去翻 pad 里的历史内容。这正是“属性标记 + 渲染层映射”比“直接存 HTML”更适合协同场景的根本原因。

2.3 标题级别映射:从工具栏下拉到文档属性的完整链路

我一般会把 ep_headings2 的工作流程拆成四步来看。第一步,用户在工具栏的选择器里选择“标题2”;第二步,客户端脚本把当前光标所在行或选中文本包成一个带h:2属性的 changeset;第三步,服务端广播给所有在线协同者,其它客户端收到后更新本地文档模型;第四步,渲染层根据属性把该行显示成二级标题的样式。

在前端渲染时,编辑区并不会直接把<h1>标签写进协同 DOM,它可能只是给对应行加一个类似heading-level-2的 class,然后用 CSS 把它画成标题样子。只有走到导出 HTML 这一步,才需要真正输出<h1>、<h2>语义标签。这也是为什么很多人在 pad 编辑区看着没问题,放到导出器里就发现标题丢了:因为导出器要知道“属性怎么转成标签”,这通常依赖 ep_headings2 或导出插件是否实现了对应的 hook。

理解了这条链路,后面安装和调参时就不容易懵。你改的每一个配置,要么是在影响“选取哪些级别显示在下拉框里”,要么是在影响“属性怎么映射成 CSS/HTML 结构”。带着这个认知去读插件 README,比对着网上教程机械复制命令可靠得多。

3. 用 npm 或 admin 面板装好 ep_headings2:两条安装路径和配置文件参数

3.1 最小安装:installPlugin.sh 与手动 npm 方式

假设你的 Etherpad 部署在常见的/opt/etherpad-lite,并且已经配置了 systemd 服务。安装 ep_headings2 最简单的方式是直接用官方提供的安装脚本:

cd /opt/etherpad-lite sudo -u etherpad ./bin/installPlugin.sh ep_headings2

installPlugin.sh本质上是对 npm install 的一层封装,它会切换到 Etherpad 工作目录并安装插件到node_modules。我特别强调用-u etherpad,是因为很多系统里 Etherpad 以独立用户运行,如果直接用 root 装了插件,node_modules下会出现 root 属主的文件,之后服务重启时可能没有权限读取。命令跑完后不要急着干活,先重启服务:

sudo systemctl restart etherpad

在无法使用在线 npm registry 的内网环境,也可以用离线方式:把ep_headings2的 tar 包下载到服务器,然后解压到node_modules目录。不过这种情况会把版本依赖搞得很难维护,我一般建议还是开一个 npm 代理,或者把依赖打进自己的内部 npm 仓库。手动 npm 方式本质上一样,区别只是绕过了官方脚本:

cd /opt/etherpad-lite sudo -u etherpad npm install ep_headings2

注意这里的参数没有--save,因为installPlugin.sh和 npm install 都会在node_modules里产生包,但package.json不一定同步更新,升级 Etherpad 时容易丢。稳妥起见,装完之后可以把插件名加到容器镜像或部署脚本里,这样重新拉代码时不会忘。

3.2 确认插件加载:从日志到管理界面的检查方法

安装完成后,最怕的是按钮没出现,你误以为失败。先做两个确认:第一,插件是否真的进了node_modules;第二,Etherpad 进程是否把插件加载进来了。第一个问题用 ls 就能看,比如:

ls -la /opt/etherpad-lite/node_modules/ep_headings2

如果目录存在,再看它的package.json,确认name字段是ep_headings2。有些情况下安装脚本会把包装到错误目录,导致目录存在但 Etherpad 识别不到。确认目录只用了几秒钟,能省下后面很多排查时间。

第二个问题,到 Etherpad 管理后台访问/admin/plugins,登录后看已安装插件列表里有没有 ep_headings2。这个页面显示的是启动时真正扫描到的插件,比你自己看目录更权威。如果你不想开浏览器,也可以看服务日志:

grep -i headings /var/log/etherpad/etherpad.log

正常启动时,日志里会出现类似registered plugin ep_headings2或found plugin ep_headings2的记录。如果目录存在但日志里没有,说明可能是启动时权限不够,或者插件被settings.json里的disablePlugins配置禁用掉了。这个检查点值得养成习惯,因为不少人折腾半天,最后发现只是没重启服务,日志还是旧的。

3.3 settings.json 里常用的 ep_headings2 配置参数

ep_headings2 并不一定需要配置才能用,多数版本装上就能从工具栏下拉里看到标题级别。但当你只想开放“二级标题到四级标题”时,就需要看配置。以常见维护版本为例,/opt/etherpad-lite/settings.json里可以单独加一个ep_headings2节点:

{ "ep_headings2": { "levels": [ { "level": 1, "label": "标题 1", "tag": "h1" }, { "level": 2, "label": "标题 2", "tag": "h2" }, { "level": 3, "label": "标题 3", "tag": "h3" } ], "shortcuts": { "h1": "Ctrl+1", "h2": "Ctrl+2", "h3": "Ctrl+3" } } }

这里的字段含义很直接:levels决定工具栏下拉里出现哪些标题级别,以及这些级别在导出 HTML 时要映射成的标签;shortcuts给标题级别设置快捷键。需要提醒的是,不同版本的 ep_headings2 能识别的字段并不完全一致,有些版本只支持levels,有些版本根本没有shortcuts配置,强行写入会被忽略。先看插件 README 或去 node_modules 里翻一遍源码,确认配置项存在再改,比在网上复制一段“通用配置”更靠谱。

如果 settings.json 里的配置没有被插件响应,还有另一个入口:工具栏自定义。很多 Etherpad 实例为了精简界面,会在启动参数或皮肤配置里自定义工具栏,把一些按钮排除掉了。ep_headings2 的标题选择器在自定义工具栏里可能默认不出现,这时候需要把对应的按钮加回来。具体按钮名以插件 README 里的editbar说明为准,常见版本会提供类似headings的按钮标识。

提示:配置完 settings.json 后,必须重启 Etherpad 才会重新加载。有些版本支持热加载插件列表,但不建议依赖这种行为,因为在配置频繁改动时,你很难判断当前真实生效的是哪一份配置。

4. 把标题用起来:工具栏操作、样式定制与导出注意

4.1 在页面上给一段文本套用标题级别的操作要点

ep_headings2 装上之后,打开任意 pad,光标落到文字所在行,然后从工具栏下拉里选择“标题 2”或“标题 3”。它处理的基本单位是“行”,也就是 Etherpad 里的 line。这里最容易踩坑的是选区跨度:如果只选了行内半句话,插件通常会把这半句单独拆成一行并应用标题,后半句变成正文,视觉上就成了两行。所以我的习惯是:先让光标停在该行任意位置,不做跨行选区,再点标题;如果已经出现拆行误操作,立刻用 Ctrl+Z 撤销,不要手动拼接。

如果你想取消标题,一般做法是把下拉选项切回“正文”或“普通文本”。切换回正文后,该行内容会保留,但结构属性被移除。这一操作对协同者的影响也会实时同步,不会把整段格式弄乱。对于新手用户,最好在团队内约定:标题只用于真正的章节标题,不要把整段正文都设成二级标题,否则 pad 导出后的目录结构会非常臃肿。

另外,ep_headings2 通常会把标题样式继承到 text 样式——也就是说,你仍然可以对标题文字再调颜色、加粗或斜体。不要惊讶于“标题还带细微格式”,因为标题属性只负责“层级结构”,其它内联格式互不干涉。如果你发现某一行同时带有标题属性和加粗属性,导出 HTML 时可能同时生成<h2>和<strong>,这是符合预期的,不必视为冲突。

4.2 用 CSS 重定义标题外观:在线自定义与皮肤定制

很多人装 ep_headings2 后觉得标题样式太素,想改成带下划线、有背景色的块级样式。常见做法是给编辑区写自定义 CSS。不同 Etherpad 版本的自定义样式入口不太一样,有的在后台管理界面有 “Custom CSS” 输入框,有的则需要到皮肤目录写pad.css。先找出你版本的自定义样式入口,再按标签覆盖:

/* 自定义 ep_headings2 标题外观 */ h1 { font-size: 28px; border-bottom: 2px solid #2d87ff; padding-bottom: 4px; margin: 16px 0 8px; } h2 { font-size: 22px; border-left: 4px solid #2d87ff; padding-left: 8px; margin: 12px 0 6px; }

这段 CSS 同时作用于编辑视图和大多数字体导出场景,因为它直接针对最终渲染出的h1、h2元素。不过要留意,编辑区内某些版本不会渲染出真正的<h1>,而是用class来模拟标题样式。这时上面的代码就不够用了,需要加上层级选择器备用:

/* 如果编辑区不使用 h1/h2 标签,改走 class 方案 */ .heading-level-1, .heading1 { font-size: 28px; font-weight: 600; } .heading-level-2, .heading2 { font-size: 22px; font-weight: 600; }

用哪一套类名,取决于你安装的 ep_headings2 在aceAttribsToClasses里返回了什么映射。建议打开浏览器开发者工具,选中一个带标题的段落,看它实际挂载的类名,再写 CSS。这样比盲目复制别人皮肤里的选择器可靠得多,也是标题插件定制中最值得养成的一个习惯。

样式定制时还要注意性能。编辑区里的 DOM 节点很多,如果给标题写了过于复杂的 CSS 动画,或者用了大阴影、滤镜,低端电脑在长 pad 里滚动时会明显卡顿。尽量用font-size、border-bottom、padding这类低开销属性,避免box-shadow和text-shadow大量使用。

4.3 导出 HTML/PDF 时标题样式丢失的常规处理

标题属性的价值最终要落到导出。如果你用 Etherpad 自带的导出接口,可能会发现导出的 HTML 里标题结构并不完整。常见原因有两个:一是 ep_headings2 的导出 hook 没有被当前版本的导出器调用;二是你用的是第三方导出插件,它读取的是编辑器 DOM,而不是原始属性。这时我不建议去改插件源码,而是先用 Etherpad API 导出一份 HTML,再用文档转换工具处理:

curl "http://localhost:9001/api/1/pad/export?id=YOUR_PAD&format=html&apikey=YOUR_APIKEY" -o pad.html

导出的pad.html如果能看到<h1>、<h2>,说明 ep_headings2 的导出 hook 是好的;如果导出的还是纯文本或普通段落,可以再试导出.docx格式,有些插件对内部属性和导出属性的映射不同。拿到结构正确的 HTML 后,再交给 pandoc 转成 PDF 或 DOCX,标题层级就能完整保留:

pandoc pad.html -o result.pdf --toc

如果你只希望在线预览 PDF 而不追求文件,也可以直接用浏览器的打印功能,但需要先保证编辑区没有启用水印、行号等干扰元素。这个方案比在 Etherpad 里反复调导出插件更稳,因为导出链路越短,失控的中间环节越少。

5. ep_headings2 避坑记录:安装后没按钮、样式丢失、冲突等 5 个排查实例

5.1 现象一:安装完成后工具栏没有出现标题下拉

第一次安装时,我遇到最多的情况是:插件显示已安装,日志也注册成功,但打开 pad 后工具栏里找不到标题选择器。原因通常是 Etherpad 的静态资源被浏览器缓存了,或者进程没有真的重启。也可能是你在 settings.json 里自定义过toolbar,把默认按钮组覆盖掉,导致新插件的按钮没有入口。

解决顺序是先强刷浏览器:Ctrl+F5 强制刷新 pad 页面,排除缓存。如果没变化,就回后台 /admin/plugins 确认状态为 enabled。最后检查 settings.json 的toolbar配置,确认标题按钮没有被排除。在我自己的部署里,最后一条才是真正原因,因为很多模板会在toolbar里手写一组按钮,漏掉新增插件。

5.2 现象二:标题样式一会有一会没有,多 pad 不一致

有时候同一个浏览器里,这个 pad 能看到标题样式,另一个 pad 看不到;换个浏览器又恢复了。多数情况是 CSS 和静态资源的缓存时间不一致:有的 pad 在插件更新前打开过,旧的pad.js或headings.css还在缓存里;有的是走了 CDN,CDN 节点间资源版本没同步。

解决方式是在自定义 CSS 里不要使用内联样式或临时写在控制台的测试样式,而是把样式固定到皮肤文件,并在静态资源响应头里设置合理的max-age或ETag。如果只是临时验证,就用无痕窗口开 pad;因为无痕窗口不会带入本地缓存,能看到最干净的加载结果。这个方法也是我判断“到底是代码问题还是缓存问题”的首选手段。

5.3 现象三:升级 Etherpad 后 ep_headings2 全部失效

Etherpad 版本升级后,插件失效非常常见。原因不一定是插件作者不维护了,而是 Etherpad 内核的 hooks 名称或客户端初始化流程变了;比如某个编辑器加载顺序调整后,ep_headings2 的aceInitInnerdocbodyHead再也没有被调用。现象表现为:插件还在列表里,但工具栏按钮消失、标题样式变成普通文本。

解决时不要急着卸载重装,先查 Etherpad 的升级日志和当前版本对应的 hook 列表,再去 ep_headings2 的 npm 页面看它声明支持的引擎范围。如果插件长期不更新,可以考虑转向维护更活跃的同类标题插件。最好的预防办法是把 Etherpad 版本固定在某个小版本,不要在团队协作期间随意执行跨大版本升级。

5.4 现象四:从 pad 复制标题到普通网页,标签变成正文

有些用户习惯把编辑区内容全选复制,然后粘贴到公司文档系统,结果标题层级全部丢失。这不是 bug,而是 Etherpad 编辑器在设计上不承诺“复制即得 HTML 结构”,它复制到剪贴板的内容更多是纯文本或带基础格式的富文本,标题属性没有进入剪贴板渲染。

解决墙上要靠导出。先从 pad 导出 HTML,再将 HTML 粘贴到目标编辑器,或者直接用目标系统的导入文件功能。如果目标系统支持 Markdown,也可以用 ep_markdown 之类的插件先把 pad 转成 Markdown,再复制标题的#标记。这个操作要多走一步,但能避免在段落样式上返工。

5.5 现象五:与其它插件冲突导致编辑器白屏

编辑器白屏通常不是 ep_headings2 单独的问题,而是多个插件在客户端初始化阶段互相踩。比如两个插件都往aceInitInnerdocbodyHead里插入内容,或者都在aceAttribsToClasses里注册同名属性,最终导致 JS 异常。现象上,pad 页面能打开,但编辑区一直是空白或转圈。

解决方式靠二分法:先临时停用其它 ep_ 插件,只保留 ep_headings2,看白屏是否消失;如果正常,再逐个启用来找出冲突源。找到冲突插件后,有两种处理:一是放弃其中之一,二是查看两个插件的 README 里有没有提到共存限制,必要时在启动参数里调整加载顺序。这个排查方式不优雅,但对付插件黑匣子最有效,我每次遇到白屏都这么操作,省下的时间足够重新编译一个插件。

6. 一个贯穿始终的验证技巧:用浏览器开发者工具检查标题属性

不管你是刚装好 ep_headings2,还是已经在线跑了一段时间,最值得掌握的验证手段不是看按钮,而是打开开发者工具直接查标题属性。编辑一个 pad,选中某个标题行,在控制台执行:

const lines = document.querySelectorAll('#innerdocbody .ace-line'); lines.forEach(line => { const cls = line.getAttribute('class') || ''; if (cls.includes('heading') || cls.includes('h1') || cls.includes('h2')) { console.log(line.textContent.slice(0, 30), cls); } });

这段脚本会把当前 pad 里所有被插件标记为标题的行打印出来。你能清楚看到哪些行带着标题类名,哪些没有;也能对比不同标题级别的类名规律,进而写出更精准的自定义 CSS。把它当成例行检查,比反复问“为什么我的标题不同”要快得多。

再进一步,我还习惯在验证后追加一个导出检查:每次改完 CSS 或升级版本,都用 API 导出一份 HTML,然后 grep 一下导出文件里的标题标签:

grep -E '<h[1-6]' pad.html | head -20

如果导出文件里有完整的<h1>到<h6>,说明属性到结构的链路是通的;如果这里得到的是空结果,那前端再好看也是假的。这套组合拳帮我避免过很多次“看着没问题,交付后才发现标题全丢”的尴尬。

最后说一个我这几年养成的习惯:装完 ep_headings2,不要去折腾过多神秘参数。先让它默认跑起来,把标题层级、快捷键、导出这三件事验证一遍,再考虑定制。插件本身的默认行为通常已经足够稳健,真正出问题的时机大多出现在你试图“优化”它的时候。希望这套验证思路和踩坑记录能帮到你,让你的 Etherpad 标题功能少一点玄学,多一点可维护性。

本文还有配套的精品资源,点击获取

返回列表