当前位置: 首页
前端开发
电子保函模板编辑器实战:基于WangEditor的富文本定制

电子保函模板编辑器实战:基于WangEditor的富文本定制

热心网友 时间:2026-07-17
转载

基于WangEditor和Slate js定制了电子保函模板编辑器,扩展了变量标签、附页标记、边框容器、页眉容器等5类自定义节点,打通编辑态与生成态转换,实现业务人员自主可视化编辑模板并生成FTL报文,减少开发依赖。

我用 WangEditor 做了个模板编辑器,说说踩过的坑

1. 为什么要做这个编辑器

先说说这个项目的背景。保险保单、保函这类业务,需要适配全国各地的差异化要求,模板数量多、更新频繁。以前是怎么处理的呢?业务提需求,开发人员在代码里硬编码维护。一来一回,沟通成本高,理解上还容易有偏差。更要命的是,每次改模板都得等发版才能生效,响应速度完全跟不上业务节奏。

电子保函模板编辑器实战一:基于 WangEditor 的富文本编辑器定制实践

所以,这次要做的,是一个让业务人员能自主配置的模板编辑器。核心目标很明确:业务人员自己可视化编辑模板、自己测试、自己管理版本、自己发布,不用再依赖开发人员介入。

这个编辑器的具体目标是让业务人员维护“报文模板”。模板里不只有普通文字,还会有各种变量,比如投保人姓名、保单金额这些。用户编辑完之后,系统得把内容转换成 FreeMarker 模板,也就是 FTL 报文,交给后端去渲染。

要解决的问题其实很明确:

  • 基础排版得有:字体、字号、颜色、加粗、对齐、缩进这些。
  • 变量得能插入,而且看起来不能像一串 {{xxx}},得像个正常的标签。
  • 变量插入完不是终点,最后生成 FTL 的时候还要能变回去。
  • 模板有附页,得能标记分页位置。
  • 页眉有两张图片,左右各一张,还能单独删掉。
  • 有些内容需要边框,编辑时看着是边框,生成时得转成 FTL 能识别的结构。

这么一看,问题就不是“找个编辑器让用户打字”了,而是要找一个能深度扩展、能控制文档结构、能打通编辑态和生成态的编辑器。

2. 为什么选 WangEditor

选 WangEditor 不是因为它有多完美,而是它刚好满足了几个关键条件:

  • 基础排版功能现成的,不用从零写。
  • 有 Vue 3 组件,接入起来方便。
  • 底层是 Slate.js,文档是节点树结构,不是一团不可控的 HTML。

特别是第三点,Slate 的节点树结构让我能把“变量标签”“附页标记”这些业务概念变成编辑器真正认识的节点。

不过选了 WangEditor 不代表后面就顺利了。真正花时间的地方,都在它的扩展边界上:哪些节点要做成 inline,哪些要做成 void,哪些内容要序列化,哪些只在编辑态展示。

3. 这个编辑器里扩展了哪些业务元素

根据源码里的实现,编辑器主要扩展了 5 类自定义元素:

功能Slate type节点特点作用
变量标签variableinline void编辑态展示蓝色标签,生成态转换为变量占位符
附页标记followerMarkerblock void表示附页开始位置,生成时带分页样式
边框容器borderBoxblock 容器插入上下边框或全边框区域
页眉容器headerContainerblock 容器页眉布局容器
页眉图片容器headerImageContainerblock void放置左右两张页眉图片,并支持单独删除

这几个节点的性质不一样,所以处理方式也不一样。

比如变量标签要和普通文字排在同一行,所以它是 inline;但它不能让用户把光标放进标签内部编辑,所以它又是 void。附页标记和页眉图片容器是独立块级内容,所以它们不是 inline。页眉图片容器还要避免内部图片被 Slate 当成普通子节点处理,因此也被标记成 void。

handleCreated 里,编辑器会重写 editor.isVoideditor.isInline

 复制代码editor.isVoid = (element) => {
    const type = (element as { type?: string }).type
    if (type === 'variable' || type === 'followerMarker' || type === 'headerImageContainer') return true
    return originalIsVoid(element)
}editor.isInline = (element) => {
    const type = (element as { type?: string }).type
    if (type === 'variable') return true
    if (type === 'followerMarker' || type === 'borderBox' || type === 'headerContainer' || type === 'headerImageContainer') return false
    return originalIsInline(element)
}

