如何实现contenteditable/Markdown与EJS混合的半可视化模板编辑?
哈哈,这个需求我前段时间刚折腾过!要平衡可视化编辑和EJS模板的可编程性,确实不能直接用原生contenteditable或普通Markdown编辑器,给你几个亲测有效的解决方案:
1. 拆分编辑区:可视化内容 + 独立代码块
这是最稳妥的方案,完全避免两种内容互相污染。核心思路是把编辑器分成两个独立区域:
- 可视化内容区:用成熟的Markdown编辑器(比如SimpleMDE)让用户写富文本内容,不用管HTML结构;
- EJS代码区:用代码编辑器(比如CodeMirror)让用户写逻辑片段,支持语法高亮;
给用户加一个「插入EJS片段」的按钮,用户可以在Markdown内容里插入自定义占位符(比如{{ejs-1}}),最后后台把占位符替换成对应的EJS代码块,生成完整的模板。
举个例子:
用户在Markdown区写:
# 欢迎{{ejs-username}} {{ejs-admin-note}}
在代码区写两个片段:ejs-username:<%= user.name %>ejs-admin-note:
<% if(user.isAdmin) { %> > 管理员专属提示:请定期清理数据 <% } %>
后台合并后就是标准EJS:
<h1>欢迎<%= user.name %></h1> <% if(user.isAdmin) { %> <blockquote>管理员专属提示:请定期清理数据</blockquote> <% } %>
2. 增强版contenteditable:实时过滤非法标签
如果一定要用单一的contenteditable编辑器,那必须监听输入事件,实时清理破坏EJS结构的自动生成标签(比如<div>)。
核心步骤:
- 让用户用特定标记包裹EJS代码(比如
<% ... %>,这个是EJS原生语法,用户容易理解); - 监听编辑器的
input事件,每次输入后遍历DOM,把EJS代码段外面的冗余标签去掉; - 处理光标位置,避免用户输入时光标跳走(这个是坑,一定要做)。
示例代码(简化版):
const editor = document.getElementById('template-editor'); // 保存光标位置的工具函数 function saveCursorPosition(el) { const selection = window.getSelection(); const range = selection.getRangeAt(0); const preSelectionRange = range.cloneRange(); preSelectionRange.selectNodeContents(el); preSelectionRange.setEnd(range.startContainer, range.startOffset); return preSelectionRange.toString().length; } // 恢复光标位置的工具函数 function restoreCursorPosition(el, pos) { const selection = window.getSelection(); const range = document.createRange(); range.setStart(el.firstChild, pos); range.collapse(true); selection.removeAllRanges(); selection.addRange(range); } editor.addEventListener('input', () => { const cursorPos = saveCursorPosition(editor); const parser = new DOMParser(); const doc = parser.parseFromString(editor.innerHTML, 'text/html'); // 找出所有包含EJS代码的节点 const ejsMatches = Array.from(doc.body.childNodes).reduce((acc, node) => { if (node.nodeType === Node.TEXT_NODE && /<%.*%>/.test(node.textContent)) { acc.push(node); } else if (node.nodeType === Node.ELEMENT_NODE) { const text = node.textContent; if (/<%.*%>/.test(text)) { acc.push(node); } } return acc; }, []); // 清理EJS代码外的冗余标签 ejsMatches.forEach(node => { if (node.tagName === 'DIV' && /<%.*%>/.test(node.textContent)) { const textNode = document.createTextNode(node.textContent); node.replaceWith(textNode); } }); editor.innerHTML = doc.body.innerHTML; restoreCursorPosition(editor, cursorPos); });
3. 自定义简化语法 + 编译转换
如果不想让用户直接接触EJS的语法,可以设计一套更简单的可视化友好语法,用户用Markdown编辑器写内容时插入这些简化标签,最后后台编译成标准EJS。
比如自定义语法规则:
[[var]]→<%= var %>[[if condition]] ... [[/if]]→<% if(condition) { %> ... <% } %>[[each item in list]] ... [[/each]]→<% list.forEach(item => { %> ... <% }) %>
用户输入的内容:
# 欢迎[[username]] [[if isAdmin]] > 管理员你好,当前用户数:[[userCount]] [[/if]]
后台用正则编译成EJS:
<h1>欢迎<%= username %></h1> <% if(isAdmin) { %> <blockquote>管理员你好,当前用户数:<%= userCount %></blockquote> <% } %>
这个方案对用户门槛最低,完全不用懂EJS,而且不会被Markdown或contenteditable的标签干扰。
4. 用可定制的富文本编辑器:自定义节点类型
如果想要更流畅的混合编辑体验,可以用Slate.js这种高度可定制的富文本编辑器,自定义一个「EJS代码块」节点类型,让这个节点的内容不会被富文本的格式化逻辑影响。
具体做法:
- 定义一个
EjsBlock节点,渲染成一个带语法高亮的代码输入框; - 配置编辑器,当用户输入
<%时自动切换到EJS代码节点; - 导出内容时,直接把EJS节点的内容和其他富文本内容拼接成标准EJS模板。
这种方案体验最好,但需要一定的前端开发成本,适合对编辑器体验要求高的场景。
总结一下:
- 追求简单稳妥:选拆分编辑区方案;
- 追求用户零门槛:选自定义简化语法方案;
- 必须单一编辑器:选增强版contenteditable方案;
- 追求极致体验:选Slate自定义节点方案。
内容的提问来源于stack exchange,提问作者Merc

