
简介这是Etherpad的标题插件ep_headings2面向使用Etherpad进行多人协作文档编辑的团队或个人可将H1等各级标题应用到文档中改善长文档的结构与可读性。插件由Etherpad基金会维护具备测试覆盖、代码检查、多语言翻译、导入导出与复制粘贴支持并能在编辑区显示当前活动标题。压缩包体积仅八十六千字节共包含五十四份文件其中语言翻译文件占三十八份覆盖中英德法等多种语言另有核心逻辑脚本、样式表、持续集成配置、说明文档和界面截图整体结构清晰便于二次开发。该资源目前已有二百九十人学习下载适合熟悉JavaScript、想了解Etherpad插件机制或需要部署标题功能的开发者参考既能直接安装使用也能借鉴其模块划分与测试方案。1. 从“纯文本”到“结构化”ep_headings2 解决的不只是标题在 Etherpad 里协作写长文档最让人抓狂的是没有标题。默认编辑器只有加粗、斜体、无序列表、对齐这些基础能力你写了一个 12 屏的会议纪要想跳回第三屏找“风险清单”那段只能靠 CtrlF 慢慢扫。ep_headings2 就是为了补上这个缺口而生的社区插件它给 Etherpad 增加 H1/H2/H3 这样的层级标题按钮点一下当前行就变成标题文档从“一坨纯文本”变成“有骨架的结构化内容”。它的价值不在“多一个按钮”而在于让后续的导航、查找、导出都有了锚点。适合三类人用 Etherpad 维护团队知识库的把 Etherpad 当会议纪要工具的以及想给 Etherpad 做二次开发、需要理解其行属性机制的 JavaScript 工程师。2. 安装与激活npm 安装、依赖检查与工具栏的三种确认方式2.1 安装前的环境核对Etherpad 版本与 ep_ 前缀Etherpad 是一个 Node.js 应用服务端和客户端都是 JavaScript插件通过 npm 包分发。ep_headings2 这个包名继承了 Etherpad 插件的ep_前缀看到这个前缀插件系统就能把它和普通 npm 包区分开。安装前先确认几件事你的 Etherpad 安装在哪个目录Node.js 和 npm 是什么版本以及你有没有改过 Etherpad 的默认端口。node -v npm -v我一般会在 Etherpad 的根目录里跑这两条命令比如/opt/etherpad。如果 Node 版本低于 12有些新安装的插件可能不认你的环境。npm 版本只要不是太老5.2 以上都行就能正常工作。另外一点经验不要在别的目录下运行安装命令npm 装包会先找当前目录的 package.json装错地方等于白装。Etherpad 对插件的版本要求不算严格但大版本升级时 API 会有变化。安装前最好能确认一下你的 Etherpad 版本打开/admin页面能看到或者在根目录执行cat src/package.json | grep version看到版本号后去 ep_headings2 的 npm 页面看一眼它的 peerDependencies 或 README 里写的兼容范围。社区插件更新可能滞后于 Etherpad 主版本这是一个值得提前记住的坑。2.2 安装与激活npm 命令、重启服务、检查插件列表安装命令很简单前提是你已经进入 Etherpad 的根目录。用--save参数把依赖写进 package.json这样以后部署或者重构时不会丢包。cd /opt/etherpad npm install ep_headings2 --save这里--save表示将包名和版本号记录到 package.json 的 dependencies 里。Etherpad 的插件机制是启动时扫描 node_modules 下所有ep_开头的包所以只要装进 node_modules 并重启服务插件就会被自动加载。重启方式取决于你的部署方式。直接前台跑的用 CtrlC 后重新执行./bin/run.sh如果用了进程守护一般是重启守护进程比如 pm2pm2 restart etherpad重启后打开浏览器访问/admin/plugins在插件列表里找到 ep_headings2。如果状态是 active 或 enabled说明插件已经激活。有些版本会显示一个开关确保它处于打开状态。如果这里没出现 ep_headings2多半是安装到了错误的目录或者 npm 没有真正把它装进 Etherpad 的 node_modules 里。2.3 验证插件生效三种确认方式第一种方式就是上面说的/admin/plugins这是最确定的它直接告诉你插件有没有被系统识别。第二种方式是看启动日志。在启动 Etherpad 的终端里通常能看到类似 “Plugin ep_headings2 registered” 或 “Found plugin ep_headings2” 这样的记录。不同版本措辞不同但必然会有。如果你是用 pm2 跑的用pm2 logs etherpad就能看到。第三种方式是打开任意一个 pad看工具栏有没有出现 H1、H2、H3 按钮。注意Etherpad 的工具栏有缓存尤其是浏览器端所以第一次安装后如果没看到按钮先按 CtrlShiftR 强制刷新。如果按钮还是没有按 F12 打开开发者工具切到 Console 标签页看有没有 JavaScript 报错。很多插件失效的根源是客户端脚本报错而不是服务端没加载。3. 配置与使用工具栏按钮、标题层级与样式定制3.1 默认配置与工具栏按钮ep_headings2 装上之后默认会给 Etherpad 的工具栏加几个按钮。不同版本的 UI 不完全一样有的是 H1、H2、H3 三个小方块按钮有的是一个下拉列表让你选“标题 1 / 标题 2 / 正文”。作用都一样——把当前光标所在的行或者你选中的多行文本标记成对应级别的标题。使用逻辑和大多数编辑器一致把光标移到某一行的任意位置点 H2这一行就变成二级标题。想取消标题就再点一次 H2或者先选成正文。需要注意的是Etherpad 的标题是基于“行属性”实现的不是 Word 里那种段落样式。它标记的是整行不管你选中几个字变化的都是整行。这里有个和普通文档编辑器不一样的细节Etherpad 的文档模型是一行行带属性的文本属性挂在行的开端。所以 ep_headings2 的按钮动作本质是修改当前行的属性集合。这个机制决定了它的两个特点一是样式只影响整行不能在一行内混排标题和正文二是当行已经有了其它属性比如列表时标题属性可能和它冲突。3.2 标题层级与快捷操作标题层级默认是 H1 到 H3你可以在插件代码里看到它定义的三个等级。超出 H3 的深度在页面展示上通常没有对应样式所以不需要去改默认值。实际使用中我习惯把 H1 用作文档大章节H2 用作小节H3 用作具体要点这样脑内导航成本最低。Etherpad 本身不提供“Ctrl1 变成标题”这样的快捷键ep_headings2 也没有默认绑定。如果你不喜欢频繁点鼠标可以在 Etherpad 的客户端 hook 里自己加。常见做法是写一个小小的插件拦截键盘事件把 Ctrl1/2/3 映射到对应命令。// static/js/headings_shortcut.js window.addEventListener(keydown, function (e) { if (!e.ctrlKey || e.shiftKey) return; if (e.key 1 || e.key 2 || e.key 3) { e.preventDefault(); var level parseInt(e.key, 10); // pad.ace 是 Etherpad 编辑器暴露的 API 对象 if (pad pad.ace) { pad.ace.doInsertLineAttribute(heading level); } } });这段代码在按键发生时先阻止浏览器默认行为再通过pad.ace的doInsertLineAttribute方法设置标题属性。注意这里heading level是 ep_headings2 内部使用的属性名在旧版本里也可能是h level以实际源码为准不同版本有差异。如果你不想冒这个险也可以直接调用编辑器的 execCommand但那样要写更多的分支判断。3.3 用 CSS 定制标题外观Etherpad 的样式覆盖机制ep_headings2 默认的标题样式是加粗加放大视觉上中规中矩。但团队知识库通常有统一的视觉规范你可能希望 H1 字号 24px 且带下划线H2 字号 18px 且颜色深灰。这时候就需要覆盖插件自带的 CSS。Etherpad 提供了一个全局自定义样式的入口在 Etherpad 根目录下建一个src/static/custom/目录里面有custom.css这个文件最后加载优先级最高。往里面写样式即可。.heading1 { font-size: 28px; font-weight: 700; color: #1a1a1a; border-bottom: 2px solid #d0d0d0; padding-bottom: 4px; } .heading2 { font-size: 20px; font-weight: 600; color: #333333; } .heading3 { font-size: 16px; font-weight: 600; color: #555555; }Etherpad 的 DOM 结构里每一行文字外面有一个div.line标题行内的文字会被包在带特殊 class 的span里常见命名就是上面代码里的heading1、heading2、heading3。不同版本可能有细微差异最稳妥的验证方法是在浏览器里打开一个 pad光标点到标题行按 F12 选中该行看实际渲染出来的 class 名。看到什么就在 CSS 里写什么。这一点很重要不要拿网上搜到的类名直接套自己看一下最省事。# 修改后重启 Etherpad或看看是否支持热加载 ./bin/run.sh修改 custom.css 之后需要重启服务才生效除非你在开发模式下运行。注意custom.css 只影响你自己这个 Etherpad 实例的显示效果。如果你导出 PDF 或 HTML导出器要用另一套模板这个后面避坑章节细说。4. 避坑ep_headings2 使用中的常见问题与排查4.1 安装后工具栏没有标题按钮现象npm 安装成功重启了服务/admin/plugins 里也显示 active但工具栏没有 H1/H2/H3或者只有按钮点了没反应。原因分三种一是浏览器缓存了旧的客户端脚本工具栏渲染不出来二是插件版本和你的 Etherpad 版本不兼容客户端 JS 抛异常导致按钮初始化中断三是你在 Etherpad 的工具栏配置里自定义过toolbar的设置把插件按钮过滤掉了。解决先按 CtrlShiftR 强制刷新排除缓存。如果还是不行打开浏览器 Console看有没有Cannot read property或者Uncaught TypeError之类的报错。报错信息里如果包含 ep_headings2 的文件路径就是版本兼容问题去 npm 上装最新版或者干脆装一个和你的 Etherpad 版本匹配的旧版。如果控制台干净去检查settings.json里有没有toolbar配置看看是不是你只保留了几个人性化按钮把插件新增的按钮弄丢了。4.2 标题层级错乱从 Word 粘贴文本后属性打架现象用户从 Word 或者网页里复制了一段内容直接粘贴到 pad然后把某一行点成 H2看着没问题。但其他人随后编辑时这一行的标题时而消失时而变成正文。或者更常见的粘贴进来的内容自带行首缩进、引用符、列表符标题属性一叠加整行就乱了。原因Etherpad 的行属性不是一个独立样式它由多个属性组成。从 Word 粘贴时Etherpad 的过滤器会尽力保留文本但也会写入一些格式属性比如list、indent、align。标题属性和这些属性重叠时某些渲染逻辑会把后面的属性覆盖前面的导致标题的视觉样式被“顶掉”。解决粘贴外部内容前先在纯文本编辑器比如记事本里过一遍或者使用浏览器地址栏的javascript:void(0)伪装不更实用的办法是粘贴后全选点一下 H1 再点回正文强制清掉行属性然后再重新设置标题。如果团队里经常有人贴内容我建议在 Etherpad 的settings.json里把pasteElement等粘贴过滤参数调严格一些或者直接开启纯文本粘贴模式。具体参数每个版本不一样可以在/admin/settings里找找看。4.3 导出 HTML/PDF 时标题样式丢失现象pad 里明明有清晰的 H1/H2/H3用导出功能生成 HTML 或 PDF文字变成了普通段落或者字体大小和 pad 上完全不一样。原因Etherpad 的导出模块不能自动识别 ep_headings2 添加的行属性。它默认的导出模板只认识 Etherpad 核心预设的格式比如bold、italic、list。自定义属性需要插件自己在导出钩子里做映射而很多早期版本的 ep_headings2 只加了编辑器的渲染没把导出逻辑写完整。解决如果只是偶尔导出最快的办法是导出为 HTML然后在浏览器里打开这个 HTML用浏览器的“打印为 PDF”功能重新生成。HTML 里标题标签如果还是span而不是h1那就需要手动调整一下。如果你需要频繁导出可以自己写一个导出扩展在exports.onExport钩子里把行属性转换成语义化 HTML 标签。这个方案对后端点 JavaScript 水平有一定要求但能彻底解决问题。4.4 多人同时编辑时标题被覆盖现象两个用户同时处理同一个段落甲点 H1 把它变成标题乙紧接着输入几个字甲的标题就没了变成普通文本而且 pad 里所有协作者都看到这个变化。原因Etherpad 是实时协同编辑没有行级锁。它的同步机制会对每个字符的操作做合并但同一行的属性变更可能产生冲突。后提交的属性操作会覆盖先提交的具体到标题属性上乙的输入操作没有标题属性合并后就把甲的标题属性冲掉了。解决只能靠规范或保护机制不能靠插件本身。我和团队约定设置标题时先和附近人说一声“我动第 3 行”或者干脆把文档按章节拆到多个 pad 里。想从技术上避免可以给标题行加只读保护自己写一个 hook在aceEditEvent里检查被编辑的行是不是标题行如果是就不允许非标题属性的修改。这样做会增加协同复杂度我建议小团队先用流程规范解决不要一上来就上技术锁。4.5 升级 Etherpad 后插件失效现象升级 Etherpad 到一个大版本后原来分类好好的标题突然在工具栏里消失或者点击 H1 没有任何效果服务端日志出现一堆TypeError。原因Etherpad 主版本升级会重构前端事件 API尤其是pad.ace暴露的方法签名。ep_headings2 是社区维护的插件适配速度和官方主版本升级速度不一定同步升级前没看兼容性说明就会踩坑。解决升级前先备份整个 Etherpad 目录和数据库再检查 ep_headings2 的 npm page 上有没有针对新版本的 Release。升级后立刻打开一个测试 pad把标题按钮、样式、快捷键全部点一遍。如果没适配优先回滚回滚不了就看看插件是否在持续维护不要盲目升级主版本。从那以后我每次升级 Etherpad 都强制走一遍这个流程先看插件维护状态再动升级已经养成习惯了。5. 进阶把标题变成导航给长文档加一个自动目录有了可靠的多级标题之后下一步就是利用标题的语义做点事情。我给自己的 Etherpad 加了一个自动目录插件文档加载时扫描所有标题行在编辑器右侧生成一个可点击的目录树点击目录项就跳转到对应位置。核心逻辑并不复杂关键是监听标题变化并刷新目录。// 在插件目录下创建 ep_toc/static/js/toc.js function buildTOC() { var padBody document.querySelector(#outerdocbody); var headings padBody.querySelectorAll(.heading1, .heading2, .heading3); var toc document.getElementById(pad-toc); toc.innerHTML ; headings.forEach(function (el) { var level el.className.match(/heading(\d)/)[1]; var item document.createElement(a); item.className toc-item toc-level- level; item.textContent el.textContent; item.addEventListener(click, function () { el.scrollIntoView({ behavior: smooth, block: center }); }); toc.appendChild(item); }); }这里我直接遍历 DOM 里的标题节点用className解析出标题级别生成带缩进的目录项。scrollIntoView是浏览器原生 API不需要额外引入库。要注意的是Etherpad 的编辑器区域很特殊它的滚动容器是#innerdocbody而不是整个窗口所以你可能需要调整scrollingElement的获取方式比如用document.querySelector(#innerdocbody).scrollTo。这个细节不同版本差异较大我当初就卡在跳转位置不准上最后是用getBoundingClientRect加手动计算偏移解决的。目录的样式可以放在 custom.css 里固定到编辑区右侧用position: fixed加right: 20px。这样用户滚动文档时目录始终可见。标题修改、删除、新增时目录需要跟着刷新。Etherpad 的aceEditEventhook 会在编辑操作后触发你可以在那里调用buildTOC()但要加防抖不然多人高频输入时目录会频繁重绘。div idpad-toc/div把这段 HTML 放在 Etherpad 的index.ejs模板里或者用一个小插件在documentReady时注入到页面。如果你不想动主模板用 JavaScript 动态创建div再appendChild到 body 也一样关键是别忘记初始化调用一次buildTOC()。这段实践让我真正理解了 ep_headings2 的价值它不只是一个格式按钮而是给文档建立了机器可读的结构。有了这个结构目录、跳转、书签都能做。做目录那段时间我翻了很多 Etherpad 插件源码最深的体会是——Etherpad 的插件机制很灵活但每一次升级都可能把 hook 行为改掉所以任何二次开发都要在主版本升级后重新验证。从那以后我每次升级 Etherpad 都强制走一遍检查先备份、再看插件兼容、最后开测试 pad 逐项验证标题按钮、样式、导出和目录功能。希望这份折腾经验帮到你。本文还有配套的精品资源点击获取