这一步很重要,因为它决定了这些业务元素在 Slate 文档树里的行为。

4. 自定义元素要打通三条链路

在 WangEditor 里,一个自定义元素不是只写一个渲染函数就结束了。它至少要打通三条链路:

 复制代码registerRenderElem     编辑态怎么显示
registerParseElemHtml  HTML 怎么还原成 Slate 节点
registerElemToHtml     Slate 节点怎么序列化成 HTML

拿变量标签来说,编辑态要显示成一个不可编辑的蓝色标签;保存 HTML 时要带上 data-variabledata-label 和样式字段;再次加载 HTML 时,又要能根据这些属性还原成 type: 'variable' 的 Slate 节点。

也就是说,这里不是简单地“插入一段 span”。如果只插入普通 HTML,编辑器下一次解析、保存、生成 FTL 的时候都很容易丢信息。变量必须成为编辑器文档树里的正式节点。

5. 变量标签为什么要保存自己的样式

变量是这个编辑器里最麻烦、也最关键的元素。

在编辑器里,变量看起来是一个标签,例如“投保人姓名”。但生成模板时,它要变成类似 {{xxx}} 的占位符。更进一步,如果用户给这个变量设置了字号、颜色、加粗,那么这些样式也要跟着变量一起进入最终 FTL。

源码里的做法是:变量节点自己保存样式字段,例如:

 复制代码{
    type: 'variable',
    label,
    prop,
    fontSize,
    color,
    fontWeight,
    fontFamily,
    fontStyle,
    textDecoration,
    children: [{ text: '' }],
}

序列化成 HTML 时,这些字段会写成 data-font-sizedata-colordata-font-weight 等属性;生成报文前,再通过 convertVariablesForGenerate 把它们编码进占位符:

 复制代码{{prop}}
{{prop||font-size:14pt;color:#000;font-weight:bold}}

这么做的原因很实际:变量是 void 节点,Slate 普通的 mark 机制不会自然作用到它内部的空文本节点上。如果只依赖选区上下文,变量一旦复制、移动、保存再回显,样式就容易丢。

所以代码里拦截了 editor.addMarkeditor.removeMark。当选区覆盖变量节点时,不是只给普通文本加 mark,而是用 Transforms.setNodes 直接更新变量节点本身:

 复制代码editor.addMark = (key: string, value: unknown) => {
    const varPaths = getSelectedVariablePaths()
    if (varPaths.length > 0) {
        varPaths.forEach((varPath) => {
            if (key === 'bold') {
                Transforms.setNodes(editor, { fontWeight: value ? 'bold' : '' }, { at: varPath })
            } else if (key === 'color' || key === 'fontSize' || key === 'fontFamily') {
                Transforms.setNodes(editor, { [key]: value }, { at: varPath })
            }
        })
    }
    originalAddMark.call(editor, key, value)
}

这部分逻辑看起来有点绕,但解决的是一个很具体的问题:变量不是普通文字,但用户对它的操作又应该像普通文字一样自然。

6. 编辑态和生成态之间怎么转换

这个系统有两个状态:

  • 编辑态:变量是可视化标签,附页是可见标记,页眉图片可以删除。
  • 生成态:变量要变成占位符,附页要变成分页结构,边框和页眉要变成适合 FTL 输出的 HTML。

变量的转换分两步。

编辑态生成报文时,getGenerateContent 会先拿到编辑器 HTML,然后调用 convertVariablesForGenerate,把带 data-variable 的元素替换成 {{prop}}{{prop||style}}

从生成内容回显到编辑器时,setGenerateContent 会调用 restoreVariablesFromGenerate。它先用 setHtml 加载内容,再在 Slate 文档树中查找 {{prop}} 占位符,找到一个就删除文本,再插入一个真正的 variable 节点。

这里没有直接拼一段 HTML 再塞回编辑器,而是通过 Slate API 插入节点。这样做更稳,因为后续选中、高亮、工具栏格式联动都依赖这个节点类型。

7. 生成 FTL 时做了哪些处理

最终生成 FTL 的逻辑在 ftlGenerator.ts 里。

它不是简单把 {{prop}} 替换成 ${prop!},还做了几类处理:

  • 普通变量会转成 FreeMarker 表达式,例如 ${xxx!}
  • 数组变量会根据变量配置识别数组路径,生成 <#list ... as info>
  • 动态变量会根据配置替换成对应字符,例如分隔符、序号等。
  • 带样式的变量会用 包起来,保证变量值渲染出来后还有字号、颜色等样式。
  • 附页标记会转换成带 page-break-before:always 的分页段落。
  • 边框容器会从 div 转成 table,因为最终 FTL 渲染环境里 table 比 div border 更稳定。
  • 页眉图片容器也会从 flex div 转成 table 布局,避免渲染端对 flex 支持不完整导致左右图片错位。

这也是为什么编辑器里要保存比较完整的结构信息。编辑态可以用更适合交互的 DOM,生成态则要换成更适合模板渲染的结构。

8. 工具栏为什么用了 DOM 注入

项目里没有完全依赖 WangEditor 默认工具栏,还额外加了几个按钮:

  • A-A+:按固定档位缩小或放大字号。
  • 上下边框:插入只有上下边框的区域。
  • 全边框:插入完整边框区域。
  • 页眉:插入左右页眉图片。

这些按钮是通过 DOM 注入到工具栏里的,不是通过 Boot.registerMenu 注册菜单。

源码注释里写得很清楚:这样做是为了避开开发环境 HMR 下菜单重复注册的问题。DOM 注入本身也做了防重复判断,比如发现 .custom-font-size-btn 已经存在就直接返回。

这不是最“框架化”的写法,但在这个项目里是一个比较务实的选择。它绕开了菜单注册表的重复注册问题,同时又能复用 WangEditor 原来的工具栏布局。

9. 页眉图片容器里的几个细节

页眉图片容器看起来只是左右两张图片,但实现里有几个容易踩坑的点。

第一,删除按钮要能点,但图片容器又不能破坏 Slate 的光标定位。代码里通过 pointer-events 做了分层:图片区域整体先设为不可响应,图片 wrapper 和删除按钮再恢复响应。这样中间空白区域不会抢事件,删除按钮又能正常点击。

第二,删除一侧图片后,另一侧图片不能跑到对面去。因为编辑态用的是 flex + space-between,如果直接把图片 display:none,布局空间就没了。源码里用的是 visibility:hidden 的占位元素,保留宽度但不显示内容。

第三,headerImageContainer 是 void 节点,所以反解析 HTML 时必须强制返回:

 复制代码children: [{ text: '' }]

不能把内部的 当成 Slate 子节点保留下来。图片数据应该存在 leftImageSrcrightImageSrc 字段里,而不是存在 children 里。

10. 变量选中和工具栏联动

变量是 contenteditable="false" 的标签,用户点它的时候,浏览器不会像普通文字那样自然选中一段文本。

为了让工具栏的加粗、字号、颜色能作用到变量上,代码里做了两件事。

一是维护 selectedVariablePath,记录当前选中的变量节点路径。渲染变量标签时,如果路径匹配,就加上 variable-tag--selected 类名,让用户看得出来当前选中了哪个变量。

二是在编辑区域监听 mousedown。当用户点到 .variable-tag 时,通过 DomEditor.toSlateNode 找回对应的 Slate 节点,再设置一个覆盖整个 void 节点的选区。这样工具栏命令才能找到这个变量节点,并更新它的样式字段。

这个体验细节很重要。否则用户看到的是一个标签,但实际点不上、改不了样式,就会觉得编辑器是坏的。

11. 这次定制的核心思路

这套实现的核心不是“把 WangEditor 改得很复杂”,而是把业务概念放进编辑器的文档模型里。

变量不是普通 span,它是 variable 节点。

附页不是一段蓝色文字,它是 followerMarker 节点。

页眉图片不是两张普通 img,它是带 leftImageSrcrightImageSrcheaderImageContainer 节点。

边框也不是随手加一个样式,它是后续生成 FTL 时要被转换的 borderBox 节点。

只要这些业务元素在编辑态就是明确的结构,后面的保存、回显、生成、校验才有基础。否则表面上编辑器能显示,真正进入模板生成链路时就会不断丢信息、错结构、补特殊逻辑。

12. 总结

这个模板编辑器的难点不在于富文本本身,而在于“编辑体验”和“模板生成”之间要保持一致。

WangEditor 提供了基础编辑能力,Slate.js 提供了可扩展的文档模型。项目真正做的事情,是把变量、附页、边框、页眉这些业务元素映射成自定义节点,再分别处理它们的渲染、解析、序列化和 FTL 生成。

最终结果是:用户在页面上看到的是比较自然的模板编辑体验,系统内部拿到的则是结构化的内容,能够稳定生成 FreeMarker 模板。

这也是这次选型和定制的出发点:不是为了做一个“更复杂的富文本编辑器”,而是为了让业务人员能维护模板,同时让系统仍然拿到可生成、可回显、可校验的模板内容。

来源:https://juejin.cn/post/7661546154107600936

游乐网为非赢利性网站,所展示的游戏/软件/文章内容均来自于互联网或第三方用户上传分享,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系youleyoucom@outlook.com。

同类文章
更多
JavaScript动态加载与执行脚本的完整使用方法详解

JavaScript动态加载与执行脚本的完整使用方法详解

动态加载JavaScript脚本主要有三种方式:通过document createElement插入标签兼容性最好;import()支持按需加载ES模块并返回Promise;eval或Function构造器适用于执行代码字符串但风险较高。使用时需注意避免重复加载、处理超时及服务端渲染环境。

时间:2026-07-17 07:07
箭头函数多行逻辑的书写优化

箭头函数多行逻辑的书写优化

箭头函数多行逻辑需用花括号包裹并显式return,否则返回undefined;单行仅限一个表达式自动返回;返回对象字面量必须加小括号;复杂逻辑应拆分为独立函数,避免硬塞导致代码混乱。

时间:2026-07-17 07:07
如何设计JavaScript函数缓存防止内存无限增长

如何设计JavaScript函数缓存防止内存无限增长

防止函数缓存导致内存无限增长,需主动控制容量和寿命。采用LRU策略自动淘汰闲置条目,设定TTL防止脏数据长期驻留,优化键生成避免冗余堆积,并提供手动清理接口配合业务生命周期。

时间:2026-07-17 07:07
JavaScript布尔值转换核心逻辑详解

JavaScript布尔值转换核心逻辑详解

JavaScript布尔转换遵循明确的真值与假值规则,仅有6个假值:false、0、空字符串、null、undefined、NaN,其余所有值包括空数组、空对象均为真值。显式转换推荐Boolean(),隐式转换常见于条件判断与逻辑运算,需注意??与||的区别。

时间:2026-07-17 07:07
如何利用Performance API识别长任务来源的完整方法详解

如何利用Performance API识别长任务来源的完整方法详解

利用性能API识别长任务来源,需结合时间锚点、开发者工具验证和主动埋点。通过性能观察者捕获基础信息,使用性能标记缩小范围,借助Chrome开发者工具定位代码行,最后通过主动埋点验证,将长任务映射到具体代码段。

时间:2026-07-17 07:07
热门专题
更多
刀塔传奇破解版无限钻石下载大全 刀塔传奇破解版无限钻石下载大全
洛克王国正式正版手游下载安装大全 洛克王国正式正版手游下载安装大全
思美人手游下载专区 思美人手游下载专区
好玩的阿拉德之怒游戏下载合集 好玩的阿拉德之怒游戏下载合集
不思议迷宫手游下载合集 不思议迷宫手游下载合集
百宝袋汉化组游戏最新合集 百宝袋汉化组游戏最新合集
jsk游戏合集30款游戏大全 jsk游戏合集30款游戏大全
宾果消消消原版下载大全 宾果消消消原版下载大全
  • 热门数据榜